Skip to content

Commit 6d870c9

Browse files
authored
doc: add migration guide (#653)
1 parent c261803 commit 6d870c9

File tree

2 files changed

+316
-17
lines changed

2 files changed

+316
-17
lines changed
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# Migration Guide from v0.0.x to v0.1.0
2+
3+
This document describes how to migrate an operator project built using Operator SDK `v0.0.x` to the project structure required by `v0.1.0`.
4+
5+
The recommended way to migrate your project is to initialize a new `v0.1.0` project, then copy your code into the new project and modify as described below.
6+
7+
This guide goes over migrating the memcached-operator, an example project from the user guide, to illustrate migration steps. See the [v0.0.7 memcached-operator][v0.0.7-memcached-operator] and [v0.1.0 memcached-operator][v0.1.0-memcached-operator] project structures for pre- and post-migration examples, respectively.
8+
9+
## Create a new v0.1.0 project
10+
11+
Rename your `v0.0.x` project and create a new `v0.1.0` project in its place.
12+
13+
```sh
14+
# Ensure SDK version is v0.1.0
15+
$ operator-sdk --version
16+
operator-sdk version 0.1.0
17+
18+
# Create new project
19+
$ cd $GOPATH/src/github.com/example-inc/
20+
$ mv memcached-operator old-memcached-operator
21+
$ operator-sdk new memcached-operator
22+
$ ls
23+
memcached-operator old-memcached-operator
24+
25+
# Copy over .git from old project
26+
$ cp -rf old-memcached-operator/.git memcached-operator/.git
27+
```
28+
29+
## Migrate custom types from pkg/apis
30+
31+
### Scaffold api for custom types
32+
33+
Create the api for your custom resource (CR) in the new project with `operator-sdk add api --api-version=<apiversion> --kind=<kind>`
34+
```sh
35+
$ cd memcached-operator
36+
$ operator-sdk add api --api-version=cache.example.com/v1alpha1 --kind=Memcached
37+
38+
$ tree pkg/apis
39+
pkg/apis/
40+
├── addtoscheme_cache_v1alpha1.go
41+
├── apis.go
42+
└── cache
43+
└── v1alpha1
44+
├── doc.go
45+
├── memcached_types.go
46+
├── register.go
47+
└── zz_generated.deepcopy.go
48+
```
49+
50+
Repeat the above command for as many custom types as you had defined in your old project. Each type will be defined in the file `pkg/apis/<group>/<version>/<kind>_types.go`.
51+
52+
### Copy the contents of the type
53+
54+
Copy the `Spec` and `Status` contents of the `pkg/apis/<group>/<version>/types.go` file from the old project to the new project's `pkg/apis/<group>/<version>/<kind>_types.go` file.
55+
56+
**Note:** Each `<kind>_types.go` file has an `init()` function. Be sure not to remove that since that registers the type with the Manager's scheme.
57+
```Go
58+
func init() {
59+
SchemeBuilder.Register(&Memcached{}, &MemcachedList{})
60+
}
61+
```
62+
63+
## Migrate reconcile code
64+
65+
### Add a controller to watch your CR
66+
67+
In a `v0.0.x` project you would define what resource to watch in `cmd/<operator-name>/main.go`
68+
```Go
69+
sdk.Watch("cache.example.com/v1alpha1", "Memcached", "default", time.Duration(5)*time.Second)
70+
```
71+
72+
For a `v0.1.0` project you define a [Controller][controller-go-doc] to watch resources.
73+
74+
Add a controller to watch your CR type with `operator-sdk add controller --api-version=<apiversion> --kind=<kind>`.
75+
```
76+
$ operator-sdk add controller --api-version=cache.example.com/v1alpha1 --kind=Memcached
77+
$ tree pkg/controller
78+
pkg/controller/
79+
├── add_memcached.go
80+
├── controller.go
81+
└── memcached
82+
└── memcached_controller.go
83+
```
84+
85+
Inspect the `add()` function in your `pkg/controller/<kind>/<kind>_controller.go` file:
86+
```Go
87+
import (
88+
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
89+
...
90+
)
91+
92+
func add(mgr manager.Manager, r reconcile.Reconciler) error {
93+
c, err := controller.New("memcached-controller", mgr, controller.Options{Reconciler: r})
94+
95+
// Watch for changes to the primary resource Memcached
96+
err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{})
97+
98+
// Watch for changes to the secondary resource Pods and enqueue reconcile requests for the owner Memcached
99+
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
100+
IsController: true,
101+
OwnerType: &cachev1alpha1.Memcached{},
102+
})
103+
}
104+
```
105+
Remove the second `Watch()` or modify it to watch a secondary resource type that is owned by your CR.
106+
107+
Watching multiple resources lets you trigger the reconcile loop for multiple resources relevant to your application. See the [watching and eventhandling][watching-eventhandling-doc] doc and the Kubernetes [controller conventions][controller-conventions] doc for more details.
108+
109+
#### Multiple custom resources
110+
111+
If your operator is watching more than 1 CR type then you can do one of the following depending on your application:
112+
- If the CR is owned by your primary CR then watch it as a secondary resource in the same controller to trigger the reconcile loop for the primary resource.
113+
```Go
114+
// Watch for changes to the primary resource Memcached
115+
err = c.Watch(&source.Kind{Type: &cachev1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{})
116+
117+
// Watch for changes to the secondary resource AppService and enqueue reconcile requests for the owner Memcached
118+
err = c.Watch(&source.Kind{Type: &appv1alpha1.AppService{}}, &handler.EnqueueRequestForOwner{
119+
IsController: true,
120+
OwnerType: &cachev1alpha1.Memcached{},
121+
})
122+
```
123+
- Add a new controller to watch and reconcile the CR independently of the other CR.
124+
```sh
125+
$ operator-sdk add controller --api-version=app.example.com/v1alpha1 --kind=AppService
126+
```
127+
```Go
128+
// Watch for changes to the primary resource AppService
129+
err = c.Watch(&source.Kind{Type: &appv1alpha1.AppService{}}, &handler.EnqueueRequestForObject{})
130+
```
131+
132+
### Copy and modify reconcile code from pkg/stub/handler.go
133+
134+
In a `v0.1.0` project the reconcile code is defined in the `Reconcile()` method of a controller's [Reconciler][reconciler-go-doc]. This is similar to the `Handle()` function in the older project. Note the difference in the arguments and return values:
135+
- Reconcile
136+
```Go
137+
func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error)
138+
```
139+
- Handle
140+
```Go
141+
func (h *Handler) Handle(ctx context.Context, event sdk.Event) error
142+
```
143+
144+
Instead of receiving an `sdk.Event` (with the object), the `Reconcile()` function receives a [Request][request-go-doc] (Name/Namespace key) to lookup the object.
145+
146+
If the `Reconcile()` function returns an error, the controller will requeue and retry the `Request`. If no error is returned, then depending on the [Result][result-go-doc] the controller will either not retry the `Request`, immediately retry, or retry after a specified duration.
147+
148+
Copy the code from the old project's `Handle()` function over the existing code in your controller's `Reconcile()` function.
149+
Be sure to keep the initial section in the `Reconcile()` code that looks up the object for the `Request` and checks to see if it's deleted.
150+
151+
```Go
152+
import (
153+
apierrors "k8s.io/apimachinery/pkg/api/errors"
154+
cachev1alpha1 "github.com/example-inc/memcached-operator/pkg/apis/cache/v1alpha1"
155+
...
156+
)
157+
func (r *ReconcileMemcached) Reconcile(request reconcile.Request) (reconcile.Result, error) {
158+
// Fetch the Memcached instance
159+
instance := &cachev1alpha1.Memcached{}
160+
err := r.client.Get(context.TODO()
161+
request.NamespacedName, instance)
162+
if err != nil {
163+
if apierrors.IsNotFound(err) {
164+
// Request object not found, could have been deleted after reconcile request.
165+
// Owned objects are automatically garbage collected.
166+
// Return and don't requeue
167+
return reconcile.Result{}, nil
168+
}
169+
// Error reading the object - requeue the request.
170+
return reconcile.Result{}, err
171+
}
172+
173+
// Rest of your reconcile code goes here.
174+
...
175+
}
176+
```
177+
#### Update return values
178+
179+
Change the return values in your reconcile code:
180+
- Replace `return err` with `return reconcile.Result{}, err`
181+
- Replace `return nil` with `return reconcile.Result{}, nil`
182+
183+
#### Periodic reconcile
184+
In order to periodically reconcile a CR in your controller you can set the [RequeueAfter][result-go-doc] field for reconcile.Result.
185+
This will cause the controller to requeue the `Request` and trigger the reconcile after the desired duration. Note that the default value of 0 means no requeue.
186+
187+
```Go
188+
reconcilePeriod := 30 * time.Second
189+
reconcileResult := reconcile.Result{RequeueAfter: reconcilePeriod}
190+
...
191+
192+
// Update the status
193+
err := r.client.Update(context.TODO(), memcached)
194+
if err != nil {
195+
log.Printf("failed to update memcached status: %v", err)
196+
return reconcileResult, err
197+
}
198+
return reconcileResult, nil
199+
200+
```
201+
202+
#### Update client
203+
204+
Replace the calls to the SDK client(Create, Update, Delete, Get, List) with the reconciler's client.
205+
206+
See the examples below and the controller-runtime [client API doc][client-api-doc] for more details.
207+
208+
```Go
209+
// Create
210+
dep := &appsv1.Deployment{...}
211+
err := sdk.Create(dep)
212+
// v0.0.1
213+
err := r.client.Create(context.TODO(), dep)
214+
215+
// Update
216+
err := sdk.Update(dep)
217+
// v0.0.1
218+
err := r.client.Update(context.TODO(), dep)
219+
220+
// Delete
221+
err := sdk.Delete(dep)
222+
// v0.0.1
223+
err := r.client.Delete(context.TODO(), dep)
224+
225+
// List
226+
podList := &corev1.PodList{}
227+
labelSelector := labels.SelectorFromSet(labelsForMemcached(memcached.Name))
228+
listOps := &metav1.ListOptions{LabelSelector: labelSelector}
229+
err := sdk.List(memcached.Namespace, podList, sdk.WithListOptions(listOps))
230+
// v0.1.0
231+
listOps := &client.ListOptions{Namespace: memcached.Namespace, LabelSelector: labelSelector}
232+
err := r.client.List(context.TODO(), listOps, podList)
233+
234+
// Get
235+
dep := &appsv1.Deployment{APIVersion: "apps/v1", Kind: "Deployment", Name: name, Namespace: namespace}
236+
err := sdk.Get(dep)
237+
// v0.1.0
238+
dep := &appsv1.Deployment{}
239+
err = r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, dep)
240+
```
241+
242+
Lastly copy and initialize any other fields that you may have had in your `Handler` struct into the `Reconcile<Kind>` struct:
243+
244+
```Go
245+
// newReconciler returns a new reconcile.Reconciler
246+
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
247+
return &ReconcileMemcached{client: mgr.GetClient(), scheme: mgr.GetScheme(), foo: "bar"}
248+
}
249+
250+
// ReconcileMemcached reconciles a Memcached object
251+
type ReconcileMemcached struct {
252+
client client.Client
253+
scheme *runtime.Scheme
254+
// Other fields
255+
foo string
256+
}
257+
```
258+
259+
### Copy changes from main.go
260+
261+
The main function for a `v0.1.0` operator in `cmd/manager/main.go` sets up the [Manager][manager-go-doc] which registers the custom resources and starts all the controllers.
262+
263+
There is no need to migrate the SDK functions `sdk.Watch()`,`sdk.Handle()`, and `sdk.Run()` from the old `main.go` since that logic is now defined in a controller.
264+
265+
However if there are any operator specific flags or settings defined in the old main file copy those over.
266+
267+
If you have any 3rd party resource types registered with the SDK's scheme, then register those with the Manager's scheme in the new project. See how to [register 3rd party resources][register-3rd-party-resources].
268+
269+
### Copy user defined files
270+
271+
If there are any user defined pkgs, scripts, and docs in the older project, copy these files into the new project.
272+
273+
### Copy changes to deployment manifests
274+
275+
For any updates made to the following manifests in the old project, copy over the changes to their corresponding files in the new project. Be careful not to directly overwrite the files but inspect and make any changes necessary.
276+
- `tmp/build/Dockerfile` to `build/Dockerfile`
277+
- There is no tmp directory in the new project layout
278+
- RBAC rules updates from `deploy/rbac.yaml` to `deploy/role.yaml` and `deploy/role_binding.yaml`
279+
- `deploy/cr.yaml` to `deploy/crds/<group>_<version>_<kind>_cr.yaml`
280+
- `deploy/crd.yaml` to `deploy/crds/<group>_<version>_<kind>_crd.yaml`
281+
282+
### Copy user defined dependencies
283+
284+
For any user defined dependencies added to the old project's Gopkg.toml, copy and append them to the new project's Gopkg.toml.
285+
Run `dep ensure` to update the vendor in the new project.
286+
287+
### Confirmation
288+
289+
At this point you should be able to build and run your operator to verify that it works. See the [user-guide][user-guide-build-run] on how to build and run your operator.
290+
291+
[v0.1.0-changes-doc]: ./v0.1.0-changes.md
292+
[v0.0.7-memcached-operator]: https://github.com/operator-framework/operator-sdk-samples/tree/aa15bd278eec0959595e0a0a7282a26055d7f9d6/memcached-operator
293+
[v0.1.0-memcached-operator]: https://github.com/operator-framework/operator-sdk-samples/tree/4c6934448684a6953ece4d3d9f3f77494b1c125e/memcached-operator
294+
[controller-conventions]: https://github.com/kubernetes/community/blob/master/contributors/devel/controllers.md#guidelines
295+
[reconciler-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Reconciler
296+
[watching-eventhandling-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg#hdr-Watching_and_EventHandling
297+
[controller-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg#hdr-Controller
298+
[request-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Request
299+
[result-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/reconcile#Result
300+
[client-api-doc]: ./../user/client.md
301+
[manager-go-doc]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/manager
302+
[register-3rd-party-resources]: ./../user-guide.md#adding-3rd-party-resources-to-your-operator
303+
[user-guide-build-run]: ./../user-guide.md#build-and-run-the-operator

doc/user-guide.md

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ $ cd memcached-operator
4848
To learn about the project directory structure, see [project layout][layout_doc] doc.
4949

5050
### Manager
51-
The main program for the operator is the manager `cmd/manager/main.go`.
51+
The main program for the operator is the Manager `cmd/manager/main.go`.
5252

5353
The manager will automatically register the scheme for all custom resources defined under `pkg/apis/...` and run all controllers under `pkg/controller/...`.
5454

@@ -326,25 +326,23 @@ $ kubectl delete -f deploy/service_account.yaml
326326

327327
## Advanced Topics
328328
### Adding 3rd Party Resources To Your Operator
329-
To add a resource to an operator, you must add it to a scheme. By creating an `AddToScheme` method or reusing one you can easily add a resource to your scheme. An [example][deployments_register] shows that you define a function and then use the [runtime][runtime_package] package to create a `SchemeBuilder`
330-
331-
#### Current Operator-SDK
332-
You then need to tell the operators to use these functions to add the resources to its scheme. In operator-sdk you use [AddToSDKScheme][osdk_add_to_scheme] to add this.
333-
Example of you main.go:
334-
```go
329+
By default the operator's Manager will register all custom resource types defined in your project under `pkg/apis` with its scheme.
330+
```Go
335331
import (
336-
....
337-
appsv1 "k8s.io/api/apps/v1"
332+
"github.com/example-inc/memcached-operator/pkg/apis"
333+
...
338334
)
339-
340-
func main() {
341-
k8sutil.AddToSDKScheme(appsv1.AddToScheme)`
342-
sdk.Watch(appsv1.SchemeGroupVersion.String(), "Deployments", <namespace>, <resyncPeriod>)
335+
// Setup Scheme for all resources
336+
if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
337+
log.Fatal(err)
343338
}
344339
```
345340

346-
#### Future with Controller Runtime
347-
When using controller runtime, you will also need to tell its scheme about your resourece. In controller runtime to add to the scheme, you can get the managers [scheme][manager_scheme]. If you would like to see what kubebuilder generates to add the resoureces to the [scheme][simple_resource].
341+
To add a 3rd party resource to an operator, you must add it to the Manager's scheme. By creating an `AddToScheme` method or reusing one you can easily add a resource to your scheme. An [example][deployments_register] shows that you define a function and then use the [runtime][runtime_package] package to create a `SchemeBuilder`.
342+
343+
#### Register with the manager's scheme
344+
Call the `AddToScheme()` function for your 3rd party resource and pass it the Manager's scheme via `mgr.GetScheme()`.
345+
348346
Example:
349347
```go
350348
import (
@@ -371,8 +369,6 @@ func main() {
371369
[docker_tool]:https://docs.docker.com/install/
372370
[kubectl_tool]:https://kubernetes.io/docs/tasks/tools/install-kubectl/
373371
[minikube_tool]:https://github.com/kubernetes/minikube#installation
374-
[manager_scheme]: https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/manager/manager.go#L61
375-
[simple_resource]: https://book.kubebuilder.io/basics/simple_resource.html
376372
[deployments_register]: https://github.com/kubernetes/api/blob/master/apps/v1/register.go#L41
377373
[doc_client_api]:./user/client.md
378374
[runtime_package]: https://godoc.org/k8s.io/apimachinery/pkg/runtime

0 commit comments

Comments
 (0)