Skip to content

Commit 0003f00

Browse files
authored
Add status to KubernetesOperator (#467)
<!-- Thank you for contributing! Please make sure that your code changes are covered with tests. In case of new features or big changes remember to adjust the documentation. In case of an existing issue, reference it using one of the following: closes: #ISSUE related: #ISSUE How to write a good git commit message: http://chris.beams.io/posts/git-commit/ --> ## What *Describe what the change is solving* Update status fields Add basic registration status handling to KubernetesOperator ## How *Describe the solution* Use similar pattern to #464 ## Breaking Changes *Are there any breaking changes in this PR?* No.
2 parents 06944f8 + 1b88707 commit 0003f00

File tree

7 files changed

+154
-98
lines changed

7 files changed

+154
-98
lines changed

api/bindings/v1alpha1/endpointbinding_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ type BindingEndpoint struct {
125125
v6.Ref `json:",inline"`
126126

127127
// +kubebuilder:validation:Required
128+
// +kube:validation:Enum=provisioning;bound;error;unknown
128129
// +kubebuilder:default="unknown"
129130
Status BindingEndpointStatus `json:"status"`
130131

@@ -159,6 +160,7 @@ const (
159160
// +kubebuilder:printcolumn:name="URI",type="string",JSONPath=".spec.endpointURI"
160161
// +kubebuilder:printcolumn:name="Port",type="string",JSONPath=".spec.port"
161162
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.endpoints[0].status"
163+
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
162164
type EndpointBinding struct {
163165
metav1.TypeMeta `json:",inline"`
164166
metav1.ObjectMeta `json:"metadata,omitempty"`

api/ngrok/v1alpha1/kubernetesoperator_types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,30 @@ type KubernetesOperatorStatus struct {
8080

8181
// URI is the URI for this Kubernetes Operator
8282
URI string `json:"uri,omitempty"`
83+
84+
// RegistrationStatus is the status of the registration of this Kubernetes Operator with the ngrok API
85+
// +kubebuilder:validation:Required
86+
// +kube:validation:Enum=registered;error;pending
87+
// +kubebuilder:default="pending"
88+
RegistrationStatus string `json:"registrationStatus,omitempty"`
89+
90+
// RegistrationErrorCode is the returned ngrok error code
91+
// +kube:validation:Optional
92+
// +kubebuilder:validation:Pattern=`^NGROK_ERR_\d+$`
93+
RegistrationErrorCode string `json:"registrationErrorCode,omitempty"`
94+
95+
// RegistrationErrorMessage is a free-form error message if the status is error
96+
// +kubebuilder:validation:Optional
97+
// +kubebuilder:validation:MaxLength=4096
98+
RegistrationErrorMessage string `json:"errorMessage,omitempty"`
8399
}
84100

101+
const (
102+
KubernetesOperatorRegistrationStatusSuccess = "registered"
103+
KubernetesOperatorRegistrationStatusError = "error"
104+
KubernetesOperatorRegistrationStatusPending = "pending"
105+
)
106+
85107
const (
86108
KubernetesOperatorFeatureIngress = "ingress"
87109
KubernetesOperatorFeatureGateway = "gateway"
@@ -115,6 +137,7 @@ type KubernetesOperatorSpec struct {
115137
// +kubebuilder:object:root=true
116138
// +kubebuilder:subresource:status
117139
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`,description="Kubernetes Operator ID"
140+
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.registrationStatus"
118141
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age"
119142

120143
// KubernetesOperator is the Schema for the ngrok kubernetesoperators API

helm/ngrok-operator/templates/crds/bindings.k8s.ngrok.com_endpointbindings.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helm/ngrok-operator/templates/crds/ngrok.k8s.ngrok.com_kubernetesoperators.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/base_controller.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,22 @@ func (self *BaseController[T]) handleErr(op BaseControllerOp, obj T, err error)
136136
return CtrlResultForErr(err)
137137
}
138138

139+
// ReconcileStatus is a helper function to reconcile the status of an object and requeue on update errors
140+
// Note: obj must be the latest resource version from k8s api
141+
func (self *BaseController[T]) ReconcileStatus(ctx context.Context, obj T, origErr error) error {
142+
log := ctrl.LoggerFrom(ctx)
143+
144+
if err := self.Kube.Status().Update(ctx, obj); err != nil {
145+
self.Recorder.Event(obj, v1.EventTypeWarning, "StatusError", fmt.Sprintf("Failed to reconcile status: %s", err.Error()))
146+
log.V(1).Error(err, "Failed to update status")
147+
return fmt.Errorf("Error occured during status update: %w: %w", StatusError{origErr}, err)
148+
}
149+
150+
self.Recorder.Event(obj, v1.EventTypeNormal, "Status", "Successfully reconciled status")
151+
log.V(1).Info("Successfully updated status")
152+
return origErr
153+
}
154+
139155
// CtrlResultForErr is a helper function to convert an error into a ctrl.Result passing through ngrok error mappings
140156
func CtrlResultForErr(err error) (ctrl.Result, error) {
141157
var nerr *ngrok.Error
@@ -151,5 +167,20 @@ func CtrlResultForErr(err error) (ctrl.Result, error) {
151167
}
152168
}
153169

170+
// if error was because of status update, requeue for 10 seconds
171+
var serr *StatusError
172+
if errors.As(err, &serr) {
173+
return ctrl.Result{RequeueAfter: 10 * time.Second}, serr
174+
}
175+
154176
return ctrl.Result{}, err
155177
}
178+
179+
// StatusError wraps .Status().*() errors returned from k8s client
180+
type StatusError struct {
181+
err error
182+
}
183+
184+
func (e StatusError) Error() string {
185+
return e.err.Error()
186+
}

internal/controller/bindings/endpointbinding_controller.go

Lines changed: 32 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -160,59 +160,38 @@ func (r *EndpointBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
160160
// - ExternalName Target Service in the Target Namespace/Service name pointed at the Upstream Service
161161
// - Upstream Service in the ngrok-op namespace pointed at the Pod Forwarders
162162
func (r *EndpointBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
163-
return r.controller.Reconcile(ctx, req, new(bindingsv1alpha1.EndpointBinding))
163+
cr := &bindingsv1alpha1.EndpointBinding{}
164+
if ctrlErr, err := r.controller.Reconcile(ctx, req, cr); err != nil {
165+
return ctrlErr, err
166+
}
167+
168+
// update ngrok api resource status on upsert
169+
if controller.IsUpsert(cr) {
170+
if err := postEndpointBindingUpdateToNgrokAPI(ctx, cr); err != nil {
171+
return controller.CtrlResultForErr(r.controller.ReconcileStatus(ctx, cr, err))
172+
}
173+
}
174+
175+
// success
176+
return ctrl.Result{}, nil
164177
}
165178

166179
func (r *EndpointBindingReconciler) create(ctx context.Context, cr *bindingsv1alpha1.EndpointBinding) error {
167180
targetService, upstreamService := r.convertEndpointBindingToServices(cr)
168181

169-
// defer updating statuses
170-
defer func() {
171-
// best effort
172-
_ = r.updateEndpointBindingStatus(ctx, cr)
173-
174-
// best effort for now TODO(hkatz) retries?
175-
_ = postEndpointBindingUpdateToNgrokAPI(ctx, cr)
176-
}()
177-
178182
if err := r.createUpstreamService(ctx, cr, upstreamService); err != nil {
179-
return err
183+
return r.controller.ReconcileStatus(ctx, cr, err)
180184
}
181185

182186
if err := r.createTargetService(ctx, cr, targetService); err != nil {
183-
return err
187+
return r.controller.ReconcileStatus(ctx, cr, err)
184188
}
185189

186-
go r.tryToBindEndpointBinding(ctx, cr)
187-
188-
return nil
189-
}
190-
191-
// updateEndpointBindingStatus updates the status of the EndpointBinding to the desired state
192-
func (r *EndpointBindingReconciler) updateEndpointBindingStatus(ctx context.Context, desired *bindingsv1alpha1.EndpointBinding) error {
193-
log := ctrl.LoggerFrom(ctx).WithValues("name", desired.Name)
194-
195-
existing := desired.DeepCopy()
196-
if err := r.Client.Get(ctx, client.ObjectKeyFromObject(desired), existing); err != nil {
197-
log.Error(err, "Failed to get EndpointBinding for status update")
198-
r.Recorder.Event(desired, v1.EventTypeWarning, "UpdateFailed", "Failed to get EndpointBinding for status update")
199-
return err
190+
if err := r.tryToBindEndpointBinding(ctx, cr); err != nil {
191+
return r.controller.ReconcileStatus(ctx, cr, err)
200192
}
201193

202-
// use existing
203-
204-
// set status to desired
205-
existing.Status = desired.Status
206-
207-
if err := r.Client.Status().Update(ctx, existing); err != nil {
208-
log.Error(err, "Failed to update EndpointBinding status")
209-
r.Recorder.Event(existing, v1.EventTypeWarning, "UpdateFailed", "Failed to update EndpointBinding status")
210-
return err
211-
}
212-
213-
log.Info("Updated EndpointBinding status")
214-
r.Recorder.Event(existing, v1.EventTypeNormal, "Updated", "Updated EndpointBinding status")
215-
return nil
194+
return r.controller.ReconcileStatus(ctx, cr, nil)
216195
}
217196

218197
// setEndpointsStatus sets the status of every endpoint on endpointBinding to the desired status
@@ -251,7 +230,6 @@ func (r *EndpointBindingReconciler) createTargetService(ctx context.Context, own
251230
r.Recorder.Event(service, v1.EventTypeNormal, "Created", "Created Target Service")
252231
r.Recorder.Event(owner, v1.EventTypeNormal, "Created", "Created Target Service")
253232
log.Info("Created Upstream Service", "service", service.Name)
254-
255233
return nil
256234
}
257235

@@ -270,6 +248,7 @@ func (r *EndpointBindingReconciler) createUpstreamService(ctx context.Context, o
270248

271249
return err
272250
}
251+
273252
r.Recorder.Event(service, v1.EventTypeNormal, "Created", "Created Upstream Service")
274253
r.Recorder.Event(owner, v1.EventTypeNormal, "Created", "Created Upstream Service")
275254
log.Info("Created Upstream Service", "service", service.Name)
@@ -285,28 +264,19 @@ func (r *EndpointBindingReconciler) update(ctx context.Context, cr *bindingsv1al
285264
var existingTargetService v1.Service
286265
var existingUpstreamService v1.Service
287266

288-
// defer updating statuses
289-
defer func() {
290-
// best effort
291-
_ = r.updateEndpointBindingStatus(ctx, cr)
292-
293-
// best effort for now TODO(hkatz) retries?
294-
_ = postEndpointBindingUpdateToNgrokAPI(ctx, cr)
295-
}()
296-
297267
// upstream service
298268
err := r.Get(ctx, client.ObjectKey{Namespace: desiredUpstreamService.Namespace, Name: desiredUpstreamService.Name}, &existingUpstreamService)
299269
if err != nil {
300270
if client.IgnoreNotFound(err) == nil {
301271
// Upstream Service doesn't exist, create it
302272
log.Info("Unable to find existing Upstream Service, creating...", "name", desiredUpstreamService.Name)
303273
if err := r.createUpstreamService(ctx, cr, desiredUpstreamService); err != nil {
304-
return err
274+
return r.controller.ReconcileStatus(ctx, cr, err)
305275
}
306276
} else {
307277
// real error
308278
log.Error(err, "Failed to find existing Upstream Service", "name", cr.Name, "uri", cr.Spec.EndpointURI)
309-
return err
279+
return r.controller.ReconcileStatus(ctx, cr, err)
310280
}
311281
} else {
312282
// update upstream service
@@ -319,7 +289,7 @@ func (r *EndpointBindingReconciler) update(ctx context.Context, cr *bindingsv1al
319289
r.Recorder.Event(&existingUpstreamService, v1.EventTypeWarning, "UpdateFailed", "Failed to update Upstream Service")
320290
r.Recorder.Event(cr, v1.EventTypeWarning, "UpdateFailed", "Failed to update Upstream Service")
321291
log.Error(err, "Failed to update Upstream Service")
322-
return err
292+
return r.controller.ReconcileStatus(ctx, cr, err)
323293
}
324294
r.Recorder.Event(&existingUpstreamService, v1.EventTypeNormal, "Updated", "Updated Upstream Service")
325295
}
@@ -331,12 +301,12 @@ func (r *EndpointBindingReconciler) update(ctx context.Context, cr *bindingsv1al
331301
// Target Service doesn't exist, create it
332302
log.Info("Unable to find existing Target Service, creating...", "name", desiredTargetService.Name)
333303
if err := r.createTargetService(ctx, cr, desiredTargetService); err != nil {
334-
return err
304+
return r.controller.ReconcileStatus(ctx, cr, err)
335305
}
336306
} else {
337307
// real error
338308
log.Error(err, "Failed to find existing Target Service", "name", cr.Name, "uri", cr.Spec.EndpointURI)
339-
return err
309+
return r.controller.ReconcileStatus(ctx, cr, err)
340310
}
341311
} else {
342312
// update target service
@@ -349,15 +319,17 @@ func (r *EndpointBindingReconciler) update(ctx context.Context, cr *bindingsv1al
349319
r.Recorder.Event(&existingTargetService, v1.EventTypeWarning, "UpdateFailed", "Failed to update Target Service")
350320
r.Recorder.Event(cr, v1.EventTypeWarning, "UpdateFailed", "Failed to update Target Service")
351321
log.Error(err, "Failed to update Target Service")
352-
return err
322+
return r.controller.ReconcileStatus(ctx, cr, err)
353323
}
354324
r.Recorder.Event(&existingTargetService, v1.EventTypeNormal, "Updated", "Updated Target Service")
355325
}
356326

357-
go r.tryToBindEndpointBinding(ctx, cr)
327+
if err := r.tryToBindEndpointBinding(ctx, cr); err != nil {
328+
return r.controller.ReconcileStatus(ctx, cr, err)
329+
}
358330

359331
r.Recorder.Event(cr, v1.EventTypeNormal, "Updated", "Updated Services")
360-
return nil
332+
return r.controller.ReconcileStatus(ctx, cr, nil)
361333
}
362334

363335
func (r *EndpointBindingReconciler) delete(ctx context.Context, cr *bindingsv1alpha1.EndpointBinding) error {
@@ -549,7 +521,7 @@ func (r *EndpointBindingReconciler) findEndpointBindingsForService(ctx context.C
549521
}
550522

551523
// tryToBindEndpointBinding attempts a TCP connection through the provisioned services for the EndpointBinding
552-
func (r *EndpointBindingReconciler) tryToBindEndpointBinding(ctx context.Context, endpointBinding *bindingsv1alpha1.EndpointBinding) {
524+
func (r *EndpointBindingReconciler) tryToBindEndpointBinding(ctx context.Context, endpointBinding *bindingsv1alpha1.EndpointBinding) error {
553525
log := ctrl.LoggerFrom(ctx).WithValues("uri", endpointBinding.Spec.EndpointURI)
554526

555527
retries := 5
@@ -613,10 +585,5 @@ func (r *EndpointBindingReconciler) tryToBindEndpointBinding(ctx context.Context
613585

614586
// set status
615587
setEndpointsStatus(endpointBinding, desired)
616-
617-
// best effort
618-
_ = r.updateEndpointBindingStatus(ctx, endpointBinding)
619-
620-
// best effort for now TODO(hkatz) retries?
621-
_ = postEndpointBindingUpdateToNgrokAPI(ctx, endpointBinding)
588+
return bindErr
622589
}

0 commit comments

Comments
 (0)