From d8c931520ab9229b1f8f8b9091d43f96817144a4 Mon Sep 17 00:00:00 2001 From: zoetrope Date: Sun, 18 Aug 2024 14:14:04 +0000 Subject: [PATCH] =?UTF-8?q?MarkdownViewStatus=E3=81=A7metav1.Conditions?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20_manifests/api/v1/markdownview_types.go | 21 +-- .../api/v1/zz_generated.deepcopy.go | 24 +++ ...view.zoetrope.github.io_markdownviews.yaml | 88 +++++++++-- codes/30_client/api/v1/markdownview_types.go | 21 +-- .../30_client/api/v1/zz_generated.deepcopy.go | 24 +++ ...view.zoetrope.github.io_markdownviews.yaml | 88 +++++++++-- .../40_reconcile/api/v1/markdownview_types.go | 21 +-- .../api/v1/zz_generated.deepcopy.go | 24 +++ ...view.zoetrope.github.io_markdownviews.yaml | 88 +++++++++-- .../controller/markdownview_controller.go | 106 ++++++++++--- .../markdownview_controller_test.go | 4 +- .../50_completed/api/v1/markdownview_types.go | 21 +-- .../api/v1/zz_generated.deepcopy.go | 24 +++ ...view.zoetrope.github.io_markdownviews.yaml | 88 +++++++++-- .../controller/markdownview_controller.go | 139 ++++++++++++------ .../markdownview_controller_test.go | 2 +- .../internal/controller/metrics.go | 14 +- docs/controller-runtime/manager.md | 5 +- docs/controller-runtime/monitoring.md | 9 +- docs/controller-runtime/reconcile.md | 8 +- docs/controller-tools/crd.md | 16 +- 21 files changed, 660 insertions(+), 175 deletions(-) diff --git a/codes/20_manifests/api/v1/markdownview_types.go b/codes/20_manifests/api/v1/markdownview_types.go index effcbbe..45119c3 100644 --- a/codes/20_manifests/api/v1/markdownview_types.go +++ b/codes/20_manifests/api/v1/markdownview_types.go @@ -52,13 +52,17 @@ type MarkdownViewSpec struct { //! [status] // MarkdownViewStatus defines the observed state of MarkdownView -// +kubebuilder:validation:Enum=NotReady;Available;Healthy -type MarkdownViewStatus string +type MarkdownViewStatus struct { + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} const ( - MarkdownViewNotReady = MarkdownViewStatus("NotReady") - MarkdownViewAvailable = MarkdownViewStatus("Available") - MarkdownViewHealthy = MarkdownViewStatus("Healthy") + TypeMarkdownViewAvailable = "Available" + TypeMarkdownViewDegraded = "Degraded" ) //! [status] @@ -66,16 +70,15 @@ const ( //! [markdown-view] // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="REPLICAS",type="integer",JSONPath=".spec.replicas" -// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status" +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status" // MarkdownView is the Schema for the markdownviews API type MarkdownView struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec MarkdownViewSpec `json:"spec,omitempty"` - // +kubebuilder:default=NotReady + Spec MarkdownViewSpec `json:"spec,omitempty"` Status MarkdownViewStatus `json:"status,omitempty"` } diff --git a/codes/20_manifests/api/v1/zz_generated.deepcopy.go b/codes/20_manifests/api/v1/zz_generated.deepcopy.go index e2c89e7..d15f8cf 100644 --- a/codes/20_manifests/api/v1/zz_generated.deepcopy.go +++ b/codes/20_manifests/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,6 +31,7 @@ func (in *MarkdownView) DeepCopyInto(out *MarkdownView) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownView. @@ -103,3 +105,25 @@ func (in *MarkdownViewSpec) DeepCopy() *MarkdownViewSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MarkdownViewStatus) DeepCopyInto(out *MarkdownViewStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownViewStatus. +func (in *MarkdownViewStatus) DeepCopy() *MarkdownViewStatus { + if in == nil { + return nil + } + out := new(MarkdownViewStatus) + in.DeepCopyInto(out) + return out +} diff --git a/codes/20_manifests/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml b/codes/20_manifests/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml index 15b00d3..8819187 100644 --- a/codes/20_manifests/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml +++ b/codes/20_manifests/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml @@ -16,10 +16,10 @@ spec: versions: - additionalPrinterColumns: - jsonPath: .spec.replicas - name: REPLICAS + name: Replicas type: integer - - jsonPath: .status - name: STATUS + - jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available type: string name: v1 schema: @@ -65,13 +65,83 @@ spec: type: string type: object status: - default: NotReady description: MarkdownViewStatus defines the observed state of MarkdownView - enum: - - NotReady - - Available - - Healthy - type: string + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object type: object served: true storage: true diff --git a/codes/30_client/api/v1/markdownview_types.go b/codes/30_client/api/v1/markdownview_types.go index effcbbe..45119c3 100644 --- a/codes/30_client/api/v1/markdownview_types.go +++ b/codes/30_client/api/v1/markdownview_types.go @@ -52,13 +52,17 @@ type MarkdownViewSpec struct { //! [status] // MarkdownViewStatus defines the observed state of MarkdownView -// +kubebuilder:validation:Enum=NotReady;Available;Healthy -type MarkdownViewStatus string +type MarkdownViewStatus struct { + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} const ( - MarkdownViewNotReady = MarkdownViewStatus("NotReady") - MarkdownViewAvailable = MarkdownViewStatus("Available") - MarkdownViewHealthy = MarkdownViewStatus("Healthy") + TypeMarkdownViewAvailable = "Available" + TypeMarkdownViewDegraded = "Degraded" ) //! [status] @@ -66,16 +70,15 @@ const ( //! [markdown-view] // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="REPLICAS",type="integer",JSONPath=".spec.replicas" -// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status" +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status" // MarkdownView is the Schema for the markdownviews API type MarkdownView struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec MarkdownViewSpec `json:"spec,omitempty"` - // +kubebuilder:default=NotReady + Spec MarkdownViewSpec `json:"spec,omitempty"` Status MarkdownViewStatus `json:"status,omitempty"` } diff --git a/codes/30_client/api/v1/zz_generated.deepcopy.go b/codes/30_client/api/v1/zz_generated.deepcopy.go index e2c89e7..d15f8cf 100644 --- a/codes/30_client/api/v1/zz_generated.deepcopy.go +++ b/codes/30_client/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,6 +31,7 @@ func (in *MarkdownView) DeepCopyInto(out *MarkdownView) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownView. @@ -103,3 +105,25 @@ func (in *MarkdownViewSpec) DeepCopy() *MarkdownViewSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MarkdownViewStatus) DeepCopyInto(out *MarkdownViewStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownViewStatus. +func (in *MarkdownViewStatus) DeepCopy() *MarkdownViewStatus { + if in == nil { + return nil + } + out := new(MarkdownViewStatus) + in.DeepCopyInto(out) + return out +} diff --git a/codes/30_client/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml b/codes/30_client/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml index 15b00d3..8819187 100644 --- a/codes/30_client/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml +++ b/codes/30_client/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml @@ -16,10 +16,10 @@ spec: versions: - additionalPrinterColumns: - jsonPath: .spec.replicas - name: REPLICAS + name: Replicas type: integer - - jsonPath: .status - name: STATUS + - jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available type: string name: v1 schema: @@ -65,13 +65,83 @@ spec: type: string type: object status: - default: NotReady description: MarkdownViewStatus defines the observed state of MarkdownView - enum: - - NotReady - - Available - - Healthy - type: string + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object type: object served: true storage: true diff --git a/codes/40_reconcile/api/v1/markdownview_types.go b/codes/40_reconcile/api/v1/markdownview_types.go index effcbbe..45119c3 100644 --- a/codes/40_reconcile/api/v1/markdownview_types.go +++ b/codes/40_reconcile/api/v1/markdownview_types.go @@ -52,13 +52,17 @@ type MarkdownViewSpec struct { //! [status] // MarkdownViewStatus defines the observed state of MarkdownView -// +kubebuilder:validation:Enum=NotReady;Available;Healthy -type MarkdownViewStatus string +type MarkdownViewStatus struct { + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} const ( - MarkdownViewNotReady = MarkdownViewStatus("NotReady") - MarkdownViewAvailable = MarkdownViewStatus("Available") - MarkdownViewHealthy = MarkdownViewStatus("Healthy") + TypeMarkdownViewAvailable = "Available" + TypeMarkdownViewDegraded = "Degraded" ) //! [status] @@ -66,16 +70,15 @@ const ( //! [markdown-view] // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="REPLICAS",type="integer",JSONPath=".spec.replicas" -// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status" +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status" // MarkdownView is the Schema for the markdownviews API type MarkdownView struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec MarkdownViewSpec `json:"spec,omitempty"` - // +kubebuilder:default=NotReady + Spec MarkdownViewSpec `json:"spec,omitempty"` Status MarkdownViewStatus `json:"status,omitempty"` } diff --git a/codes/40_reconcile/api/v1/zz_generated.deepcopy.go b/codes/40_reconcile/api/v1/zz_generated.deepcopy.go index e2c89e7..d15f8cf 100644 --- a/codes/40_reconcile/api/v1/zz_generated.deepcopy.go +++ b/codes/40_reconcile/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,6 +31,7 @@ func (in *MarkdownView) DeepCopyInto(out *MarkdownView) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownView. @@ -103,3 +105,25 @@ func (in *MarkdownViewSpec) DeepCopy() *MarkdownViewSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MarkdownViewStatus) DeepCopyInto(out *MarkdownViewStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownViewStatus. +func (in *MarkdownViewStatus) DeepCopy() *MarkdownViewStatus { + if in == nil { + return nil + } + out := new(MarkdownViewStatus) + in.DeepCopyInto(out) + return out +} diff --git a/codes/40_reconcile/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml b/codes/40_reconcile/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml index 15b00d3..8819187 100644 --- a/codes/40_reconcile/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml +++ b/codes/40_reconcile/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml @@ -16,10 +16,10 @@ spec: versions: - additionalPrinterColumns: - jsonPath: .spec.replicas - name: REPLICAS + name: Replicas type: integer - - jsonPath: .status - name: STATUS + - jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available type: string name: v1 schema: @@ -65,13 +65,83 @@ spec: type: string type: object status: - default: NotReady description: MarkdownViewStatus defines the observed state of MarkdownView - enum: - - NotReady - - Available - - Healthy - type: string + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object type: object served: true storage: true diff --git a/codes/40_reconcile/internal/controller/markdownview_controller.go b/codes/40_reconcile/internal/controller/markdownview_controller.go index 9b2880b..e0d872a 100644 --- a/codes/40_reconcile/internal/controller/markdownview_controller.go +++ b/codes/40_reconcile/internal/controller/markdownview_controller.go @@ -20,10 +20,13 @@ package controller import ( "context" + viewv1 "github.com/zoetrope/markdown-view/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -35,8 +38,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - - viewv1 "github.com/zoetrope/markdown-view/api/v1" ) //! [import] @@ -91,15 +92,21 @@ func (r *MarkdownViewReconciler) Reconcile(ctx context.Context, req ctrl.Request err = r.reconcileConfigMap(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } err = r.reconcileDeployment(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } err = r.reconcileService(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } return r.updateStatus(ctx, mdView) @@ -314,33 +321,84 @@ func (r *MarkdownViewReconciler) reconcileService(ctx context.Context, mdView vi //! [update-status] func (r *MarkdownViewReconciler) updateStatus(ctx context.Context, mdView viewv1.MarkdownView) (ctrl.Result, error) { - var dep appsv1.Deployment - err := r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &dep) - if err != nil { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionTrue, + Reason: "OK", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionFalse, + Reason: "OK", + }) + + var cm corev1.ConfigMap + err := r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "markdowns-" + mdView.Name}, &cm) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "ConfigMap not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { return ctrl.Result{}, err } - var status viewv1.MarkdownViewStatus - if dep.Status.AvailableReplicas == 0 { - status = viewv1.MarkdownViewNotReady - } else if dep.Status.AvailableReplicas == mdView.Spec.Replicas { - status = viewv1.MarkdownViewHealthy - } else { - status = viewv1.MarkdownViewAvailable + var svc corev1.Service + err = r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &svc) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "Service not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { + return ctrl.Result{}, err } - if mdView.Status != status { - mdView.Status = status - err = r.Status().Update(ctx, &mdView) - if err != nil { - return ctrl.Result{}, err - } + var dep appsv1.Deployment + err = r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &dep) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "Deployment not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { + return ctrl.Result{}, err } - if mdView.Status != viewv1.MarkdownViewHealthy { - return ctrl.Result{Requeue: true}, nil + result := ctrl.Result{} + if dep.Status.AvailableReplicas == 0 { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Unavailable", + Message: "AvailableReplicas is 0", + }) + result = ctrl.Result{Requeue: true} } - return ctrl.Result{}, nil + + err = r.Status().Update(ctx, &mdView) + return result, err } //! [update-status] diff --git a/codes/40_reconcile/internal/controller/markdownview_controller_test.go b/codes/40_reconcile/internal/controller/markdownview_controller_test.go index 0e1302b..268d59c 100644 --- a/codes/40_reconcile/internal/controller/markdownview_controller_test.go +++ b/codes/40_reconcile/internal/controller/markdownview_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "context" - "k8s.io/utils/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -29,6 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -133,7 +133,7 @@ var _ = Describe("MarkdownView Controller", func() { updated := viewv1.MarkdownView{} err = k8sClient.Get(ctx, typeNamespacedName, &updated) Expect(err).NotTo(HaveOccurred()) - Expect(updated.Status).ShouldNot(BeEmpty(), "status should be updated") + Expect(updated.Status.Conditions).ShouldNot(BeEmpty(), "status should be updated") }) }) }) diff --git a/codes/50_completed/api/v1/markdownview_types.go b/codes/50_completed/api/v1/markdownview_types.go index effcbbe..45119c3 100644 --- a/codes/50_completed/api/v1/markdownview_types.go +++ b/codes/50_completed/api/v1/markdownview_types.go @@ -52,13 +52,17 @@ type MarkdownViewSpec struct { //! [status] // MarkdownViewStatus defines the observed state of MarkdownView -// +kubebuilder:validation:Enum=NotReady;Available;Healthy -type MarkdownViewStatus string +type MarkdownViewStatus struct { + // Conditions represent the latest available observations of an object's state + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} const ( - MarkdownViewNotReady = MarkdownViewStatus("NotReady") - MarkdownViewAvailable = MarkdownViewStatus("Available") - MarkdownViewHealthy = MarkdownViewStatus("Healthy") + TypeMarkdownViewAvailable = "Available" + TypeMarkdownViewDegraded = "Degraded" ) //! [status] @@ -66,16 +70,15 @@ const ( //! [markdown-view] // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="REPLICAS",type="integer",JSONPath=".spec.replicas" -// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status" +// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status" // MarkdownView is the Schema for the markdownviews API type MarkdownView struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec MarkdownViewSpec `json:"spec,omitempty"` - // +kubebuilder:default=NotReady + Spec MarkdownViewSpec `json:"spec,omitempty"` Status MarkdownViewStatus `json:"status,omitempty"` } diff --git a/codes/50_completed/api/v1/zz_generated.deepcopy.go b/codes/50_completed/api/v1/zz_generated.deepcopy.go index e2c89e7..d15f8cf 100644 --- a/codes/50_completed/api/v1/zz_generated.deepcopy.go +++ b/codes/50_completed/api/v1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,6 +31,7 @@ func (in *MarkdownView) DeepCopyInto(out *MarkdownView) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownView. @@ -103,3 +105,25 @@ func (in *MarkdownViewSpec) DeepCopy() *MarkdownViewSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MarkdownViewStatus) DeepCopyInto(out *MarkdownViewStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MarkdownViewStatus. +func (in *MarkdownViewStatus) DeepCopy() *MarkdownViewStatus { + if in == nil { + return nil + } + out := new(MarkdownViewStatus) + in.DeepCopyInto(out) + return out +} diff --git a/codes/50_completed/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml b/codes/50_completed/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml index 15b00d3..8819187 100644 --- a/codes/50_completed/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml +++ b/codes/50_completed/config/crd/bases/view.zoetrope.github.io_markdownviews.yaml @@ -16,10 +16,10 @@ spec: versions: - additionalPrinterColumns: - jsonPath: .spec.replicas - name: REPLICAS + name: Replicas type: integer - - jsonPath: .status - name: STATUS + - jsonPath: .status.conditions[?(@.type=="Available")].status + name: Available type: string name: v1 schema: @@ -65,13 +65,83 @@ spec: type: string type: object status: - default: NotReady description: MarkdownViewStatus defines the observed state of MarkdownView - enum: - - NotReady - - Available - - Healthy - type: string + properties: + conditions: + description: Conditions represent the latest available observations + of an object's state + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object type: object served: true storage: true diff --git a/codes/50_completed/internal/controller/markdownview_controller.go b/codes/50_completed/internal/controller/markdownview_controller.go index e651bde..e484ef6 100644 --- a/codes/50_completed/internal/controller/markdownview_controller.go +++ b/codes/50_completed/internal/controller/markdownview_controller.go @@ -20,12 +20,12 @@ package controller import ( "context" "fmt" - viewv1 "github.com/zoetrope/markdown-view/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -102,15 +102,21 @@ func (r *MarkdownViewReconciler) Reconcile(ctx context.Context, req ctrl.Request err = r.reconcileConfigMap(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } err = r.reconcileDeployment(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } err = r.reconcileService(ctx, mdView) if err != nil { - return ctrl.Result{}, err + result, err2 := r.updateStatus(ctx, mdView) + logger.Error(err2, "unable to update status") + return result, err } return r.updateStatus(ctx, mdView) @@ -339,41 +345,101 @@ func (r *MarkdownViewReconciler) reconcileService(ctx context.Context, mdView vi //! [update-status] func (r *MarkdownViewReconciler) updateStatus(ctx context.Context, mdView viewv1.MarkdownView) (ctrl.Result, error) { + prevStatus := mdView.Status.DeepCopy() + + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionTrue, + Reason: "OK", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionFalse, + Reason: "OK", + }) + + var cm corev1.ConfigMap + err := r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "markdowns-" + mdView.Name}, &cm) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "ConfigMap not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { + return ctrl.Result{}, err + } + + var svc corev1.Service + err = r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &svc) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "Service not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { + return ctrl.Result{}, err + } + var dep appsv1.Deployment - err := r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &dep) - if err != nil { + err = r.Get(ctx, client.ObjectKey{Namespace: mdView.Namespace, Name: "viewer-" + mdView.Name}, &dep) + if errors.IsNotFound(err) { + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewDegraded, + Status: metav1.ConditionTrue, + Reason: "Reconciling", + Message: "Deployment not found", + }) + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Reconciling", + }) + } else if err != nil { return ctrl.Result{}, err } - var status viewv1.MarkdownViewStatus + result := ctrl.Result{} if dep.Status.AvailableReplicas == 0 { - status = viewv1.MarkdownViewNotReady - } else if dep.Status.AvailableReplicas == mdView.Spec.Replicas { - status = viewv1.MarkdownViewHealthy - } else { - status = viewv1.MarkdownViewAvailable + meta.SetStatusCondition(&mdView.Status.Conditions, metav1.Condition{ + Type: viewv1.TypeMarkdownViewAvailable, + Status: metav1.ConditionFalse, + Reason: "Unavailable", + Message: "AvailableReplicas is 0", + }) + result = ctrl.Result{Requeue: true} } - if mdView.Status != status { - mdView.Status = status - //! [call-set-metrics] - r.setMetrics(mdView) - //! [call-set-metrics] + //! [call-set-metrics] + r.setMetrics(mdView) + //! [call-set-metrics] - //! [call-recorder-event] - r.Recorder.Event(&mdView, corev1.EventTypeNormal, "Updated", fmt.Sprintf("MarkdownView(%s:%s) updated: %s", mdView.Namespace, mdView.Name, mdView.Status)) - //! [call-recorder-event] - - err = r.Status().Update(ctx, &mdView) - if err != nil { - return ctrl.Result{}, err - } + //! [call-recorder-event] + if meta.IsStatusConditionFalse(prevStatus.Conditions, viewv1.TypeMarkdownViewDegraded) && + meta.IsStatusConditionTrue(mdView.Status.Conditions, viewv1.TypeMarkdownViewDegraded) { + r.Recorder.Event(&mdView, corev1.EventTypeWarning, "Degraded", fmt.Sprintf("MarkdownView(%s:%s) degraded", mdView.Namespace, mdView.Name)) } - - if mdView.Status != viewv1.MarkdownViewHealthy { - return ctrl.Result{Requeue: true}, nil + if meta.IsStatusConditionFalse(prevStatus.Conditions, viewv1.TypeMarkdownViewAvailable) && + meta.IsStatusConditionTrue(mdView.Status.Conditions, viewv1.TypeMarkdownViewAvailable) { + r.Recorder.Event(&mdView, corev1.EventTypeNormal, "Available", fmt.Sprintf("MarkdownView(%s:%s) available", mdView.Namespace, mdView.Name)) } - return ctrl.Result{}, nil + //! [call-recorder-event] + + err = r.Status().Update(ctx, &mdView) + return result, err } //! [update-status] @@ -381,19 +447,10 @@ func (r *MarkdownViewReconciler) updateStatus(ctx context.Context, mdView viewv1 //! [set-metrics] func (r *MarkdownViewReconciler) setMetrics(mdView viewv1.MarkdownView) { - switch mdView.Status { - case viewv1.MarkdownViewNotReady: - NotReadyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(1) - AvailableVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) - HealthyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) - case viewv1.MarkdownViewAvailable: - NotReadyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) + if meta.IsStatusConditionTrue(mdView.Status.Conditions, viewv1.TypeMarkdownViewAvailable) { AvailableVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(1) - HealthyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) - case viewv1.MarkdownViewHealthy: - NotReadyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) + } else { AvailableVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(0) - HealthyVec.WithLabelValues(mdView.Name, mdView.Namespace).Set(1) } } @@ -402,9 +459,7 @@ func (r *MarkdownViewReconciler) setMetrics(mdView viewv1.MarkdownView) { //! [remove-metrics] func (r *MarkdownViewReconciler) removeMetrics(mdView viewv1.MarkdownView) { - NotReadyVec.DeleteLabelValues(mdView.Name, mdView.Namespace) AvailableVec.DeleteLabelValues(mdView.Name, mdView.Namespace) - HealthyVec.DeleteLabelValues(mdView.Name, mdView.Namespace) } //! [remove-metrics] diff --git a/codes/50_completed/internal/controller/markdownview_controller_test.go b/codes/50_completed/internal/controller/markdownview_controller_test.go index 748f8ad..268d59c 100644 --- a/codes/50_completed/internal/controller/markdownview_controller_test.go +++ b/codes/50_completed/internal/controller/markdownview_controller_test.go @@ -133,7 +133,7 @@ var _ = Describe("MarkdownView Controller", func() { updated := viewv1.MarkdownView{} err = k8sClient.Get(ctx, typeNamespacedName, &updated) Expect(err).NotTo(HaveOccurred()) - Expect(updated.Status).ShouldNot(BeEmpty(), "status should be updated") + Expect(updated.Status.Conditions).ShouldNot(BeEmpty(), "status should be updated") }) }) }) diff --git a/codes/50_completed/internal/controller/metrics.go b/codes/50_completed/internal/controller/metrics.go index e0c0003..e0350bb 100644 --- a/codes/50_completed/internal/controller/metrics.go +++ b/codes/50_completed/internal/controller/metrics.go @@ -10,25 +10,13 @@ const ( ) var ( - NotReadyVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: metricsNamespace, - Name: "notready", - Help: "The cluster status about not ready condition", - }, []string{"name", "namespace"}) - AvailableVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "available", Help: "The cluster status about available condition", }, []string{"name", "namespace"}) - - HealthyVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: metricsNamespace, - Name: "healthy", - Help: "The cluster status about healthy condition", - }, []string{"name", "namespace"}) ) func init() { - metrics.Registry.MustRegister(NotReadyVec, AvailableVec, HealthyVec) + metrics.Registry.MustRegister(AvailableVec) } diff --git a/docs/controller-runtime/manager.md b/docs/controller-runtime/manager.md index 1753cd0..595dd75 100644 --- a/docs/controller-runtime/manager.md +++ b/docs/controller-runtime/manager.md @@ -82,9 +82,8 @@ Eventを記録するための関数として、`Event`, `Eventf`, `AnnotatedEven ``` $ kubectl get events -n default -LAST SEEN TYPE REASON OBJECT MESSAGE -14s Normal Updated markdownview/markdownview-sample MarkdownView(default:markdownview-sample) updated: NotReady -13s Normal Updated markdownview/markdownview-sample MarkdownView(default:markdownview-sample) updated: Healthy +LAST SEEN TYPE REASON OBJECT MESSAGE +4s Normal Available markdownview/markdownview-sample MarkdownView(default:markdownview-sample) available ``` ## HealthProbe diff --git a/docs/controller-runtime/monitoring.md b/docs/controller-runtime/monitoring.md index 834661e..868bc0c 100644 --- a/docs/controller-runtime/monitoring.md +++ b/docs/controller-runtime/monitoring.md @@ -74,7 +74,6 @@ controller-runtimeが提供するメトリクスだけでなく、カスタム 詳しくは[Prometheusのドキュメント](https://prometheus.io/docs/instrumenting/writing_exporters/)を参照してください。 ここではMarkdownViewリソースのステータスをメトリクスとして公開してみましょう。 -MarkdownViewには3種類のステータスがあるので、Gauge Vectorも3つ用意します。 [import, title="metrics.go"](../../codes/50_completed/internal/controller/metrics.go) @@ -102,13 +101,7 @@ $ curl localhost:8080/metrics # HELP markdownview_available The cluster status about available condition # TYPE markdownview_available gauge -markdownview_available{name="markdownview-sample",namespace="markdownview-sample"} 0 -# HELP markdownview_healthy The cluster status about healthy condition -# TYPE markdownview_healthy gauge -markdownview_healthy{name="markdownview-sample",namespace="markdownview-sample"} 1 -# HELP markdownview_notready The cluster status about not ready condition -# TYPE markdownview_notready gauge -markdownview_notready{name="markdownview-sample",namespace="markdownview-sample"} 0 +markdownview_available{name="markdownview-sample",namespace="markdownview-sample"} 1 ``` ## Grafanaでの可視化 diff --git a/docs/controller-runtime/reconcile.md b/docs/controller-runtime/reconcile.md index 77a5ea7..9fafe94 100644 --- a/docs/controller-runtime/reconcile.md +++ b/docs/controller-runtime/reconcile.md @@ -96,7 +96,7 @@ Reconcileの引数として渡ってきたRequestを利用して、対象とな そして、`reconcileConfigMap`, `reconcileDeployment`, `reconcileService`で、それぞれConfigMap, Deployment, Serviceリソースの作成・更新処理をおこないます。 -最後に`updateStatus`でステータスの更新をおこないます。 +エラーが発生した場合と、Reconcile処理の最後に`updateStatus`でステータスの更新をおこないます。 また、Reconcileの中では`logger := log.FromContext(ctx)`を呼び出してコンテキストからロガーを取得し、ログの出力をおこなうことができます。 このロガーを利用すると、Reconcile対象のオブジェクトのNamespaceやNameなどの情報が自動的にログに埋め込まれます。 @@ -131,7 +131,7 @@ DeploymentやServiceリソースはフィールド数が多いこともあり、 [import:"update-status"](../../codes/40_reconcile/internal/controller/markdownview_controller.go) -ここでは、`reconcileDeployment`で作成したDeploymentリソースをチェックし、その状態に応じてMarkdownViewリソースの +ここでは、Reconcile処理で作成したConfigMap, Service, Deploymentリソースをチェックし、その状態に応じてMarkdownViewリソースの ステータスを決定しています。 ## 動作確認 @@ -154,8 +154,8 @@ NAME DATA AGE configmap/markdowns-markdownview-sample 2 177m $ kubectl get markdownview markdownview-sample -NAME REPLICAS STATUS -markdownview-sample 1 Healthy +NAME REPLICAS AVAILABLE +markdownview-sample 1 True ``` 次にローカル環境から作成されたサービスにアクセスするため、Port Forwardをおこないます。 diff --git a/docs/controller-tools/crd.md b/docs/controller-tools/crd.md index 426db3a..3ae0f59 100644 --- a/docs/controller-tools/crd.md +++ b/docs/controller-tools/crd.md @@ -18,6 +18,10 @@ controller-genは、これらの構造体とマーカーを頼りにCRDの生成 一般的にカスタムリソースの`Spec`はユーザーが記述するもので、システムのあるべき状態をユーザーからコントローラーに伝える用途として利用されます。 一方の`Status`は、コントローラーが処理した結果をユーザーや他のシステムに伝える用途として利用されます。 +なお、CRDを利用してKubernetes APIを拡張する際には、以下の規約に従うことが推奨されています。一度目を通しておくとよいでしょう。 + +- [Kubernetes API Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) + ## MarkdownViewSpec さっそく`MarkdownViewSpec`を書き換えていきましょう。 @@ -93,9 +97,11 @@ Kubebuilderには`Required`以外にも様々なバリデーションが用意 [import:"status"](../../codes/20_manifests/api/v1/markdownview_types.go) -今回のカスタムコントローラーでは、`MarkdownViewStatus`を文字列型とし、`NotReady`,`Available`,`Healty`の3つの状態をあらわすようにしました。 +カスタムリソースの状態を表現するには、[`metav1.Condition`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1)を利用することが一般的です。 +今回のカスタムコントローラーでは、ConditionのTypeとして`Available`,`Degraded`の3つの状態を表現できるようにしました。 -`//+kubebuilder:validation:Enum`を利用すると、指定した文字列以外の値を設定できないようになります。 +- Available: レンダリングされたMarkdownが閲覧可能な状態 +- Degraded: Reconcile処理に失敗した状態 ## MarkdownView @@ -110,8 +116,6 @@ Kubebuilderが生成した初期状態では、`+kubebuilder:object:root=true` `+kubebuilder:subresource`と`+kubebuilder:printcolumn`マーカーについて、以降で解説します。 -また、`Status`フィールドに`+kubebuilder:default=NotReady`マーカーを付与することで、初期値を`NotReady`に設定しています。 - ### subresource `+kubebuilder:subresource:status`というマーカーを追加すると、`status`フィールドがサブリソースとして扱われるようになります。 @@ -139,8 +143,8 @@ kubectlでMarkdownViewリソースを取得すると、下記のようにREPLICA ``` $ kubectl get markdownview -NAME REPLICAS STATUS -MarkdownView-sample 1 NotReady +NAME REPLICAS AVAILABLE +MarkdownView-sample 1 ``` ## CRDマニフェストの生成