From a6535156c89ccf80e6c8dcc20866f45f65ba0cb4 Mon Sep 17 00:00:00 2001 From: zhaojizhuang <571130360@qq.com> Date: Tue, 20 Apr 2021 00:00:01 +0800 Subject: [PATCH] change status to duckv1.status, add reconciler framework (#454) * add reconciler for imagecache * change status to duckv1.status * fix UT * fix static check * fix isReady * add UT case for generation --- go.mod | 1 + pkg/apis/caching/v1alpha1/image_lifecycle.go | 60 +++ .../caching/v1alpha1/image_lifecycle_test.go | 163 +++++++ pkg/apis/caching/v1alpha1/image_types.go | 104 +--- pkg/apis/caching/v1alpha1/image_types_test.go | 169 ------- .../caching/v1alpha1/zz_generated.deepcopy.go | 25 +- .../caching/v1alpha1/image/controller.go | 153 ++++++ .../caching/v1alpha1/image/reconciler.go | 449 ++++++++++++++++++ .../caching/v1alpha1/image/state.go | 106 +++++ .../client/injection/kube/client/client.go | 57 +++ vendor/modules.txt | 2 + 11 files changed, 996 insertions(+), 293 deletions(-) create mode 100644 pkg/apis/caching/v1alpha1/image_lifecycle.go create mode 100644 pkg/apis/caching/v1alpha1/image_lifecycle_test.go delete mode 100644 pkg/apis/caching/v1alpha1/image_types_test.go create mode 100644 pkg/client/injection/reconciler/caching/v1alpha1/image/controller.go create mode 100644 pkg/client/injection/reconciler/caching/v1alpha1/image/reconciler.go create mode 100644 pkg/client/injection/reconciler/caching/v1alpha1/image/state.go create mode 100644 vendor/knative.dev/pkg/client/injection/kube/client/client.go diff --git a/go.mod b/go.mod index ff8e1297..31cfa611 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/tsenart/go-tsz v0.0.0-20180814235614-0bd30b3df1c3 // indirect github.com/tsenart/vegeta v12.7.1-0.20190725001342-b5f4fca92137+incompatible go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.16.0 k8s.io/api v0.19.7 k8s.io/apimachinery v0.19.7 k8s.io/client-go v0.19.7 diff --git a/pkg/apis/caching/v1alpha1/image_lifecycle.go b/pkg/apis/caching/v1alpha1/image_lifecycle.go new file mode 100644 index 00000000..11a2a7cd --- /dev/null +++ b/pkg/apis/caching/v1alpha1/image_lifecycle.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + duckv1 "knative.dev/pkg/apis/duck/v1" + + "knative.dev/pkg/apis" +) + +const ( + // ImageConditionReady is set when the revision is starting to materialize + // runtime resources, and becomes true when those resources are ready. + ImageConditionReady = apis.ConditionReady +) + +var condSet = apis.NewLivingConditionSet() + +// GetGroupVersionKind implements kmeta.OwnerRefable +func (i *Image) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind("Image") +} + +// GetConditionSet retrieves the condition set for this resource. Implements the KRShaped interface. +func (i *Image) GetConditionSet() apis.ConditionSet { + return condSet +} + +// InitializeConditions sets the initial values to the conditions. +func (is *ImageStatus) InitializeConditions() { + condSet.Manage(is).InitializeConditions() +} + +// IsReady looks at the conditions and if the Status has a condition +// ImageConditionReady returns true if ConditionStatus is True +func (i *Image) IsReady() bool { + is := i.Status + return is.ObservedGeneration == i.Generation && + is.GetCondition(ImageConditionReady).IsTrue() +} + +// GetStatus retrieves the status of the Image. Implements the KRShaped interface. +func (t *Image) GetStatus() *duckv1.Status { + return &t.Status.Status +} diff --git a/pkg/apis/caching/v1alpha1/image_lifecycle_test.go b/pkg/apis/caching/v1alpha1/image_lifecycle_test.go new file mode 100644 index 00000000..8edc1a7d --- /dev/null +++ b/pkg/apis/caching/v1alpha1/image_lifecycle_test.go @@ -0,0 +1,163 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +func TestIsReady(t *testing.T) { + cases := []struct { + name string + generation int64 + status ImageStatus + isReady bool + }{{ + name: "empty status should not be ready", + status: ImageStatus{}, + isReady: false, + }, { + name: "Different condition type should not be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: "foo", + Status: corev1.ConditionTrue, + }, + }}, + }, + isReady: false, + }, { + name: "False condition status should not be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: ImageConditionReady, + Status: corev1.ConditionFalse, + }}, + }, + }, + isReady: false, + }, { + name: "Unknown condition status should not be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: ImageConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, + isReady: false, + }, { + name: "Missing condition status should not be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: ImageConditionReady, + }}, + }, + }, + isReady: false, + }, { + name: "True condition status should be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: ImageConditionReady, + Status: corev1.ConditionTrue, + }}, + }, + }, + isReady: true, + }, { + name: "Multiple conditions with ready status should be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: "foo", + Status: corev1.ConditionTrue, + }, { + Type: ImageConditionReady, + Status: corev1.ConditionTrue, + }}, + }, + }, + isReady: true, + }, { + name: "Multiple conditions with ready status false should not be ready", + generation: 0, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 0, + Conditions: duckv1.Conditions{{ + Type: "foo", + Status: corev1.ConditionTrue, + }, { + Type: ImageConditionReady, + Status: corev1.ConditionFalse, + }}, + }, + }, + isReady: false, + }, { + name: "Generation not equal ObservedGeneration should not be ready", + generation: 1, + status: ImageStatus{ + Status: duckv1.Status{ + ObservedGeneration: 2, + Conditions: duckv1.Conditions{{ + Type: "foo", + Status: corev1.ConditionTrue, + }, { + Type: ImageConditionReady, + Status: corev1.ConditionFalse, + }}, + }, + }, + isReady: false, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + m := Image{ + ObjectMeta: metav1.ObjectMeta{ + Generation: tc.generation, + }, + Status: tc.status} + if e, a := tc.isReady, m.IsReady(); e != a { + t.Errorf("Ready = %v, want: %v", a, e) + } + }) + } +} diff --git a/pkg/apis/caching/v1alpha1/image_types.go b/pkg/apis/caching/v1alpha1/image_types.go index d9a37d4b..f113b463 100644 --- a/pkg/apis/caching/v1alpha1/image_types.go +++ b/pkg/apis/caching/v1alpha1/image_types.go @@ -17,19 +17,16 @@ limitations under the License. package v1alpha1 import ( - "reflect" - "sort" - "time" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/kmeta" ) // +genclient +// +genreconciler // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Image is a Knative abstraction that encapsulates the interface by which Knative @@ -52,6 +49,7 @@ type Image struct { var _ apis.Validatable = (*Image)(nil) var _ apis.Defaultable = (*Image)(nil) var _ kmeta.OwnerRefable = (*Image)(nil) +var _ duckv1.KRShaped = (*Image)(nil) // ImageSpec holds the desired state of the Image (from the client). type ImageSpec struct { @@ -72,41 +70,9 @@ type ImageSpec struct { ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` } -// ImageConditionType is used to communicate the status of the reconciliation process. -type ImageConditionType string - -const ( - // ImageConditionReady is set when the revision is starting to materialize - // runtime resources, and becomes true when those resources are ready. - ImageConditionReady ImageConditionType = "Ready" -) - -// ImageCondition defines a readiness condition for a Image. -// See: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#typical-status-properties -type ImageCondition struct { - Type ImageConditionType `json:"type" description:"type of Image condition"` - - Status corev1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` - - // +optional - // We use VolatileTime in place of metav1.Time to exclude this from creating equality.Semantic - // differences (all other things held constant). - LastTransitionTime apis.VolatileTime `json:"lastTransitionTime,omitempty" description:"last time the condition transit from one status to another"` - - // +optional - Reason string `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` - - // +optional - Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` -} - // ImageStatus communicates the observed state of the Image (from the controller). type ImageStatus struct { - // Conditions communicates information about ongoing/complete - // reconciliation processes that bring the "spec" inline with the observed - // state of the world. - // +optional - Conditions []ImageCondition `json:"conditions,omitempty"` + duckv1.Status `json:",inline"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -118,65 +84,3 @@ type ImageList struct { Items []Image `json:"items"` } - -// IsReady looks at the conditions and if the Status has a condition -// ImageConditionReady returns true if ConditionStatus is True -func (rs *ImageStatus) IsReady() bool { - if c := rs.GetCondition(ImageConditionReady); c != nil { - return c.Status == corev1.ConditionTrue - } - return false -} - -func (rs *ImageStatus) GetCondition(t ImageConditionType) *ImageCondition { - for _, cond := range rs.Conditions { - if cond.Type == t { - return &cond - } - } - return nil -} - -func (rs *ImageStatus) SetCondition(new *ImageCondition) { - if new == nil { - return - } - - t := new.Type - var conditions []ImageCondition - for _, cond := range rs.Conditions { - if cond.Type != t { - conditions = append(conditions, cond) - } else { - // If we'd only update the LastTransitionTime, then return. - new.LastTransitionTime = cond.LastTransitionTime - if reflect.DeepEqual(new, &cond) { - return - } - } - } - new.LastTransitionTime = apis.VolatileTime{ - Inner: metav1.NewTime(time.Now()), - } - conditions = append(conditions, *new) - // Deterministically order the conditions - sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type }) - rs.Conditions = conditions -} - -func (rs *ImageStatus) InitializeConditions() { - for _, cond := range []ImageConditionType{ - ImageConditionReady, - } { - if rc := rs.GetCondition(cond); rc == nil { - rs.SetCondition(&ImageCondition{ - Type: cond, - Status: corev1.ConditionUnknown, - }) - } - } -} - -func (i *Image) GetGroupVersionKind() schema.GroupVersionKind { - return SchemeGroupVersion.WithKind("Image") -} diff --git a/pkg/apis/caching/v1alpha1/image_types_test.go b/pkg/apis/caching/v1alpha1/image_types_test.go deleted file mode 100644 index 79839359..00000000 --- a/pkg/apis/caching/v1alpha1/image_types_test.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2018 The Knative Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package v1alpha1 - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "knative.dev/pkg/apis" -) - -func TestIsReady(t *testing.T) { - cases := []struct { - name string - status ImageStatus - isReady bool - }{{ - name: "empty status should not be ready", - status: ImageStatus{}, - isReady: false, - }, { - name: "Different condition type should not be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: "foo", - Status: corev1.ConditionTrue, - }}, - }, - isReady: false, - }, { - name: "False condition status should not be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: ImageConditionReady, - Status: corev1.ConditionFalse, - }}, - }, - isReady: false, - }, { - name: "Unknown condition status should not be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: ImageConditionReady, - Status: corev1.ConditionUnknown, - }}, - }, - isReady: false, - }, { - name: "Missing condition status should not be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: ImageConditionReady, - }}, - }, - isReady: false, - }, { - name: "True condition status should be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: ImageConditionReady, - Status: corev1.ConditionTrue, - }}, - }, - isReady: true, - }, { - name: "Multiple conditions with ready status should be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: "foo", - Status: corev1.ConditionTrue, - }, { - Type: ImageConditionReady, - Status: corev1.ConditionTrue, - }}, - }, - isReady: true, - }, { - name: "Multiple conditions with ready status false should not be ready", - status: ImageStatus{ - Conditions: []ImageCondition{{ - Type: "foo", - Status: corev1.ConditionTrue, - }, { - Type: ImageConditionReady, - Status: corev1.ConditionFalse, - }}, - }, - isReady: false, - }} - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if e, a := tc.isReady, tc.status.IsReady(); e != a { - t.Errorf("%q expected: %v got: %v", tc.name, e, a) - } - }) - } -} - -func TestImageConditions(t *testing.T) { - rev := &Image{} - foo := &ImageCondition{ - Type: "Foo", - Status: "True", - } - bar := &ImageCondition{ - Type: "Bar", - Status: "True", - } - - // Add a new condition. - rev.Status.SetCondition(foo) - - if got, want := len(rev.Status.Conditions), 1; got != want { - t.Fatalf("Unexpected Condition length; got %d, want %d", got, want) - } - - // Add nothing - rev.Status.SetCondition(nil) - - if got, want := len(rev.Status.Conditions), 1; got != want { - t.Fatalf("Unexpected Condition length; got %d, want %d", got, want) - } - - // Add a second condition. - rev.Status.SetCondition(bar) - - if got, want := len(rev.Status.Conditions), 2; got != want { - t.Fatalf("Unexpected Condition length; got %d, want %d", got, want) - } - - // Add nil condition. - rev.Status.SetCondition(nil) - - if got, want := len(rev.Status.Conditions), 2; got != want { - t.Fatalf("Unexpected Condition length; got %d, want %d", got, want) - } - - // Add a condition that varies only in LastTransitionTime, and check that - // things are not updated. - bar = rev.Status.GetCondition("Bar") - bar2 := bar.DeepCopy() - bar2.LastTransitionTime = apis.VolatileTime{ - Inner: metav1.NewTime(time.Unix(1234, 0)), - } - rev.Status.SetCondition(bar2) - - got, want := rev.Status.GetCondition("Bar"), bar - if diff := cmp.Diff(want, got); diff != "" { - t.Error("Unexpected traffic diff (-want +got):", diff) - } -} diff --git a/pkg/apis/caching/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/caching/v1alpha1/zz_generated.deepcopy.go index c94046d5..db05c88b 100644 --- a/pkg/apis/caching/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/caching/v1alpha1/zz_generated.deepcopy.go @@ -53,23 +53,6 @@ func (in *Image) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ImageCondition) DeepCopyInto(out *ImageCondition) { - *out = *in - in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageCondition. -func (in *ImageCondition) DeepCopy() *ImageCondition { - if in == nil { - return nil - } - out := new(ImageCondition) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageList) DeepCopyInto(out *ImageList) { *out = *in @@ -127,13 +110,7 @@ func (in *ImageSpec) DeepCopy() *ImageSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImageStatus) DeepCopyInto(out *ImageStatus) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]ImageCondition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.Status.DeepCopyInto(&out.Status) return } diff --git a/pkg/client/injection/reconciler/caching/v1alpha1/image/controller.go b/pkg/client/injection/reconciler/caching/v1alpha1/image/controller.go new file mode 100644 index 00000000..d8d68b24 --- /dev/null +++ b/pkg/client/injection/reconciler/caching/v1alpha1/image/controller.go @@ -0,0 +1,153 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package image + +import ( + context "context" + fmt "fmt" + reflect "reflect" + strings "strings" + + zap "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + scheme "k8s.io/client-go/kubernetes/scheme" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + record "k8s.io/client-go/tools/record" + versionedscheme "knative.dev/caching/pkg/client/clientset/versioned/scheme" + client "knative.dev/caching/pkg/client/injection/client" + image "knative.dev/caching/pkg/client/injection/informers/caching/v1alpha1/image" + kubeclient "knative.dev/pkg/client/injection/kube/client" + controller "knative.dev/pkg/controller" + logging "knative.dev/pkg/logging" + logkey "knative.dev/pkg/logging/logkey" + reconciler "knative.dev/pkg/reconciler" +) + +const ( + defaultControllerAgentName = "image-controller" + defaultFinalizerName = "images.caching.internal.knative.dev" +) + +// NewImpl returns a controller.Impl that handles queuing and feeding work from +// the queue through an implementation of controller.Reconciler, delegating to +// the provided Interface and optional Finalizer methods. OptionsFn is used to return +// controller.Options to be used by the internal reconciler. +func NewImpl(ctx context.Context, r Interface, optionsFns ...controller.OptionsFn) *controller.Impl { + logger := logging.FromContext(ctx) + + // Check the options function input. It should be 0 or 1. + if len(optionsFns) > 1 { + logger.Fatal("Up to one options function is supported, found: ", len(optionsFns)) + } + + imageInformer := image.Get(ctx) + + lister := imageInformer.Lister() + + rec := &reconcilerImpl{ + LeaderAwareFuncs: reconciler.LeaderAwareFuncs{ + PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error { + all, err := lister.List(labels.Everything()) + if err != nil { + return err + } + for _, elt := range all { + // TODO: Consider letting users specify a filter in options. + enq(bkt, types.NamespacedName{ + Namespace: elt.GetNamespace(), + Name: elt.GetName(), + }) + } + return nil + }, + }, + Client: client.Get(ctx), + Lister: lister, + reconciler: r, + finalizerName: defaultFinalizerName, + } + + ctrType := reflect.TypeOf(r).Elem() + ctrTypeName := fmt.Sprintf("%s.%s", ctrType.PkgPath(), ctrType.Name()) + ctrTypeName = strings.ReplaceAll(ctrTypeName, "/", ".") + + logger = logger.With( + zap.String(logkey.ControllerType, ctrTypeName), + zap.String(logkey.Kind, "caching.internal.knative.dev.Image"), + ) + + impl := controller.NewImpl(rec, logger, ctrTypeName) + agentName := defaultControllerAgentName + + // Pass impl to the options. Save any optional results. + for _, fn := range optionsFns { + opts := fn(impl) + if opts.ConfigStore != nil { + rec.configStore = opts.ConfigStore + } + if opts.FinalizerName != "" { + rec.finalizerName = opts.FinalizerName + } + if opts.AgentName != "" { + agentName = opts.AgentName + } + if opts.SkipStatusUpdates { + rec.skipStatusUpdates = true + } + if opts.DemoteFunc != nil { + rec.DemoteFunc = opts.DemoteFunc + } + } + + rec.Recorder = createRecorder(ctx, agentName) + + return impl +} + +func createRecorder(ctx context.Context, agentName string) record.EventRecorder { + logger := logging.FromContext(ctx) + + recorder := controller.GetEventRecorder(ctx) + if recorder == nil { + // Create event broadcaster + logger.Debug("Creating event broadcaster") + eventBroadcaster := record.NewBroadcaster() + watches := []watch.Interface{ + eventBroadcaster.StartLogging(logger.Named("event-broadcaster").Infof), + eventBroadcaster.StartRecordingToSink( + &v1.EventSinkImpl{Interface: kubeclient.Get(ctx).CoreV1().Events("")}), + } + recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: agentName}) + go func() { + <-ctx.Done() + for _, w := range watches { + w.Stop() + } + }() + } + + return recorder +} + +func init() { + versionedscheme.AddToScheme(scheme.Scheme) +} diff --git a/pkg/client/injection/reconciler/caching/v1alpha1/image/reconciler.go b/pkg/client/injection/reconciler/caching/v1alpha1/image/reconciler.go new file mode 100644 index 00000000..40fb75a2 --- /dev/null +++ b/pkg/client/injection/reconciler/caching/v1alpha1/image/reconciler.go @@ -0,0 +1,449 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package image + +import ( + context "context" + json "encoding/json" + fmt "fmt" + reflect "reflect" + + zap "go.uber.org/zap" + v1 "k8s.io/api/core/v1" + equality "k8s.io/apimachinery/pkg/api/equality" + errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + sets "k8s.io/apimachinery/pkg/util/sets" + record "k8s.io/client-go/tools/record" + v1alpha1 "knative.dev/caching/pkg/apis/caching/v1alpha1" + versioned "knative.dev/caching/pkg/client/clientset/versioned" + cachingv1alpha1 "knative.dev/caching/pkg/client/listers/caching/v1alpha1" + controller "knative.dev/pkg/controller" + kmp "knative.dev/pkg/kmp" + logging "knative.dev/pkg/logging" + reconciler "knative.dev/pkg/reconciler" +) + +// Interface defines the strongly typed interfaces to be implemented by a +// controller reconciling v1alpha1.Image. +type Interface interface { + // ReconcileKind implements custom logic to reconcile v1alpha1.Image. Any changes + // to the objects .Status or .Finalizers will be propagated to the stored + // object. It is recommended that implementors do not call any update calls + // for the Kind inside of ReconcileKind, it is the responsibility of the calling + // controller to propagate those properties. The resource passed to ReconcileKind + // will always have an empty deletion timestamp. + ReconcileKind(ctx context.Context, o *v1alpha1.Image) reconciler.Event +} + +// Finalizer defines the strongly typed interfaces to be implemented by a +// controller finalizing v1alpha1.Image. +type Finalizer interface { + // FinalizeKind implements custom logic to finalize v1alpha1.Image. Any changes + // to the objects .Status or .Finalizers will be ignored. Returning a nil or + // Normal type reconciler.Event will allow the finalizer to be deleted on + // the resource. The resource passed to FinalizeKind will always have a set + // deletion timestamp. + FinalizeKind(ctx context.Context, o *v1alpha1.Image) reconciler.Event +} + +// ReadOnlyInterface defines the strongly typed interfaces to be implemented by a +// controller reconciling v1alpha1.Image if they want to process resources for which +// they are not the leader. +type ReadOnlyInterface interface { + // ObserveKind implements logic to observe v1alpha1.Image. + // This method should not write to the API. + ObserveKind(ctx context.Context, o *v1alpha1.Image) reconciler.Event +} + +// ReadOnlyFinalizer defines the strongly typed interfaces to be implemented by a +// controller finalizing v1alpha1.Image if they want to process tombstoned resources +// even when they are not the leader. Due to the nature of how finalizers are handled +// there are no guarantees that this will be called. +type ReadOnlyFinalizer interface { + // ObserveFinalizeKind implements custom logic to observe the final state of v1alpha1.Image. + // This method should not write to the API. + ObserveFinalizeKind(ctx context.Context, o *v1alpha1.Image) reconciler.Event +} + +type doReconcile func(ctx context.Context, o *v1alpha1.Image) reconciler.Event + +// reconcilerImpl implements controller.Reconciler for v1alpha1.Image resources. +type reconcilerImpl struct { + // LeaderAwareFuncs is inlined to help us implement reconciler.LeaderAware. + reconciler.LeaderAwareFuncs + + // Client is used to write back status updates. + Client versioned.Interface + + // Listers index properties about resources. + Lister cachingv1alpha1.ImageLister + + // Recorder is an event recorder for recording Event resources to the + // Kubernetes API. + Recorder record.EventRecorder + + // configStore allows for decorating a context with config maps. + // +optional + configStore reconciler.ConfigStore + + // reconciler is the implementation of the business logic of the resource. + reconciler Interface + + // finalizerName is the name of the finalizer to reconcile. + finalizerName string + + // skipStatusUpdates configures whether or not this reconciler automatically updates + // the status of the reconciled resource. + skipStatusUpdates bool +} + +// Check that our Reconciler implements controller.Reconciler. +var _ controller.Reconciler = (*reconcilerImpl)(nil) + +// Check that our generated Reconciler is always LeaderAware. +var _ reconciler.LeaderAware = (*reconcilerImpl)(nil) + +func NewReconciler(ctx context.Context, logger *zap.SugaredLogger, client versioned.Interface, lister cachingv1alpha1.ImageLister, recorder record.EventRecorder, r Interface, options ...controller.Options) controller.Reconciler { + // Check the options function input. It should be 0 or 1. + if len(options) > 1 { + logger.Fatal("Up to one options struct is supported, found: ", len(options)) + } + + // Fail fast when users inadvertently implement the other LeaderAware interface. + // For the typed reconcilers, Promote shouldn't take any arguments. + if _, ok := r.(reconciler.LeaderAware); ok { + logger.Fatalf("%T implements the incorrect LeaderAware interface. Promote() should not take an argument as genreconciler handles the enqueuing automatically.", r) + } + // TODO: Consider validating when folks implement ReadOnlyFinalizer, but not Finalizer. + + rec := &reconcilerImpl{ + LeaderAwareFuncs: reconciler.LeaderAwareFuncs{ + PromoteFunc: func(bkt reconciler.Bucket, enq func(reconciler.Bucket, types.NamespacedName)) error { + all, err := lister.List(labels.Everything()) + if err != nil { + return err + } + for _, elt := range all { + // TODO: Consider letting users specify a filter in options. + enq(bkt, types.NamespacedName{ + Namespace: elt.GetNamespace(), + Name: elt.GetName(), + }) + } + return nil + }, + }, + Client: client, + Lister: lister, + Recorder: recorder, + reconciler: r, + finalizerName: defaultFinalizerName, + } + + for _, opts := range options { + if opts.ConfigStore != nil { + rec.configStore = opts.ConfigStore + } + if opts.FinalizerName != "" { + rec.finalizerName = opts.FinalizerName + } + if opts.SkipStatusUpdates { + rec.skipStatusUpdates = true + } + if opts.DemoteFunc != nil { + rec.DemoteFunc = opts.DemoteFunc + } + } + + return rec +} + +// Reconcile implements controller.Reconciler +func (r *reconcilerImpl) Reconcile(ctx context.Context, key string) error { + logger := logging.FromContext(ctx) + + // Initialize the reconciler state. This will convert the namespace/name + // string into a distinct namespace and name, determine if this instance of + // the reconciler is the leader, and any additional interfaces implemented + // by the reconciler. Returns an error is the resource key is invalid. + s, err := newState(key, r) + if err != nil { + logger.Error("Invalid resource key: ", key) + return nil + } + + // If we are not the leader, and we don't implement either ReadOnly + // observer interfaces, then take a fast-path out. + if s.isNotLeaderNorObserver() { + return controller.NewSkipKey(key) + } + + // If configStore is set, attach the frozen configuration to the context. + if r.configStore != nil { + ctx = r.configStore.ToContext(ctx) + } + + // Add the recorder to context. + ctx = controller.WithEventRecorder(ctx, r.Recorder) + + // Get the resource with this namespace/name. + + getter := r.Lister.Images(s.namespace) + + original, err := getter.Get(s.name) + + if errors.IsNotFound(err) { + // The resource may no longer exist, in which case we stop processing. + logger.Debugf("Resource %q no longer exists", key) + return nil + } else if err != nil { + return err + } + + // Don't modify the informers copy. + resource := original.DeepCopy() + + var reconcileEvent reconciler.Event + + name, do := s.reconcileMethodFor(resource) + // Append the target method to the logger. + logger = logger.With(zap.String("targetMethod", name)) + switch name { + case reconciler.DoReconcileKind: + // Set and update the finalizer on resource if r.reconciler + // implements Finalizer. + if resource, err = r.setFinalizerIfFinalizer(ctx, resource); err != nil { + return fmt.Errorf("failed to set finalizers: %w", err) + } + + if !r.skipStatusUpdates { + reconciler.PreProcessReconcile(ctx, resource) + } + + // Reconcile this copy of the resource and then write back any status + // updates regardless of whether the reconciliation errored out. + reconcileEvent = do(ctx, resource) + + if !r.skipStatusUpdates { + reconciler.PostProcessReconcile(ctx, resource, original) + } + + case reconciler.DoFinalizeKind: + // For finalizing reconcilers, if this resource being marked for deletion + // and reconciled cleanly (nil or normal event), remove the finalizer. + reconcileEvent = do(ctx, resource) + + if resource, err = r.clearFinalizer(ctx, resource, reconcileEvent); err != nil { + return fmt.Errorf("failed to clear finalizers: %w", err) + } + + case reconciler.DoObserveKind, reconciler.DoObserveFinalizeKind: + // Observe any changes to this resource, since we are not the leader. + reconcileEvent = do(ctx, resource) + + } + + // Synchronize the status. + switch { + case r.skipStatusUpdates: + // This reconciler implementation is configured to skip resource updates. + // This may mean this reconciler does not observe spec, but reconciles external changes. + case equality.Semantic.DeepEqual(original.Status, resource.Status): + // If we didn't change anything then don't call updateStatus. + // This is important because the copy we loaded from the injectionInformer's + // cache may be stale and we don't want to overwrite a prior update + // to status with this stale state. + case !s.isLeader: + // High-availability reconcilers may have many replicas watching the resource, but only + // the elected leader is expected to write modifications. + logger.Warn("Saw status changes when we aren't the leader!") + default: + if err = r.updateStatus(ctx, original, resource); err != nil { + logger.Warnw("Failed to update resource status", zap.Error(err)) + r.Recorder.Eventf(resource, v1.EventTypeWarning, "UpdateFailed", + "Failed to update status for %q: %v", resource.Name, err) + return err + } + } + + // Report the reconciler event, if any. + if reconcileEvent != nil { + var event *reconciler.ReconcilerEvent + if reconciler.EventAs(reconcileEvent, &event) { + logger.Infow("Returned an event", zap.Any("event", reconcileEvent)) + r.Recorder.Eventf(resource, event.EventType, event.Reason, event.Format, event.Args...) + + // the event was wrapped inside an error, consider the reconciliation as failed + if _, isEvent := reconcileEvent.(*reconciler.ReconcilerEvent); !isEvent { + return reconcileEvent + } + return nil + } + + logger.Errorw("Returned an error", zap.Error(reconcileEvent)) + r.Recorder.Event(resource, v1.EventTypeWarning, "InternalError", reconcileEvent.Error()) + return reconcileEvent + } + + return nil +} + +func (r *reconcilerImpl) updateStatus(ctx context.Context, existing *v1alpha1.Image, desired *v1alpha1.Image) error { + existing = existing.DeepCopy() + return reconciler.RetryUpdateConflicts(func(attempts int) (err error) { + // The first iteration tries to use the injectionInformer's state, subsequent attempts fetch the latest state via API. + if attempts > 0 { + + getter := r.Client.CachingV1alpha1().Images(desired.Namespace) + + existing, err = getter.Get(ctx, desired.Name, metav1.GetOptions{}) + if err != nil { + return err + } + } + + // If there's nothing to update, just return. + if reflect.DeepEqual(existing.Status, desired.Status) { + return nil + } + + if diff, err := kmp.SafeDiff(existing.Status, desired.Status); err == nil && diff != "" { + logging.FromContext(ctx).Debug("Updating status with: ", diff) + } + + existing.Status = desired.Status + + updater := r.Client.CachingV1alpha1().Images(existing.Namespace) + + _, err = updater.UpdateStatus(ctx, existing, metav1.UpdateOptions{}) + return err + }) +} + +// updateFinalizersFiltered will update the Finalizers of the resource. +// TODO: this method could be generic and sync all finalizers. For now it only +// updates defaultFinalizerName or its override. +func (r *reconcilerImpl) updateFinalizersFiltered(ctx context.Context, resource *v1alpha1.Image) (*v1alpha1.Image, error) { + + getter := r.Lister.Images(resource.Namespace) + + actual, err := getter.Get(resource.Name) + if err != nil { + return resource, err + } + + // Don't modify the informers copy. + existing := actual.DeepCopy() + + var finalizers []string + + // If there's nothing to update, just return. + existingFinalizers := sets.NewString(existing.Finalizers...) + desiredFinalizers := sets.NewString(resource.Finalizers...) + + if desiredFinalizers.Has(r.finalizerName) { + if existingFinalizers.Has(r.finalizerName) { + // Nothing to do. + return resource, nil + } + // Add the finalizer. + finalizers = append(existing.Finalizers, r.finalizerName) + } else { + if !existingFinalizers.Has(r.finalizerName) { + // Nothing to do. + return resource, nil + } + // Remove the finalizer. + existingFinalizers.Delete(r.finalizerName) + finalizers = existingFinalizers.List() + } + + mergePatch := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": finalizers, + "resourceVersion": existing.ResourceVersion, + }, + } + + patch, err := json.Marshal(mergePatch) + if err != nil { + return resource, err + } + + patcher := r.Client.CachingV1alpha1().Images(resource.Namespace) + + resourceName := resource.Name + updated, err := patcher.Patch(ctx, resourceName, types.MergePatchType, patch, metav1.PatchOptions{}) + if err != nil { + r.Recorder.Eventf(existing, v1.EventTypeWarning, "FinalizerUpdateFailed", + "Failed to update finalizers for %q: %v", resourceName, err) + } else { + r.Recorder.Eventf(updated, v1.EventTypeNormal, "FinalizerUpdate", + "Updated %q finalizers", resource.GetName()) + } + return updated, err +} + +func (r *reconcilerImpl) setFinalizerIfFinalizer(ctx context.Context, resource *v1alpha1.Image) (*v1alpha1.Image, error) { + if _, ok := r.reconciler.(Finalizer); !ok { + return resource, nil + } + + finalizers := sets.NewString(resource.Finalizers...) + + // If this resource is not being deleted, mark the finalizer. + if resource.GetDeletionTimestamp().IsZero() { + finalizers.Insert(r.finalizerName) + } + + resource.Finalizers = finalizers.List() + + // Synchronize the finalizers filtered by r.finalizerName. + return r.updateFinalizersFiltered(ctx, resource) +} + +func (r *reconcilerImpl) clearFinalizer(ctx context.Context, resource *v1alpha1.Image, reconcileEvent reconciler.Event) (*v1alpha1.Image, error) { + if _, ok := r.reconciler.(Finalizer); !ok { + return resource, nil + } + if resource.GetDeletionTimestamp().IsZero() { + return resource, nil + } + + finalizers := sets.NewString(resource.Finalizers...) + + if reconcileEvent != nil { + var event *reconciler.ReconcilerEvent + if reconciler.EventAs(reconcileEvent, &event) { + if event.EventType == v1.EventTypeNormal { + finalizers.Delete(r.finalizerName) + } + } + } else { + finalizers.Delete(r.finalizerName) + } + + resource.Finalizers = finalizers.List() + + // Synchronize the finalizers filtered by r.finalizerName. + return r.updateFinalizersFiltered(ctx, resource) +} diff --git a/pkg/client/injection/reconciler/caching/v1alpha1/image/state.go b/pkg/client/injection/reconciler/caching/v1alpha1/image/state.go new file mode 100644 index 00000000..e1020597 --- /dev/null +++ b/pkg/client/injection/reconciler/caching/v1alpha1/image/state.go @@ -0,0 +1,106 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package image + +import ( + fmt "fmt" + + types "k8s.io/apimachinery/pkg/types" + cache "k8s.io/client-go/tools/cache" + v1alpha1 "knative.dev/caching/pkg/apis/caching/v1alpha1" + reconciler "knative.dev/pkg/reconciler" +) + +// state is used to track the state of a reconciler in a single run. +type state struct { + // key is the original reconciliation key from the queue. + key string + // namespace is the namespace split from the reconciliation key. + namespace string + // name is the name split from the reconciliation key. + name string + // reconciler is the reconciler. + reconciler Interface + // roi is the read only interface cast of the reconciler. + roi ReadOnlyInterface + // isROI (Read Only Interface) the reconciler only observes reconciliation. + isROI bool + // rof is the read only finalizer cast of the reconciler. + rof ReadOnlyFinalizer + // isROF (Read Only Finalizer) the reconciler only observes finalize. + isROF bool + // isLeader the instance of the reconciler is the elected leader. + isLeader bool +} + +func newState(key string, r *reconcilerImpl) (*state, error) { + // Convert the namespace/name string into a distinct namespace and name. + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return nil, fmt.Errorf("invalid resource key: %s", key) + } + + roi, isROI := r.reconciler.(ReadOnlyInterface) + rof, isROF := r.reconciler.(ReadOnlyFinalizer) + + isLeader := r.IsLeaderFor(types.NamespacedName{ + Namespace: namespace, + Name: name, + }) + + return &state{ + key: key, + namespace: namespace, + name: name, + reconciler: r.reconciler, + roi: roi, + isROI: isROI, + rof: rof, + isROF: isROF, + isLeader: isLeader, + }, nil +} + +// isNotLeaderNorObserver checks to see if this reconciler with the current +// state is enabled to do any work or not. +// isNotLeaderNorObserver returns true when there is no work possible for the +// reconciler. +func (s *state) isNotLeaderNorObserver() bool { + if !s.isLeader && !s.isROI && !s.isROF { + // If we are not the leader, and we don't implement either ReadOnly + // interface, then take a fast-path out. + return true + } + return false +} + +func (s *state) reconcileMethodFor(o *v1alpha1.Image) (string, doReconcile) { + if o.GetDeletionTimestamp().IsZero() { + if s.isLeader { + return reconciler.DoReconcileKind, s.reconciler.ReconcileKind + } else if s.isROI { + return reconciler.DoObserveKind, s.roi.ObserveKind + } + } else if fin, ok := s.reconciler.(Finalizer); s.isLeader && ok { + return reconciler.DoFinalizeKind, fin.FinalizeKind + } else if !s.isLeader && s.isROF { + return reconciler.DoObserveFinalizeKind, s.rof.ObserveFinalizeKind + } + return "unknown", nil +} diff --git a/vendor/knative.dev/pkg/client/injection/kube/client/client.go b/vendor/knative.dev/pkg/client/injection/kube/client/client.go new file mode 100644 index 00000000..b7994cb7 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/client/client.go @@ -0,0 +1,57 @@ +/* +Copyright 2020 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package client + +import ( + context "context" + + kubernetes "k8s.io/client-go/kubernetes" + rest "k8s.io/client-go/rest" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterClient(withClient) + injection.Default.RegisterClientFetcher(func(ctx context.Context) interface{} { + return Get(ctx) + }) +} + +// Key is used as the key for associating information with a context.Context. +type Key struct{} + +func withClient(ctx context.Context, cfg *rest.Config) context.Context { + return context.WithValue(ctx, Key{}, kubernetes.NewForConfigOrDie(cfg)) +} + +// Get extracts the kubernetes.Interface client from the context. +func Get(ctx context.Context) kubernetes.Interface { + untyped := ctx.Value(Key{}) + if untyped == nil { + if injection.GetConfig(ctx) == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/kubernetes.Interface from context. This context is not the application context (which is typically given to constructors via sharedmain).") + } else { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/kubernetes.Interface from context.") + } + } + return untyped.(kubernetes.Interface) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 53459de8..c8953521 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -227,6 +227,7 @@ go.uber.org/atomic ## explicit go.uber.org/multierr # go.uber.org/zap v1.16.0 +## explicit go.uber.org/zap go.uber.org/zap/buffer go.uber.org/zap/internal/bufferpool @@ -666,6 +667,7 @@ knative.dev/pkg/apis/duck knative.dev/pkg/apis/duck/ducktypes knative.dev/pkg/apis/duck/v1 knative.dev/pkg/changeset +knative.dev/pkg/client/injection/kube/client knative.dev/pkg/codegen/cmd/injection-gen knative.dev/pkg/codegen/cmd/injection-gen/args knative.dev/pkg/codegen/cmd/injection-gen/generators