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  `