github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/controller_kind.go (about) 1 // Copyright 2018 The Operator-SDK Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package scaffold 16 17 import ( 18 "fmt" 19 "path" 20 "path/filepath" 21 "strings" 22 "unicode" 23 24 "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" 25 ) 26 27 // ControllerKind is the input needed to generate a pkg/controller/<kind>/<kind>_controller.go file 28 type ControllerKind struct { 29 input.Input 30 31 // Resource defines the inputs for the controller's primary resource 32 Resource *Resource 33 // CustomImport holds the import path for a built-in or custom Kubernetes 34 // API that this controller reconciles, if specified by the scaffold invoker. 35 CustomImport string 36 37 // The following fields will be overwritten by GetInput(). 38 // 39 // ImportMap maps all imports destined for the scaffold to their import 40 // identifier, if any. 41 ImportMap map[string]string 42 // GoImportIdent is the import identifier for the API reconciled by this 43 // controller. 44 GoImportIdent string 45 } 46 47 func (s *ControllerKind) GetInput() (input.Input, error) { 48 if s.Path == "" { 49 fileName := s.Resource.LowerKind + "_controller.go" 50 s.Path = filepath.Join(ControllerDir, s.Resource.LowerKind, fileName) 51 } 52 // Error if this file exists. 53 s.IfExistsAction = input.Error 54 s.TemplateBody = controllerKindTemplate 55 56 // Set imports. 57 if err := s.setImports(); err != nil { 58 return input.Input{}, err 59 } 60 return s.Input, nil 61 } 62 63 func (s *ControllerKind) setImports() (err error) { 64 s.ImportMap = controllerKindImports 65 importPath := "" 66 if s.CustomImport != "" { 67 importPath, s.GoImportIdent, err = getCustomAPIImportPathAndIdent(s.CustomImport) 68 if err != nil { 69 return err 70 } 71 } else { 72 importPath = path.Join(s.Repo, "pkg", "apis", s.Resource.GoImportGroup, s.Resource.Version) 73 s.GoImportIdent = s.Resource.GoImportGroup + s.Resource.Version 74 } 75 // Import identifiers must be unique within a file. 76 for p, id := range s.ImportMap { 77 if s.GoImportIdent == id && importPath != p { 78 // Append "api" to the conflicting import identifier. 79 s.GoImportIdent = s.GoImportIdent + "api" 80 break 81 } 82 } 83 s.ImportMap[importPath] = s.GoImportIdent 84 return nil 85 } 86 87 func getCustomAPIImportPathAndIdent(m string) (p string, id string, err error) { 88 sm := strings.Split(m, "=") 89 for i, e := range sm { 90 if i == 0 { 91 p = strings.TrimSpace(e) 92 } else if i == 1 { 93 id = strings.TrimSpace(e) 94 } 95 } 96 if p == "" { 97 return "", "", fmt.Errorf(`custom import "%s" path is empty`, m) 98 } 99 if id == "" { 100 if len(sm) == 2 { 101 return "", "", fmt.Errorf(`custom import "%s" identifier is empty, remove "=" from passed string`, m) 102 } 103 sp := strings.Split(p, "/") 104 if len(sp) > 1 { 105 id = sp[len(sp)-2] + sp[len(sp)-1] 106 } else { 107 id = sp[0] 108 } 109 id = strings.ToLower(id) 110 } 111 idb := &strings.Builder{} 112 // By definition, all package identifiers must be comprised of "_", unicode 113 // digits, and/or letters. 114 for _, r := range id { 115 if unicode.IsDigit(r) || unicode.IsLetter(r) || r == '_' { 116 if _, err := idb.WriteRune(r); err != nil { 117 return "", "", err 118 } 119 } 120 } 121 return p, idb.String(), nil 122 } 123 124 var controllerKindImports = map[string]string{ 125 "k8s.io/api/core/v1": "corev1", 126 "k8s.io/apimachinery/pkg/api/errors": "", 127 "k8s.io/apimachinery/pkg/apis/meta/v1": "metav1", 128 "k8s.io/apimachinery/pkg/runtime": "", 129 "k8s.io/apimachinery/pkg/types": "", 130 "sigs.k8s.io/controller-runtime/pkg/client": "", 131 "sigs.k8s.io/controller-runtime/pkg/controller": "", 132 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil": "", 133 "sigs.k8s.io/controller-runtime/pkg/handler": "", 134 "sigs.k8s.io/controller-runtime/pkg/manager": "", 135 "sigs.k8s.io/controller-runtime/pkg/reconcile": "", 136 "sigs.k8s.io/controller-runtime/pkg/runtime/log": "logf", 137 "sigs.k8s.io/controller-runtime/pkg/source": "", 138 } 139 140 const controllerKindTemplate = `package {{ .Resource.LowerKind }} 141 142 import ( 143 "context" 144 145 {{range $p, $i := .ImportMap -}} 146 {{$i}} "{{$p}}" 147 {{end}} 148 ) 149 150 var log = logf.Log.WithName("controller_{{ .Resource.LowerKind }}") 151 152 /** 153 * USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller 154 * business logic. Delete these comments after modifying this file.* 155 */ 156 157 // Add creates a new {{ .Resource.Kind }} Controller and adds it to the Manager. The Manager will set fields on the Controller 158 // and Start it when the Manager is Started. 159 func Add(mgr manager.Manager) error { 160 return add(mgr, newReconciler(mgr)) 161 } 162 163 // newReconciler returns a new reconcile.Reconciler 164 func newReconciler(mgr manager.Manager) reconcile.Reconciler { 165 return &Reconcile{{ .Resource.Kind }}{client: mgr.GetClient(), scheme: mgr.GetScheme()} 166 } 167 168 // add adds a new Controller to mgr with r as the reconcile.Reconciler 169 func add(mgr manager.Manager, r reconcile.Reconciler) error { 170 // Create a new controller 171 c, err := controller.New("{{ .Resource.LowerKind }}-controller", mgr, controller.Options{Reconciler: r}) 172 if err != nil { 173 return err 174 } 175 176 // Watch for changes to primary resource {{ .Resource.Kind }} 177 err = c.Watch(&source.Kind{Type: &{{ .GoImportIdent }}.{{ .Resource.Kind }}{}}, &handler.EnqueueRequestForObject{}) 178 if err != nil { 179 return err 180 } 181 182 // TODO(user): Modify this to be the types you create that are owned by the primary resource 183 // Watch for changes to secondary resource Pods and requeue the owner {{ .Resource.Kind }} 184 err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ 185 IsController: true, 186 OwnerType: &{{ .GoImportIdent }}.{{ .Resource.Kind }}{}, 187 }) 188 if err != nil { 189 return err 190 } 191 192 return nil 193 } 194 195 // blank assignment to verify that Reconcile{{ .Resource.Kind }} implements reconcile.Reconciler 196 var _ reconcile.Reconciler = &Reconcile{{ .Resource.Kind }}{} 197 198 // Reconcile{{ .Resource.Kind }} reconciles a {{ .Resource.Kind }} object 199 type Reconcile{{ .Resource.Kind }} struct { 200 // This client, initialized using mgr.Client() above, is a split client 201 // that reads objects from the cache and writes to the apiserver 202 client client.Client 203 scheme *runtime.Scheme 204 } 205 206 // Reconcile reads that state of the cluster for a {{ .Resource.Kind }} object and makes changes based on the state read 207 // and what is in the {{ .Resource.Kind }}.Spec 208 // TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates 209 // a Pod as an example 210 // Note: 211 // The Controller will requeue the Request to be processed again if the returned error is non-nil or 212 // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 213 func (r *Reconcile{{ .Resource.Kind }}) Reconcile(request reconcile.Request) (reconcile.Result, error) { 214 reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 215 reqLogger.Info("Reconciling {{ .Resource.Kind }}") 216 217 // Fetch the {{ .Resource.Kind }} instance 218 instance := &{{ .GoImportIdent }}.{{ .Resource.Kind }}{} 219 err := r.client.Get(context.TODO(), request.NamespacedName, instance) 220 if err != nil { 221 if errors.IsNotFound(err) { 222 // Request object not found, could have been deleted after reconcile request. 223 // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 224 // Return and don't requeue 225 return reconcile.Result{}, nil 226 } 227 // Error reading the object - requeue the request. 228 return reconcile.Result{}, err 229 } 230 231 // Define a new Pod object 232 pod := newPodForCR(instance) 233 234 // Set {{ .Resource.Kind }} instance as the owner and controller 235 if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil { 236 return reconcile.Result{}, err 237 } 238 239 // Check if this Pod already exists 240 found := &corev1.Pod{} 241 err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) 242 if err != nil && errors.IsNotFound(err) { 243 reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) 244 err = r.client.Create(context.TODO(), pod) 245 if err != nil { 246 return reconcile.Result{}, err 247 } 248 249 // Pod created successfully - don't requeue 250 return reconcile.Result{}, nil 251 } else if err != nil { 252 return reconcile.Result{}, err 253 } 254 255 // Pod already exists - don't requeue 256 reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name) 257 return reconcile.Result{}, nil 258 } 259 260 // newPodForCR returns a busybox pod with the same name/namespace as the cr 261 func newPodForCR(cr *{{ .GoImportIdent }}.{{ .Resource.Kind }}) *corev1.Pod { 262 labels := map[string]string{ 263 "app": cr.Name, 264 } 265 return &corev1.Pod{ 266 ObjectMeta: metav1.ObjectMeta{ 267 Name: cr.Name + "-pod", 268 Namespace: cr.Namespace, 269 Labels: labels, 270 }, 271 Spec: corev1.PodSpec{ 272 Containers: []corev1.Container{ 273 { 274 Name: "busybox", 275 Image: "busybox", 276 Command: []string{"sleep", "3600"}, 277 }, 278 }, 279 }, 280 } 281 } 282 `