Skip to content

Commit e4eb3ed

Browse files
authored
Support allowedURLs (#496)
<!-- 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* Add regex validation to allowedURLs Add conversion functions and tests Create method to allow/deny BoundEndpoint projection Deny BoundEndpoints based on allowedURLs ## How *Describe the solution* ## Breaking Changes *Are there any breaking changes in this PR?* No.
2 parents 7f0172b + db21607 commit e4eb3ed

File tree

10 files changed

+505
-23
lines changed

10 files changed

+505
-23
lines changed

api/bindings/v1alpha1/boundendpoint_types.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ import (
3434

3535
// BoundEndpointSpec defines the desired state of BoundEndpoint
3636
type BoundEndpointSpec struct {
37+
// Allowed is a flag that determines if the BoundEndpoint is allowed to be projected into the cluster
38+
// This is controlled by the KubernetesOperator CRD .spec.allowedURLs field
39+
// +kubebuilder:validation:Required
40+
Allowed bool `json:"allowed"`
41+
3742
// EndpointURI is the unique identifier
3843
// representing the BoundEndpoint + its Endpoints
3944
// Format: <scheme>://<service>.<namespace>:<port>
@@ -125,7 +130,7 @@ type BindingEndpoint struct {
125130
v6.Ref `json:",inline"`
126131

127132
// +kubebuilder:validation:Required
128-
// +kube:validation:Enum=provisioning;bound;error;unknown
133+
// +kube:validation:Enum=provisioning;bound;denied;error;unknown
129134
// +kubebuilder:default="unknown"
130135
Status BindingEndpointStatus `json:"status"`
131136

@@ -143,12 +148,13 @@ type BindingEndpoint struct {
143148

144149
// BindingEndpointStatus is an enum that represents the status of a BindingEndpoint
145150
// TODO(https://github.com/ngrok-private/ngrok/issues/32666)
146-
// +kubebuilder:validation:Enum=unknown;provisioning;bound;error
151+
// +kubebuilder:validation:Enum=unknown;provisioning;denied;bound;error
147152
type BindingEndpointStatus string
148153

149154
const (
150155
StatusUnknown BindingEndpointStatus = "unknown"
151156
StatusProvisioning BindingEndpointStatus = "provisioning"
157+
StatusDenied BindingEndpointStatus = "denied"
152158
StatusBound BindingEndpointStatus = "bound"
153159
StatusError BindingEndpointStatus = "error"
154160
)

api/ngrok/v1alpha1/kubernetesoperator_types.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,14 @@ type KubernetesOperatorBinding struct {
5757
Name string `json:"name,omitempty"`
5858

5959
// AllowedURLs is a list of URI patterns ([scheme://]<service-name>.<namespace-name>) thet determine which BoundEndpoints are allowed to be created by the operator
60-
// TODO(hkatz) We are only implementing `*` for now
61-
// Support more patterns in the future, see product spec
60+
// You may specify a wildcard for:
61+
// - All endpoints: `*`
62+
// - All services in a namespace: `*.namespace`
63+
// - All namespaces: `*.*`
64+
// - Named service in all namespaces: `service.*`
65+
// See: https://regex101.com/r/APbE3G/4
6266
// +kubebuilder:validation:Required
63-
// +kubebuilder:validation:items:Pattern=`^[*]$`
67+
// +kubebuilder:validation:items:Pattern=`^(([*]|(https?|tls|tcp)://)?([*]|([*]|[a-z]([-a-z0-9]{0,61}[a-z0-9])?)[.]([*]|[a-z]([-a-z0-9]{0,61}[a-z0-9])?)))$`
6468
AllowedURLs []string `json:"allowedURLs,omitempty"`
6569

6670
// The public ingress endpoint for this Kubernetes Operator

cmd/api/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ func enableBindingsFeatureSet(_ context.Context, opts managerOpts, mgr ctrl.Mana
533533
Recorder: mgr.GetEventRecorderFor("endpoint-binding-poller"),
534534
Namespace: opts.namespace,
535535
KubernetesOperatorConfigName: opts.releaseName,
536+
AllowedURLs: opts.bindings.allowedURLs,
536537
TargetServiceAnnotations: targetServiceAnnotations,
537538
TargetServiceLabels: targetServiceLabels,
538539
PollingInterval: 10 * time.Second,
@@ -584,6 +585,8 @@ func createKubernetesOperator(ctx context.Context, client client.Client, opts ma
584585
}
585586
}
586587
k8sOperator.Spec.EnabledFeatures = features
588+
589+
setupLog.Info("created KubernetesOperator", "name", k8sOperator.Name, "namespace", k8sOperator.Namespace, "op", fmt.Sprintf("%+v", k8sOperator.Spec.Binding))
587590
return nil
588591
})
589592
return err

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

Lines changed: 7 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: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

helm/ngrok-operator/tests/controller-deployment_test.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ tests:
222222
- contains:
223223
path: spec.template.spec.containers[0].args
224224
content: --zap-log-level=error
225+
- it: Should pass bindings allowed urls if set
226+
set:
227+
bindings:
228+
enabled: true
229+
allowedURLs:
230+
- test.example
231+
- http://*
232+
template: controller-deployment.yaml
233+
documentIndex: 0 # Document 0 is the deployment since its the first template
234+
asserts:
235+
- contains:
236+
path: spec.template.spec.containers[0].args
237+
content: --bindings-allowed-urls=test.example,http://*
225238
- it: Should pass log format argument if set
226239
set:
227240
log:

helm/ngrok-operator/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ bindings:
281281
name: ""
282282
description: "Created by ngrok-operator"
283283
allowedURLs:
284-
- "*" # TODO(hkatz) only supporting this for now, implement others later
284+
- "*"
285285
serviceAnnotations: {}
286286
serviceLabels: {}
287287
ingressEndpoint: "kubernetes-binding-ingress.ngrok.io:443"

internal/controller/bindings/boundendpoint_controller.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const (
6565
NgrokErrorUpstreamServiceCreateFailed = "ERR_NGROK_0001"
6666
NgrokErrorTargetServiceCreateFailed = "ERR_NGROK_0002"
6767
NgrokErrorFailedToBind = "ERR_NGROK_003"
68+
NgrokErrorNotAllowed = "ERR_NGROK_004"
6869
)
6970

7071
var (
@@ -161,8 +162,10 @@ func (r *BoundEndpointReconciler) SetupWithManager(mgr ctrl.Manager) error {
161162
// - Upstream Service in the ngrok-op namespace pointed at the Pod Forwarders
162163
func (r *BoundEndpointReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
163164
cr := &bindingsv1alpha1.BoundEndpoint{}
164-
if ctrlErr, err := r.controller.Reconcile(ctx, req, cr); err != nil {
165-
return ctrlErr, err
165+
var res ctrl.Result
166+
var err error
167+
if res, err = r.controller.Reconcile(ctx, req, cr); err != nil {
168+
return res, err
166169
}
167170

168171
// update ngrok api resource status on upsert
@@ -179,6 +182,11 @@ func (r *BoundEndpointReconciler) Reconcile(ctx context.Context, req ctrl.Reques
179182
func (r *BoundEndpointReconciler) create(ctx context.Context, cr *bindingsv1alpha1.BoundEndpoint) error {
180183
targetService, upstreamService := r.convertBoundEndpointToServices(cr)
181184

185+
// binding is not allowed to be created
186+
if !cr.Spec.Allowed {
187+
return r.denyBoundEndpoint(ctx, cr)
188+
}
189+
182190
if err := r.createUpstreamService(ctx, cr, upstreamService); err != nil {
183191
return r.controller.ReconcileStatus(ctx, cr, err)
184192
}
@@ -259,6 +267,15 @@ func (r *BoundEndpointReconciler) createUpstreamService(ctx context.Context, own
259267
func (r *BoundEndpointReconciler) update(ctx context.Context, cr *bindingsv1alpha1.BoundEndpoint) error {
260268
log := ctrl.LoggerFrom(ctx)
261269

270+
// binding is not allowed
271+
if !cr.Spec.Allowed {
272+
if err := r.deleteBoundEndpointServices(ctx, cr); err != nil {
273+
return r.controller.ReconcileStatus(ctx, cr, err)
274+
}
275+
276+
return r.denyBoundEndpoint(ctx, cr)
277+
}
278+
262279
desiredTargetService, desiredUpstreamService := r.convertBoundEndpointToServices(cr)
263280

264281
var existingTargetService v1.Service
@@ -333,22 +350,40 @@ func (r *BoundEndpointReconciler) update(ctx context.Context, cr *bindingsv1alph
333350
}
334351

335352
func (r *BoundEndpointReconciler) delete(ctx context.Context, cr *bindingsv1alpha1.BoundEndpoint) error {
353+
return r.deleteBoundEndpointServices(ctx, cr)
354+
}
355+
356+
// deleteBoundEndpointServices deletes the Target and Upstream Services for the BoundEndpoint
357+
func (r *BoundEndpointReconciler) deleteBoundEndpointServices(ctx context.Context, cr *bindingsv1alpha1.BoundEndpoint) error {
336358
log := ctrl.LoggerFrom(ctx)
337359

338360
targetService, upstreamService := r.convertBoundEndpointToServices(cr)
339-
if err := r.Client.Delete(ctx, targetService); err != nil {
361+
362+
targetNamespace := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: targetService.Namespace}}
363+
if err := r.Client.Get(ctx, types.NamespacedName{Name: targetNamespace.Name}, targetNamespace); err != nil {
340364
if client.IgnoreNotFound(err) == nil {
341-
return nil
365+
// fallthrough, no Target Service to delete
342366
} else {
343-
r.Recorder.Event(cr, v1.EventTypeWarning, "Delete", "Failed to delete Target Service")
344-
log.Error(err, "Failed to delete Target Service")
367+
log.Error(err, "Failed to get Target Namespace")
345368
return err
346369
}
370+
} else {
371+
// Target Namespace exists, try to delete the Target Service
372+
373+
if err := r.Client.Delete(ctx, targetService); err != nil {
374+
if client.IgnoreNotFound(err) == nil {
375+
return nil
376+
} else {
377+
r.Recorder.Event(cr, v1.EventTypeWarning, "Delete", "Failed to delete Target Service")
378+
log.Error(err, "Failed to delete Target Service")
379+
return err
380+
}
381+
}
347382
}
348383

349384
if err := r.Client.Delete(ctx, upstreamService); err != nil {
350385
if client.IgnoreNotFound(err) == nil {
351-
return nil
386+
// fallthrough, nothing to do
352387
} else {
353388
r.Recorder.Event(cr, v1.EventTypeWarning, "Delete", "Failed to delete Upstream Service")
354389
log.Error(err, "Failed to delete Upstream Service")
@@ -555,6 +590,7 @@ func (r *BoundEndpointReconciler) tryToBindEndpoint(ctx context.Context, boundEn
555590
bindErr = err
556591
} else {
557592
// success case: we dialed and closed the connection
593+
bindErr = nil
558594
break
559595
}
560596
}
@@ -567,13 +603,15 @@ func (r *BoundEndpointReconciler) tryToBindEndpoint(ctx context.Context, boundEn
567603
var desired *bindingsv1alpha1.BindingEndpoint
568604
if bindErr != nil {
569605
// error
606+
log.Error(bindErr, "Failed to bind BoundEndpoint, moving to error")
570607
desired = &bindingsv1alpha1.BindingEndpoint{
571608
Status: bindingsv1alpha1.StatusError,
572609
ErrorCode: NgrokErrorFailedToBind,
573610
ErrorMessage: fmt.Sprintf("Failed to bind BoundEndpoint: %s", bindErr),
574611
}
575612
} else {
576613
// success
614+
log.Info("Bound BoundEndpoint successfully, moving to bound")
577615
desired = &bindingsv1alpha1.BindingEndpoint{
578616
Status: bindingsv1alpha1.StatusBound,
579617
ErrorCode: "",
@@ -586,3 +624,17 @@ func (r *BoundEndpointReconciler) tryToBindEndpoint(ctx context.Context, boundEn
586624
setEndpointsStatus(boundEndpoint, desired)
587625
return bindErr
588626
}
627+
628+
// denyBoundEndpoint sets the status of the BoundEndpoint to denied
629+
func (r *BoundEndpointReconciler) denyBoundEndpoint(ctx context.Context, boundEndpoint *bindingsv1alpha1.BoundEndpoint) error {
630+
reason := "Endpoint URI is not allowed by KubernetesOperator allowedURLs configuration"
631+
632+
setEndpointsStatus(boundEndpoint, &bindingsv1alpha1.BindingEndpoint{
633+
Status: bindingsv1alpha1.StatusDenied,
634+
ErrorCode: NgrokErrorNotAllowed,
635+
ErrorMessage: reason,
636+
})
637+
638+
r.Recorder.Event(boundEndpoint, v1.EventTypeWarning, "Denied", reason)
639+
return r.controller.ReconcileStatus(ctx, boundEndpoint, nil)
640+
}

0 commit comments

Comments
 (0)