diff --git a/samples/full/controller/src/samplecontroller/pkg/apis/samplecontroller/v1alpha1/foo_types.go b/samples/full/controller/src/samplecontroller/pkg/apis/samplecontroller/v1alpha1/foo_types.go index 4dc60f11934..65fd13efcdc 100644 --- a/samples/full/controller/src/samplecontroller/pkg/apis/samplecontroller/v1alpha1/foo_types.go +++ b/samples/full/controller/src/samplecontroller/pkg/apis/samplecontroller/v1alpha1/foo_types.go @@ -1,4 +1,3 @@ - /* Copyright 2017 The Kubernetes Authors. @@ -15,32 +14,27 @@ 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! -// Created by "kubebuilder create resource" for you to implement the Foo resource schema definition -// as a go struct. -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // FooSpec defines the desired state of Foo type FooSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + DeploymentName string `json:"deploymentName"` + Replicas *int32 `json:"replicas"` } // FooStatus defines the observed state of Foo type FooStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + AvailableReplicas int32 `json:"availableReplicas"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// Foo +// Foo is a specification for a Foo resource // +k8s:openapi-gen=true // +resource:path=foos type Foo struct { diff --git a/samples/full/controller/src/samplecontroller/pkg/controller/foo/controller.go b/samples/full/controller/src/samplecontroller/pkg/controller/foo/controller.go index 3b2100e058c..fb0f38c6305 100644 --- a/samples/full/controller/src/samplecontroller/pkg/controller/foo/controller.go +++ b/samples/full/controller/src/samplecontroller/pkg/controller/foo/controller.go @@ -14,59 +14,226 @@ See the License for the specific language governing permissions and limitations under the License. */ - package foo import ( - "log" + "fmt" + "github.com/golang/glog" "github.com/kubernetes-sigs/kubebuilder/pkg/controller" + "github.com/kubernetes-sigs/kubebuilder/pkg/controller/eventhandlers" + "github.com/kubernetes-sigs/kubebuilder/pkg/controller/predicates" "github.com/kubernetes-sigs/kubebuilder/pkg/controller/types" + 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/runtime/schema" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" - samplecontrollerv1alpha1client "samplecontroller/pkg/client/clientset/versioned/typed/samplecontroller/v1alpha1" - samplecontrollerv1alpha1lister "samplecontroller/pkg/client/listers/samplecontroller/v1alpha1" samplecontrollerv1alpha1 "samplecontroller/pkg/apis/samplecontroller/v1alpha1" - samplecontrollerv1alpha1informer "samplecontroller/pkg/client/informers/externalversions/samplecontroller/v1alpha1" + samplescheme "samplecontroller/pkg/client/clientset/versioned/scheme" "samplecontroller/pkg/inject/args" ) -// EDIT THIS FILE -// This files was created by "kubebuilder create resource" for you to edit. -// Controller implementation logic for Foo resources goes here. +const controllerAgentName = "sample-controller" + +const ( + // SuccessSynced is used as part of the Event 'reason' when a Foo is synced + SuccessSynced = "Synced" + // ErrResourceExists is used as part of the Event 'reason' when a Foo fails + // to sync due to a Deployment of the same name already existing. + ErrResourceExists = "ErrResourceExists" + // MessageResourceExists is the message used for Events when a resource + // fails to sync due to a Deployment already existing + MessageResourceExists = "Resource %q already exists and is not managed by Foo" + // MessageResourceSynced is the message used for an Event fired when a Foo + // is synced successfully + MessageResourceSynced = "Foo synced successfully" +) + +// Reconcile compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the Foo resource +// with the current status of the resource. func (bc *FooController) Reconcile(k types.ReconcileKey) error { - // INSERT YOUR CODE HERE - log.Printf("Implement the Reconcile function on foo.FooController to reconcile %s\n", k.Name) + namespace, name := k.Namespace, k.Name + foo, err := bc.Informers.Samplecontroller().V1alpha1().Foos().Lister().Foos(namespace).Get(name) + if err != nil { + // The Foo resource may no longer exist, in which case we stop + // processing. + if errors.IsNotFound(err) { + runtime.HandleError(fmt.Errorf("foo '%s' in work queue no longer exists", k)) + return nil + } + + return err + } + + deploymentName := foo.Spec.DeploymentName + if deploymentName == "" { + // We choose to absorb the error here as the worker would requeue the + // resource otherwise. Instead, the next time the resource is updated + // the resource will be queued again. + runtime.HandleError(fmt.Errorf("%s: deployment name must be specified", k)) + return nil + } + + // Get the deployment with the name specified in Foo.spec + deployment, err := bc.KubernetesInformers.Apps().V1().Deployments().Lister().Deployments(foo.Namespace).Get(deploymentName) + // If the resource doesn't exist, we'll create it + if errors.IsNotFound(err) { + deployment, err = bc.KubernetesClientSet.AppsV1().Deployments(foo.Namespace).Create(newDeployment(foo)) + } + + // If an error occurs during Get/Create, we'll requeue the item so we can + // attempt processing again later. This could have been caused by a + // temporary network failure, or any other transient reason. + if err != nil { + return err + } + + // If the Deployment is not controlled by this Foo resource, we should log + // a warning to the event recorder and ret + if !metav1.IsControlledBy(deployment, foo) { + msg := fmt.Sprintf(MessageResourceExists, deployment.Name) + bc.recorder.Event(foo, corev1.EventTypeWarning, ErrResourceExists, msg) + return fmt.Errorf(msg) + } + + // If this number of the replicas on the Foo resource is specified, and the + // number does not equal the current desired replicas on the Deployment, we + // should update the Deployment resource. + if foo.Spec.Replicas != nil && *foo.Spec.Replicas != *deployment.Spec.Replicas { + glog.V(4).Infof("Foo %s replicas: %d, deployment replicas: %d", name, *foo.Spec.Replicas, *deployment.Spec.Replicas) + deployment, err = bc.KubernetesClientSet.AppsV1().Deployments(foo.Namespace).Update(newDeployment(foo)) + } + + // If an error occurs during Update, we'll requeue the item so we can + // attempt processing again later. THis could have been caused by a + // temporary network failure, or any other transient reason. + if err != nil { + return err + } + + // Finally, we update the status block of the Foo resource to reflect the + // current state of the world + err = bc.updateFooStatus(foo, deployment) + if err != nil { + return err + } + + bc.recorder.Event(foo, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced) return nil } +// FooController is the controller implementation for Foo resources // +controller:group=samplecontroller,version=v1alpha1,kind=Foo,resource=foos +// +informers:group=apps,version=v1,kind=Deployment +// +rbac:rbac:groups=apps,resources=Deployment,verbs=get;list;watch;create;update;patch;delete type FooController struct { - // INSERT ADDITIONAL FIELDS HERE - fooLister samplecontrollerv1alpha1lister.FooLister - fooclient samplecontrollerv1alpha1client.SamplecontrollerV1alpha1Interface + args.InjectArgs + + // recorder is an event recorder for recording Event resources to the + // Kubernetes API. + recorder record.EventRecorder } // ProvideController provides a controller that will be run at startup. Kubebuilder will use codegeneration // to automatically register this controller in the inject package -func ProvideController(arguments args.InjectArgs) (*controller.GenericController, error) { - // INSERT INITIALIZATIONS FOR ADDITIONAL FIELDS HERE +func ProvideController(iargs args.InjectArgs) (*controller.GenericController, error) { + samplescheme.AddToScheme(scheme.Scheme) + bc := &FooController{ - fooLister: arguments.ControllerManager.GetInformerProvider(&samplecontrollerv1alpha1.Foo{}).(samplecontrollerv1alpha1informer.FooInformer).Lister(), - fooclient: arguments.Clientset.SamplecontrollerV1alpha1(), + InjectArgs: iargs, + recorder: iargs.CreateRecorder(controllerAgentName), } // Create a new controller that will call FooController.Reconcile on changes to Foos gc := &controller.GenericController{ - Name: "FooController", - Reconcile: bc.Reconcile, - InformerRegistry: arguments.ControllerManager, + Name: controllerAgentName, + Reconcile: bc.Reconcile, + InformerRegistry: iargs.ControllerManager, } + + glog.Info("Setting up event handlers") if err := gc.Watch(&samplecontrollerv1alpha1.Foo{}); err != nil { return gc, err } - // INSERT ADDITIONAL WATCHES HERE BY CALLING gc.Watch.*() FUNCTIONS - // NOTE: Informers for Kubernetes resources *MUST* be registered in the pkg/inject package so that they are started. + // Set up an event handler for when Deployment resources change. This + // handler will lookup the owner of the given Deployment, and if it is + // owned by a Foo resource will enqueue that Foo resource for + // processing. This way, we don't need to implement custom logic for + // handling Deployment resources. More info on this pattern: + // https://github.com/kubernetes/community/blob/8cafef897a22026d42f5e5bb3f104febe7e29830/contributors/devel/controllers.md + if err := gc.WatchControllerOf(&appsv1.Deployment{}, eventhandlers.Path{bc.LookupFoo}, + predicates.ResourceVersionChanged); err != nil { + return gc, err + } + return gc, nil } + +// LookupFoo looksup a Foo from the lister +func (bc FooController) LookupFoo(r types.ReconcileKey) (interface{}, error) { + return bc.Informers.Samplecontroller().V1alpha1().Foos().Lister().Foos(r.Namespace).Get(r.Name) +} + +func (bc *FooController) updateFooStatus(foo *samplecontrollerv1alpha1.Foo, deployment *appsv1.Deployment) error { + // NEVER modify objects from the store. It's a read-only, local cache. + // You can use DeepCopy() to make a deep copy of original object and modify this copy + // Or create a copy manually for better performance + fooCopy := foo.DeepCopy() + fooCopy.Status.AvailableReplicas = deployment.Status.AvailableReplicas + // Until #38113 is merged, we must use Update instead of UpdateStatus to + // update the Status block of the Foo resource. UpdateStatus will not + // allow changes to the Spec of the resource, which is ideal for ensuring + // nothing other than resource status has been updated. + _, err := bc.Clientset.SamplecontrollerV1alpha1().Foos(foo.Namespace).Update(fooCopy) + return err +} + +// newDeployment creates a new Deployment for a Foo resource. It also sets +// the appropriate OwnerReferences on the resource so handleObject can discover +// the Foo resource that 'owns' it. +func newDeployment(foo *samplecontrollerv1alpha1.Foo) *appsv1.Deployment { + labels := map[string]string{ + "app": "nginx", + "controller": foo.Name, + } + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: foo.Spec.DeploymentName, + Namespace: foo.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(foo, schema.GroupVersionKind{ + Group: samplecontrollerv1alpha1.SchemeGroupVersion.Group, + Version: samplecontrollerv1alpha1.SchemeGroupVersion.Version, + Kind: "Foo", + }), + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: foo.Spec.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx:latest", + }, + }, + }, + }, + }, + } +}