From e8cf377f00aa7f81c7004a252733ab9bd3044dd3 Mon Sep 17 00:00:00 2001 From: Lin Yang Date: Thu, 1 Aug 2024 16:23:47 +0800 Subject: [PATCH 1/4] feat: listener scope filter Signed-off-by: Lin Yang --- .../extension.gateway.flomesh.io_filters.yaml | 43 +++++++++- pkg/apis/extension/v1alpha1/filter.go | 66 +++------------ pkg/apis/extension/v1alpha1/shared_types.go | 83 +++++++++++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 31 +++++++ .../extension/v1alpha1/filter_controller.go | 46 +++++----- pkg/gateway/fgw/config.go | 6 ++ pkg/gateway/processor/v2/gateway.go | 63 ++++++++++++++ pkg/gateway/processor/v2/grpcroute.go | 7 +- pkg/gateway/processor/v2/httproute.go | 7 +- pkg/webhook/extension/v1alpha1/filter.go | 46 +++++++++- 10 files changed, 318 insertions(+), 80 deletions(-) create mode 100644 pkg/apis/extension/v1alpha1/shared_types.go diff --git a/cmd/fsm-bootstrap/crds/extension.gateway.flomesh.io_filters.yaml b/cmd/fsm-bootstrap/crds/extension.gateway.flomesh.io_filters.yaml index ed68fe2ee..9ba2b9b59 100644 --- a/cmd/fsm-bootstrap/crds/extension.gateway.flomesh.io_filters.yaml +++ b/cmd/fsm-bootstrap/crds/extension.gateway.flomesh.io_filters.yaml @@ -61,11 +61,53 @@ spec: - http - tcp type: string + scope: + default: Route + description: Scope is the scope of filter + enum: + - Route + - Listener + type: string script: description: Script is the list of scripts to be executed, key is the script name and value is the script content minLength: 1 type: string + targetRefs: + description: TargetRefs is the references to the target resources + to which the filter is applied + items: + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + port: + description: port is the port of the target listener. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - group + - kind + - name + - port + type: object + maxItems: 16 + type: array type: description: Type is the type of the filter in PascalCase, it should be unique within the namespace @@ -74,7 +116,6 @@ spec: pattern: ^[A-Z](([a-z0-9]+[A-Z]?)*)$ type: string required: - - protocol - script - type type: object diff --git a/pkg/apis/extension/v1alpha1/filter.go b/pkg/apis/extension/v1alpha1/filter.go index 13a26d4bc..92103f08f 100644 --- a/pkg/apis/extension/v1alpha1/filter.go +++ b/pkg/apis/extension/v1alpha1/filter.go @@ -4,23 +4,24 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// FilterProtocol defines the protocol of filter -type FilterProtocol string - -const ( - // FilterProtocolHTTP is the type of filter for HTTP/HTTPS/GRPC/GRPCS protocols - FilterProtocolHTTP FilterProtocol = "http" - - // FilterProtocolTCP is the type of filter for TCP protocol - FilterProtocolTCP FilterProtocol = "tcp" -) - // FilterSpec defines the desired state of Filter type FilterSpec struct { + // +optional + // +kubebuilder:validation:MaxItems=16 + // TargetRefs is the references to the target resources to which the filter is applied + TargetRefs []LocalPolicyTargetReferenceWithPort `json:"targetRefs"` + + // Scope is the scope of filter + // +optional + // +kubebuilder:default=Route + // +kubebuilder:validation:Enum=Route;Listener + Scope *FilterScope `json:"scope"` + // Protocol is the protocol of filter + // +optional // +kubebuilder:default=http // +kubebuilder:validation:Enum=http;tcp - Protocol FilterProtocol `json:"protocol"` + Protocol *FilterProtocol `json:"protocol"` // Type is the type of the filter in PascalCase, it should be unique within the namespace // +kubebuilder:validation:Pattern=`^[A-Z](([a-z0-9]+[A-Z]?)*)$` @@ -56,47 +57,6 @@ type Filter struct { Status FilterStatus `json:"status,omitempty"` } -// FilterConditionType is a type of condition for a filter. This type should be -// used with a Filter resource Status.Conditions field. -type FilterConditionType string - -// FilterConditionReason is a reason for a policy condition. -type FilterConditionReason string - -const ( - // FilterConditionAccepted indicates whether the filter has been accepted or - // rejected by a targeted resource, and why. - // - // Possible reasons for this condition to be True are: - // - // * "Accepted" - // - // Possible reasons for this condition to be False are: - // - // * "Conflicted" - // * "Invalid" - // * "TargetNotFound" - // - FilterConditionAccepted FilterConditionType = "Accepted" - - // FilterReasonAccepted is used with the "Accepted" condition when the policy - // has been accepted by the targeted resource. - FilterReasonAccepted FilterConditionReason = "Accepted" - - // FilterReasonConflicted is used with the "Accepted" condition when the - // policy has not been accepted by a targeted resource because there is - // another policy that targets the same resource and a merge is not possible. - FilterReasonConflicted FilterConditionReason = "Conflicted" - - // FilterReasonInvalid is used with the "Accepted" condition when the policy - // is syntactically or semantically invalid. - FilterReasonInvalid FilterConditionReason = "Invalid" - - // FilterReasonTargetNotFound is used with the "Accepted" condition when the - // policy is attached to an invalid target resource. - FilterReasonTargetNotFound FilterConditionReason = "TargetNotFound" -) - // FilterStatus defines the common attributes that all filters should include within // their status. type FilterStatus struct { diff --git a/pkg/apis/extension/v1alpha1/shared_types.go b/pkg/apis/extension/v1alpha1/shared_types.go new file mode 100644 index 000000000..4d61619ea --- /dev/null +++ b/pkg/apis/extension/v1alpha1/shared_types.go @@ -0,0 +1,83 @@ +package v1alpha1 + +import gwv1 "sigs.k8s.io/gateway-api/apis/v1" + +type LocalPolicyTargetReferenceWithPort struct { + // Group is the group of the target resource. + Group gwv1.Group `json:"group"` + + // Kind is kind of the target resource. + Kind gwv1.Kind `json:"kind"` + + // Name is the name of the target resource. + Name gwv1.ObjectName `json:"name"` + + // port is the port of the target listener. + Port gwv1.PortNumber `json:"port"` +} + +// FilterScope defines the scope of filter +type FilterScope string + +const ( + // FilterScopeListener is the type of filter for listener + FilterScopeListener FilterScope = "Listener" + + // FilterScopeRoute is the type of filter for route + FilterScopeRoute FilterScope = "Route" +) + +// FilterProtocol defines the protocol of filter +type FilterProtocol string + +const ( + // FilterProtocolHTTP is the type of filter for HTTP/HTTPS/GRPC/GRPCS protocols + FilterProtocolHTTP FilterProtocol = "http" + + // FilterProtocolTCP is the type of filter for TCP protocol + FilterProtocolTCP FilterProtocol = "tcp" + + // FilterProtocolUDP is the type of filter for UDP protocol + FilterProtocolUDP FilterProtocol = "udp" +) + +// FilterConditionType is a type of condition for a filter. This type should be +// used with a Filter resource Status.Conditions field. +type FilterConditionType string + +// FilterConditionReason is a reason for a policy condition. +type FilterConditionReason string + +const ( + // FilterConditionAccepted indicates whether the filter has been accepted or + // rejected by a targeted resource, and why. + // + // Possible reasons for this condition to be True are: + // + // * "Accepted" + // + // Possible reasons for this condition to be False are: + // + // * "Conflicted" + // * "Invalid" + // * "TargetNotFound" + // + FilterConditionAccepted FilterConditionType = "Accepted" + + // FilterReasonAccepted is used with the "Accepted" condition when the policy + // has been accepted by the targeted resource. + FilterReasonAccepted FilterConditionReason = "Accepted" + + // FilterReasonConflicted is used with the "Accepted" condition when the + // policy has not been accepted by a targeted resource because there is + // another policy that targets the same resource and a merge is not possible. + FilterReasonConflicted FilterConditionReason = "Conflicted" + + // FilterReasonInvalid is used with the "Accepted" condition when the policy + // is syntactically or semantically invalid. + FilterReasonInvalid FilterConditionReason = "Invalid" + + // FilterReasonTargetNotFound is used with the "Accepted" condition when the + // policy is attached to an invalid target resource. + FilterReasonTargetNotFound FilterConditionReason = "TargetNotFound" +) diff --git a/pkg/apis/extension/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/extension/v1alpha1/zz_generated.deepcopy.go index 854036764..88daea454 100644 --- a/pkg/apis/extension/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/extension/v1alpha1/zz_generated.deepcopy.go @@ -87,6 +87,21 @@ func (in *FilterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FilterSpec) DeepCopyInto(out *FilterSpec) { *out = *in + if in.TargetRefs != nil { + in, out := &in.TargetRefs, &out.TargetRefs + *out = make([]LocalPolicyTargetReferenceWithPort, len(*in)) + copy(*out, *in) + } + if in.Scope != nil { + in, out := &in.Scope, &out.Scope + *out = new(FilterScope) + **out = **in + } + if in.Protocol != nil { + in, out := &in.Protocol, &out.Protocol + *out = new(FilterProtocol) + **out = **in + } if in.Config != nil { in, out := &in.Config, &out.Config *out = make(map[string]string, len(*in)) @@ -129,3 +144,19 @@ func (in *FilterStatus) DeepCopy() *FilterStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalPolicyTargetReferenceWithPort) DeepCopyInto(out *LocalPolicyTargetReferenceWithPort) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalPolicyTargetReferenceWithPort. +func (in *LocalPolicyTargetReferenceWithPort) DeepCopy() *LocalPolicyTargetReferenceWithPort { + if in == nil { + return nil + } + out := new(LocalPolicyTargetReferenceWithPort) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controllers/extension/v1alpha1/filter_controller.go b/pkg/controllers/extension/v1alpha1/filter_controller.go index dc4bf56ee..046f10d49 100644 --- a/pkg/controllers/extension/v1alpha1/filter_controller.go +++ b/pkg/controllers/extension/v1alpha1/filter_controller.go @@ -2,6 +2,12 @@ package v1alpha1 import ( "context" + "fmt" + + "github.com/flomesh-io/fsm/pkg/constants" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -69,26 +75,26 @@ func (r *filterReconciler) SetupWithManager(mgr ctrl.Manager) error { } func addFilterIndexers(ctx context.Context, mgr manager.Manager) error { - //if err := mgr.GetFieldIndexer().IndexField(ctx, &extv1alpha1.Filter{}, constants.GatewayFilterIndex, func(obj client.Object) []string { - // filter := obj.(*extv1alpha1.Filter) - // - // var gateways []string - // for _, targetRef := range filter.Spec.TargetRefs { - // if string(targetRef.Kind) == constants.GatewayAPIGatewayKind && - // string(targetRef.Group) == gwv1.GroupName { - // gateways = append(gateways, - // types.NamespacedName{ - // Namespace: filter.Namespace, - // Name: string(targetRef.Name), - // }.String(), - // ) - // } - // } - // - // return gateways - //}); err != nil { - // return err - //} + if err := mgr.GetFieldIndexer().IndexField(ctx, &extv1alpha1.Filter{}, constants.GatewayFilterIndex, func(obj client.Object) []string { + filter := obj.(*extv1alpha1.Filter) + + scope := ptr.Deref(filter.Spec.Scope, extv1alpha1.FilterScopeRoute) + if scope != extv1alpha1.FilterScopeListener { + return nil + } + + var gateways []string + for _, targetRef := range filter.Spec.TargetRefs { + if string(targetRef.Kind) == constants.GatewayAPIGatewayKind && + string(targetRef.Group) == gwv1.GroupName { + gateways = append(gateways, fmt.Sprintf("%s/%d", string(targetRef.Name), targetRef.Port)) + } + } + + return gateways + }); err != nil { + return err + } return nil } diff --git a/pkg/gateway/fgw/config.go b/pkg/gateway/fgw/config.go index 69edb4a29..177a16781 100644 --- a/pkg/gateway/fgw/config.go +++ b/pkg/gateway/fgw/config.go @@ -62,6 +62,12 @@ type Listener struct { Port gwv1.PortNumber `json:"port"` Protocol gwv1.ProtocolType `json:"protocol"` TLS *GatewayTLSConfig `json:"tls,omitempty" copier:"-"` + Filters []ListenerFilter `json:"filters,omitempty" hash:"set" copier:"-"` +} + +type ListenerFilter struct { + Type string `json:"type"` + ExtensionConfig map[string]string `json:"extensionConfig,omitempty"` } type GatewayTLSConfig struct { diff --git a/pkg/gateway/processor/v2/gateway.go b/pkg/gateway/processor/v2/gateway.go index b67c056d0..8744039c9 100644 --- a/pkg/gateway/processor/v2/gateway.go +++ b/pkg/gateway/processor/v2/gateway.go @@ -1,8 +1,16 @@ package v2 import ( + "context" "fmt" + "k8s.io/utils/ptr" + + extv1alpha1 "github.com/flomesh-io/fsm/pkg/apis/extension/v1alpha1" + "github.com/flomesh-io/fsm/pkg/constants" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/client" + fgwv2 "github.com/flomesh-io/fsm/pkg/gateway/fgw" corev1 "k8s.io/api/core/v1" @@ -48,6 +56,9 @@ func (c *ConfigGenerator) processGateway() *fgwv2.Gateway { c.processCACerts(l, v2l) } + // process listener filters + c.processListenerFilters(l, v2l) + g2.Spec.Listeners = append(g2.Spec.Listeners, *v2l) } @@ -128,3 +139,55 @@ func (c *ConfigGenerator) processCACerts(l gwtypes.Listener, v2l *fgwv2.Listener } } } + +func (c *ConfigGenerator) processListenerFilters(l gwtypes.Listener, v2l *fgwv2.Listener) { + list := &extv1alpha1.FilterList{} + if err := c.client.List(context.Background(), list, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(constants.GatewayFilterIndex, fmt.Sprintf("%s/%d", c.gateway.Name, l.Port)), + Namespace: c.gateway.Namespace, + }); err != nil { + return + } + + if len(list.Items) == 0 { + return + } + + v2l.Filters = make([]fgwv2.ListenerFilter, 0) + for _, f := range list.Items { + scope := ptr.Deref(f.Spec.Scope, extv1alpha1.FilterScopeRoute) + if scope != extv1alpha1.FilterScopeListener { + continue + } + + protocol := toFilterProtocol(l.Protocol) + if protocol == nil { + continue + } + + filterType := f.Spec.Type + + v2l.Filters = append(v2l.Filters, fgwv2.ListenerFilter{ + Type: filterType, + ExtensionConfig: f.Spec.Config, + }) + + if c.filters[*protocol] == nil { + c.filters[*protocol] = map[string]string{} + } + c.filters[*protocol][filterType] = f.Spec.Script + } +} + +func toFilterProtocol(protocol gwv1.ProtocolType) *extv1alpha1.FilterProtocol { + switch protocol { + case gwv1.HTTPProtocolType, gwv1.HTTPSProtocolType, gwv1.TLSProtocolType: + return ptr.To(extv1alpha1.FilterProtocolHTTP) + case gwv1.TCPProtocolType: + return ptr.To(extv1alpha1.FilterProtocolTCP) + case gwv1.UDPProtocolType: + return ptr.To(extv1alpha1.FilterProtocolUDP) + default: + return nil + } +} diff --git a/pkg/gateway/processor/v2/grpcroute.go b/pkg/gateway/processor/v2/grpcroute.go index d6fb47b18..9d8261681 100644 --- a/pkg/gateway/processor/v2/grpcroute.go +++ b/pkg/gateway/processor/v2/grpcroute.go @@ -147,7 +147,12 @@ func (c *ConfigGenerator) toV2GRPCRouteFilters(grpcRoute *gwv1.GRPCRoute, routeF case gwv1.GRPCRouteFilterExtensionRef: filter := gwutils.ExtensionRefToFilter(c.client, grpcRoute, f.ExtensionRef) - filterProtocol := filter.Spec.Protocol + scope := ptr.Deref(filter.Spec.Scope, extv1alpha1.FilterScopeRoute) + if scope != extv1alpha1.FilterScopeRoute { + continue + } + + filterProtocol := ptr.Deref(filter.Spec.Protocol, extv1alpha1.FilterProtocolHTTP) if filterProtocol != extv1alpha1.FilterProtocolHTTP { continue } diff --git a/pkg/gateway/processor/v2/httproute.go b/pkg/gateway/processor/v2/httproute.go index cb0f96f4d..2efff0a84 100644 --- a/pkg/gateway/processor/v2/httproute.go +++ b/pkg/gateway/processor/v2/httproute.go @@ -146,7 +146,12 @@ func (c *ConfigGenerator) toV2HTTPRouteFilters(httpRoute *gwv1.HTTPRoute, routeF case gwv1.HTTPRouteFilterExtensionRef: filter := gwutils.ExtensionRefToFilter(c.client, httpRoute, f.ExtensionRef) - filterProtocol := filter.Spec.Protocol + scope := ptr.Deref(filter.Spec.Scope, extv1alpha1.FilterScopeRoute) + if scope != extv1alpha1.FilterScopeRoute { + continue + } + + filterProtocol := ptr.Deref(filter.Spec.Protocol, extv1alpha1.FilterProtocolHTTP) if filterProtocol != extv1alpha1.FilterProtocolHTTP { continue } diff --git a/pkg/webhook/extension/v1alpha1/filter.go b/pkg/webhook/extension/v1alpha1/filter.go index 479a24c83..9d996594c 100644 --- a/pkg/webhook/extension/v1alpha1/filter.go +++ b/pkg/webhook/extension/v1alpha1/filter.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/flomesh-io/fsm/pkg/utils" @@ -69,11 +71,29 @@ func (r *FilterWebhook) doValidation(ctx context.Context, obj runtime.Object) (w return nil, fmt.Errorf("unexpected type: %T", obj) } + _, errs := r.validateDuplicateFilterType(ctx, filter) + + scope := ptr.Deref(filter.Spec.Scope, extv1alpha1.FilterScopeRoute) + switch scope { + case extv1alpha1.FilterScopeListener: + errs = append(errs, r.validateListenerFilter(ctx, filter)...) + case extv1alpha1.FilterScopeRoute: + errs = append(errs, r.validateRouteFilter(ctx, filter)...) + } + + if len(errs) > 0 { + return nil, utils.ErrorListToError(errs) + } + + return nil, nil +} + +func (r *FilterWebhook) validateDuplicateFilterType(ctx context.Context, filter *extv1alpha1.Filter) (admission.Warnings, field.ErrorList) { var errs field.ErrorList list := &extv1alpha1.FilterList{} if err := r.Manager.GetCache().List(ctx, list, client.InNamespace(filter.Namespace)); err != nil { - return nil, err + return nil, nil } for _, f := range list.Items { @@ -88,9 +108,27 @@ func (r *FilterWebhook) doValidation(ctx context.Context, obj runtime.Object) (w } } - if len(errs) > 0 { - return nil, utils.ErrorListToError(errs) + return nil, errs +} + +func (r *FilterWebhook) validateListenerFilter(_ context.Context, filter *extv1alpha1.Filter) field.ErrorList { + var errs field.ErrorList + + if len(filter.Spec.TargetRefs) == 0 { + path := field.NewPath("spec").Child("targetRefs") + errs = append(errs, field.Required(path, "targetRefs must be specified for filter with listener scope")) } - return nil, nil + return errs +} + +func (r *FilterWebhook) validateRouteFilter(_ context.Context, filter *extv1alpha1.Filter) field.ErrorList { + var errs field.ErrorList + + if len(filter.Spec.TargetRefs) > 0 { + path := field.NewPath("spec").Child("targetRefs") + errs = append(errs, field.Invalid(path, filter.Spec.TargetRefs, "targetRefs must not be specified for filter with route scope")) + } + + return errs } From c09184e15860af017ec71587474e6e0e2cbe9b62 Mon Sep 17 00:00:00 2001 From: Lin Yang Date: Fri, 2 Aug 2024 11:45:33 +0800 Subject: [PATCH 2/4] fix: golang lint Signed-off-by: Lin Yang --- pkg/controllers/extension/v1alpha1/filter_controller.go | 3 ++- pkg/gateway/processor/v2/gateway.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/controllers/extension/v1alpha1/filter_controller.go b/pkg/controllers/extension/v1alpha1/filter_controller.go index 046f10d49..b34d7225c 100644 --- a/pkg/controllers/extension/v1alpha1/filter_controller.go +++ b/pkg/controllers/extension/v1alpha1/filter_controller.go @@ -4,11 +4,12 @@ import ( "context" "fmt" - "github.com/flomesh-io/fsm/pkg/constants" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gwv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/flomesh-io/fsm/pkg/constants" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" diff --git a/pkg/gateway/processor/v2/gateway.go b/pkg/gateway/processor/v2/gateway.go index 8744039c9..578367f63 100644 --- a/pkg/gateway/processor/v2/gateway.go +++ b/pkg/gateway/processor/v2/gateway.go @@ -6,11 +6,12 @@ import ( "k8s.io/utils/ptr" - extv1alpha1 "github.com/flomesh-io/fsm/pkg/apis/extension/v1alpha1" - "github.com/flomesh-io/fsm/pkg/constants" "k8s.io/apimachinery/pkg/fields" "sigs.k8s.io/controller-runtime/pkg/client" + extv1alpha1 "github.com/flomesh-io/fsm/pkg/apis/extension/v1alpha1" + "github.com/flomesh-io/fsm/pkg/constants" + fgwv2 "github.com/flomesh-io/fsm/pkg/gateway/fgw" corev1 "k8s.io/api/core/v1" From 3eb5579d73d8ba64bea9f19964e10ddcd75ff8d5 Mon Sep 17 00:00:00 2001 From: Lin Yang Date: Mon, 5 Aug 2024 11:21:52 +0800 Subject: [PATCH 3/4] refactor: rename filters to routeFilters Signed-off-by: Lin Yang --- pkg/gateway/fgw/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gateway/fgw/config.go b/pkg/gateway/fgw/config.go index 177a16781..d2633ab87 100644 --- a/pkg/gateway/fgw/config.go +++ b/pkg/gateway/fgw/config.go @@ -62,7 +62,7 @@ type Listener struct { Port gwv1.PortNumber `json:"port"` Protocol gwv1.ProtocolType `json:"protocol"` TLS *GatewayTLSConfig `json:"tls,omitempty" copier:"-"` - Filters []ListenerFilter `json:"filters,omitempty" hash:"set" copier:"-"` + Filters []ListenerFilter `json:"routeFilters,omitempty" hash:"set" copier:"-"` } type ListenerFilter struct { From 0f39cebd9d03d3418cb9ec5c5762e6478c24237b Mon Sep 17 00:00:00 2001 From: Lin Yang Date: Mon, 5 Aug 2024 18:02:13 +0800 Subject: [PATCH 4/4] build(deps): bump fgw to latest Signed-off-by: Lin Yang --- charts/fsm/components/scripts.tar.gz | Bin 16964 -> 18810 bytes .../fsm/components/scripts/gateways/config.js | 87 ---------- .../gateways/filters/http/HTTPForward.js | 28 ++++ .../gateways/filters/tcp/ProxyAccept.js | 55 +++++++ .../gateways/filters/tcp/TLSDelegate.js | 50 ++++++ charts/fsm/components/scripts/gateways/log.js | 24 --- .../fsm/components/scripts/gateways/main.js | 92 +++++++++-- .../gateways/modules/backend-selector.js | 38 +---- .../scripts/gateways/modules/backend-tls.js | 7 +- .../scripts/gateways/modules/backend.js | 40 +++++ .../{forward-http.js => balancer-http.js} | 59 ++----- .../{forward-tcp.js => balancer-tcp.js} | 26 +-- .../scripts/gateways/modules/balancer-udp.js | 28 ++++ .../scripts/gateways/modules/forward-udp.js | 37 ----- .../scripts/gateways/modules/health-check.js | 7 +- .../modules/{route-http.js => router-http.js} | 152 +++++++++-------- .../modules/{route-tcp.js => router-tcp.js} | 39 +++-- .../modules/{route-tls.js => router-tls.js} | 69 ++++---- .../modules/{route-udp.js => router-udp.js} | 39 +++-- .../scripts/gateways/modules/terminate-tls.js | 11 +- .../components/scripts/gateways/resources.js | 153 ++++++++++++++++++ .../fsm/components/scripts/gateways/utils.js | 66 ++++++-- 22 files changed, 700 insertions(+), 407 deletions(-) delete mode 100644 charts/fsm/components/scripts/gateways/config.js create mode 100644 charts/fsm/components/scripts/gateways/filters/http/HTTPForward.js create mode 100644 charts/fsm/components/scripts/gateways/filters/tcp/ProxyAccept.js create mode 100644 charts/fsm/components/scripts/gateways/filters/tcp/TLSDelegate.js delete mode 100644 charts/fsm/components/scripts/gateways/log.js create mode 100644 charts/fsm/components/scripts/gateways/modules/backend.js rename charts/fsm/components/scripts/gateways/modules/{forward-http.js => balancer-http.js} (77%) rename charts/fsm/components/scripts/gateways/modules/{forward-tcp.js => balancer-tcp.js} (60%) create mode 100644 charts/fsm/components/scripts/gateways/modules/balancer-udp.js delete mode 100644 charts/fsm/components/scripts/gateways/modules/forward-udp.js rename charts/fsm/components/scripts/gateways/modules/{route-http.js => router-http.js} (77%) rename charts/fsm/components/scripts/gateways/modules/{route-tcp.js => router-tcp.js} (50%) rename charts/fsm/components/scripts/gateways/modules/{route-tls.js => router-tls.js} (72%) rename charts/fsm/components/scripts/gateways/modules/{route-udp.js => router-udp.js} (50%) create mode 100644 charts/fsm/components/scripts/gateways/resources.js diff --git a/charts/fsm/components/scripts.tar.gz b/charts/fsm/components/scripts.tar.gz index e92cabc2458ce7ca919a9c2e47bc782e04c56a64..2d9d6f5706024fa17e6d1d6755b4f8565f0b226d 100644 GIT binary patch literal 18810 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 literal 16964 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 pipy.patch(`filters/http/${name}.js`, content) - ) - Object.entries(config.filters?.tcp || {}).forEach( - ([name, content]) => pipy.patch(`filters/tcp/${name}.js`, content) - ) - } -} - -function loadConfigDir(dirname) { - var list = pipy.list(dirname) - var resources = [] - var secrets = {} - list.forEach(name => { - var filename = os.path.join(dirname, name) - if (isSecret(name)) { - secrets[name] = pipy.load(filename).toString() - log?.(`Loaded resource ${filename}`) - } else if (isJSON(name)) { - resources.push(JSON.decode(pipy.load(filename))) - log?.(`Loaded resource ${filename}`) - } else if (isYAML(name)) { - resources.push(YAML.decode(pipy.load(filename))) - log?.(`Loaded resource ${filename}`) - } - }) - config.resources = resources - config.secrets = secrets -} - -function isJSON(filename) { - return filename.endsWith('.json') -} - -function isYAML(filename) { - return filename.endsWith('.yaml') || filename.endsWith('.yml') -} - -function isSecret(filename) { - return filename.endsWith('.crt') || filename.endsWith('.key') -} - -var config = { - load, - resources: null, - secrets: null, -} - -export default config diff --git a/charts/fsm/components/scripts/gateways/filters/http/HTTPForward.js b/charts/fsm/components/scripts/gateways/filters/http/HTTPForward.js new file mode 100644 index 000000000..05cb62bef --- /dev/null +++ b/charts/fsm/components/scripts/gateways/filters/http/HTTPForward.js @@ -0,0 +1,28 @@ +export default function () { + var $ctx + var $sni + var $session + + var balancers = new algo.Cache( + target => new algo.LoadBalancer([target]) + ) + + return pipeline($=>$ + .onStart(c => { + $ctx = c.parent + $sni = $ctx.originalServerName + $session = balancers.get($ctx.originalTarget).allocate() + }) + .muxHTTP(() => $session).to($=>$ + .pipe(() => $sni ? 'tls' : 'tcp', { + 'tcp': ($=>$.connect(() => $session.target)), + 'tls': ($=>$ + .connectTLS({ sni: () => $sni }).to($=>$ + .connect(() => $session.target) + ) + ), + }) + ) + .onEnd(() => $session.free()) + ) +} diff --git a/charts/fsm/components/scripts/gateways/filters/tcp/ProxyAccept.js b/charts/fsm/components/scripts/gateways/filters/tcp/ProxyAccept.js new file mode 100644 index 000000000..cc3cc8dab --- /dev/null +++ b/charts/fsm/components/scripts/gateways/filters/tcp/ProxyAccept.js @@ -0,0 +1,55 @@ +export default function () { + var $ctx + var $proto + + return pipeline($=>$ + .onStart(c => { $ctx = c }) + .detectProtocol(p => { $proto = p }) + .pipe( + () => { + if ($proto !== undefined) { + return $proto === 'HTTP' ? 'http' : 'socks' + } + }, { + 'http': ($=>$ + .demuxHTTP().to($=>$ + .pipe( + function (evt) { + if (evt instanceof MessageStart) { + return evt.head.method === 'CONNECT' ? 'tunnel' : 'forward' + } + }, { + 'tunnel': ($=>$ + .acceptHTTPTunnel( + function (req) { + $ctx.originalTarget = req.head.path + return new Message({ status: 200 }) + } + ).to($=>$.pipeNext()) + ), + 'forward': ($=>$ + .handleMessageStart( + function (req) { + var url = new URL(req.head.path) + $ctx.originalTarget = `${url.hostname}:${url.port}` + req.head.path = url.path + } + ) + .pipeNext() + ), + } + ) + ) + ), + 'socks': ($=>$ + .acceptSOCKS( + function (req) { + $ctx.originalTarget = `${req.domain || req.ip}:${req.port}` + return true + } + ).to($=>$.pipeNext()) + ) + } + ) + ) +} diff --git a/charts/fsm/components/scripts/gateways/filters/tcp/TLSDelegate.js b/charts/fsm/components/scripts/gateways/filters/tcp/TLSDelegate.js new file mode 100644 index 000000000..113b59b24 --- /dev/null +++ b/charts/fsm/components/scripts/gateways/filters/tcp/TLSDelegate.js @@ -0,0 +1,50 @@ +export default function ({ tlsDelegate }, resources) { + var ca = new crypto.Certificate(resources.secrets[tlsDelegate.certificate['ca.crt']]) + var caKey = new crypto.PrivateKey(resources.secrets[tlsDelegate.certificate['ca.key']]) + var key = new crypto.PrivateKey({ type: 'rsa', bits: 2048 }) + var pkey = new crypto.PublicKey(key) + + var cache = new algo.Cache( + domain => new crypto.Certificate({ + subject: { CN: domain }, + extensions: { subjectAltName: `DNS:${domain}` }, + days: 365, + timeOffset: -3600, + issuer: ca, + privateKey: caKey, + publicKey: pkey, + }), + null, { ttl: 60*60 } + ) + + var $ctx + var $proto + + return pipeline($=>$ + .onStart(c => { $ctx = c }) + .pipe(evt => evt instanceof MessageStart ? 'L7' : 'L4', { + 'L7': ($=>$.pipeNext()), + 'L4': ($=>$ + .detectProtocol(p => { $proto = p }) + .pipe( + () => { + if ($proto !== undefined) { + return ($proto === 'TLS' ? 'tls' : 'tcp') + } + }, { + 'tcp': ($=>$.pipeNext()), + 'tls': ($=>$ + .acceptTLS({ + certificate: s => { + if (!s) return + $ctx.originalServerName = s + return { key, cert: cache.get(s) } + } + }).to($=>$.pipeNext()) + ) + } + ) + ) + }) + ) +} diff --git a/charts/fsm/components/scripts/gateways/log.js b/charts/fsm/components/scripts/gateways/log.js deleted file mode 100644 index 505ca28c8..000000000 --- a/charts/fsm/components/scripts/gateways/log.js +++ /dev/null @@ -1,24 +0,0 @@ -export var log - -var logFunc = function (a, b, c, d, e, f) { - var n = 6 - if (f === undefined) n-- - if (e === undefined) n-- - if (d === undefined) n-- - if (c === undefined) n-- - if (b === undefined) n-- - if (a === undefined) n-- - switch (n) { - case 0: console.log(); break - case 1: console.log(a); break - case 2: console.log(a, b); break - case 3: console.log(a, b, c); break - case 4: console.log(a, b, c, d); break - case 5: console.log(a, b, c, d, e); break - case 6: console.log(a, b, c, d, e, f); break - } -} - -export function logEnable(on) { - log = on ? logFunc : null -} diff --git a/charts/fsm/components/scripts/gateways/main.js b/charts/fsm/components/scripts/gateways/main.js index 4b5260a17..3ceb1ec5c 100755 --- a/charts/fsm/components/scripts/gateways/main.js +++ b/charts/fsm/components/scripts/gateways/main.js @@ -1,8 +1,8 @@ #!/usr/bin/env -S pipy --args import options from './options.js' -import config from './config.js' -import { log, logEnable } from './log.js' +import resources from './resources.js' +import { log, logEnable, makeFilters } from './utils.js' var opts = options(pipy.argv, { defaults: { @@ -16,11 +16,14 @@ var opts = options(pipy.argv, { }) logEnable(opts['--debug']) -config.load(opts['--config']) +resources.init(opts['--config'], onResourceChange) var $ctx -config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { +resources.list('Gateway').forEach(gw => { + var gatewayName = gw.metadata?.name + if (!gatewayName) return + gw.spec.listeners.forEach(l => { var wireProto var routeKind @@ -31,12 +34,12 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { case 'HTTP': wireProto = 'tcp' routeKind = 'HTTPRoute' - routeModuleName = './modules/route-http.js' + routeModuleName = './modules/router-http.js' break case 'HTTPS': wireProto = 'tcp' routeKind = 'HTTPRoute' - routeModuleName = './modules/route-http.js' + routeModuleName = './modules/router-http.js' termTLS = true break case 'TLS': @@ -44,12 +47,12 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { switch (l.tls?.mode) { case 'Terminate': routeKind = 'TCPRoute' - routeModuleName = './modules/route-tcp.js' + routeModuleName = './modules/router-tcp.js' termTLS = true break case 'Passthrough': routeKind = 'TLSRoute' - routeModuleName = './modules/route-tls.js' + routeModuleName = './modules/router-tls.js' break default: throw `Listener: unknown TLS mode '${l.tls?.mode}'` } @@ -57,12 +60,12 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { case 'TCP': wireProto = 'tcp' routeKind = 'TCPRoute' - routeModuleName = './modules/route-tcp.js' + routeModuleName = './modules/router-tcp.js' break case 'UDP': wireProto = 'udp' routeKind = 'UDPRoute' - routeModuleName = './modules/route-udp.js' + routeModuleName = './modules/router-udp.js' break default: throw `Listener: unknown protocol '${l.protocol}'` } @@ -70,9 +73,8 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { var routeKinds = [routeKind] if (routeKind === 'HTTPRoute') routeKinds.push('GRPCRoute') - var routeResources = config.resources.filter( + var routeResources = routeKinds.flatMap(kind => resources.list(kind)).filter( r => { - if (!routeKinds.includes(r.kind)) return false var refs = r.spec?.parentRefs if (refs instanceof Array) { if (refs.some( @@ -89,22 +91,30 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { } ) - var pipelines = [ - pipy.import(routeModuleName).default(config, l, routeResources) - ] + var routerKey = [gatewayName, l.address, l.port, l.protocol] + var pipelines = [pipy.import(routeModuleName).default(routerKey, l, routeResources)] if (termTLS) { pipelines.unshift( - pipy.import('./modules/terminate-tls.js').default(config, l) + pipy.import('./modules/terminate-tls.js').default(l) ) } + if (l.filters) { + pipelines = [ + ...makeFilters(wireProto, l.filters), + ...pipelines, + ] + } + pipy.listen(l.port, wireProto, $=>$ .onStart(i => { $ctx = { inbound: i, + originalTarget: undefined, + originalServerName: undefined, messageCount: 0, - serverName: '', + serverName: undefined, serverCert: null, clientCert: null, backendResource: null, @@ -118,3 +128,51 @@ config.resources.filter(r => r.kind === 'Gateway').forEach(gw => { log?.(`Listening ${l.protocol} on ${l.port}`) }) }) + +var dirtyRouters = {} +var dirtyBackends = [] + +function onResourceChange(newResource, oldResource) { + var res = newResource || oldResource + var kind = res.kind + var name = res.metadata?.name + switch (kind) { + case 'Gateway': + break + case 'HTTPRoute': + case 'TCPRoute': + case 'TLSRoute': + case 'UDPRoute': + addDirtyRouters(res.spec?.parentRefs) + if (oldResource && res !== oldResource) addDirtyRouters(oldResource.spec?.parentRefs) + break + case 'Backend': + if (name) { + dirtyBackends[name] = newResource + } + break + } +} + +function addDirtyRouters(refs) { + if (refs instanceof Array) { + refs.forEach(ref => { + if (!dirtyRouters.some(r => isEqualListenerRef(r, ref))) { + dirtyRouters.push(ref) + } + }) + } +} + +function isEqualRef(a, b) { + if (a.kind !== b.kind) return false + if (a.name !== b.name) return false + return true +} + +function isEqualListenerRef(a, b) { + if (!isEqualRef(a, b)) return false + if (a.port !== b.port) return false + if (a.sectionName !== b.sectionName) return false + return true +} diff --git a/charts/fsm/components/scripts/gateways/modules/backend-selector.js b/charts/fsm/components/scripts/gateways/modules/backend-selector.js index 026d8c99f..d85677985 100644 --- a/charts/fsm/components/scripts/gateways/modules/backend-selector.js +++ b/charts/fsm/components/scripts/gateways/modules/backend-selector.js @@ -1,10 +1,13 @@ +import resources from '../resources.js' +import { makeFilters } from '../utils.js' + var listenerFilterCaches = new algo.Cache( protocol => new algo.Cache( - listener => makeFilters(protocol, listener?.filters) + listener => makeFilters(protocol, listener?.routeFilters) ) ) -export default function (config, protocol, listener, rule, makeForwarder) { +export default function (protocol, listener, rule, makeBalancer) { var ruleFilters = [ ...listenerFilterCaches.get(protocol).get(listener), ...makeFilters(protocol, rule?.filters), @@ -36,7 +39,7 @@ export default function (config, protocol, listener, rule, makeForwarder) { backendRef, backendResource, weight: backendRef?.weight || 1, - pipeline: makeForwarder(backendRef, backendResource, filters) + pipeline: makeBalancer(backendRef, backendResource, filters) } } @@ -44,34 +47,9 @@ export default function (config, protocol, listener, rule, makeForwarder) { if (backendRef) { var kind = backendRef.kind || 'Backend' var name = backendRef.name - return config.resources.find( - r => r.kind === kind && r.metadata.name === name + return resources.list(kind).find( + r => r.metadata.name === name ) } } } - -function makeFilters(protocol, filters) { - if (!filters) return [] - return filters.map( - config => { - var maker = ( - importFilter(`../config/filters/${protocol}/${config.type}.js`) || - importFilter(`../filters/${protocol}/${config.type}.js`) - ) - if (!maker) throw `${protocol} filter not found: ${config.type}` - if (typeof maker !== 'function') throw `filter ${config.type} is not a function` - return maker(config) - } - ) -} - -function importFilter(pathname) { - if (!pipy.load(pathname)) return null - try { - var filter = pipy.import(pathname) - return filter.default - } catch { - return null - } -} diff --git a/charts/fsm/components/scripts/gateways/modules/backend-tls.js b/charts/fsm/components/scripts/gateways/modules/backend-tls.js index 4b8b66d6b..75f56748f 100644 --- a/charts/fsm/components/scripts/gateways/modules/backend-tls.js +++ b/charts/fsm/components/scripts/gateways/modules/backend-tls.js @@ -1,13 +1,14 @@ +import resources from '../resources.js' import { findPolicies } from '../utils.js' -export default function (config, backendRef, backendResource) { - var backendTLSPolicies = findPolicies(config, 'BackendTLSPolicy', backendResource) +export default function (backendRef, backendResource) { + var backendTLSPolicies = findPolicies('BackendTLSPolicy', backendResource) var tlsValidationConfig = backendTLSPolicies.find(r => r.spec.validation)?.spec?.validation return tlsValidationConfig && { sni: tlsValidationConfig.hostname, trusted: tlsValidationConfig.caCertificates.map( c => new crypto.Certificate( - config.secrets[c['ca.crt']] + resources.secrets[c['ca.crt']] ) ) } diff --git a/charts/fsm/components/scripts/gateways/modules/backend.js b/charts/fsm/components/scripts/gateways/modules/backend.js new file mode 100644 index 000000000..b9fcddb23 --- /dev/null +++ b/charts/fsm/components/scripts/gateways/modules/backend.js @@ -0,0 +1,40 @@ +import resources from '../resources.js' + +var cache = new algo.Cache( + backendName => { + var targets = findTargets(backendName) + var balancer = new algo.LoadBalancer( + targets, { + key: t => t.address, + weight: t => t.weight, + } + ) + resources.addUpdater(backendName, () => { + var targets = findTargets(backendName) + balancer.provision(targets) + }) + return { + name: backendName, + concurrency: 0, + targets: {}, + balancer, + } + } +) + +function findTargets(backendName) { + var backendResource = resources.list('Backend').find( + r => r.metadata?.name === backendName + ) + if (!backendResource?.spec?.targets) return [] + return backendResource.spec.targets.map(t => { + var port = t.port || backendRef.port + var address = `${t.address}:${port}` + var weight = t.weight + return { address, weight } + }) +} + +export default function (backendName) { + return cache.get(backendName) +} diff --git a/charts/fsm/components/scripts/gateways/modules/forward-http.js b/charts/fsm/components/scripts/gateways/modules/balancer-http.js similarity index 77% rename from charts/fsm/components/scripts/gateways/modules/forward-http.js rename to charts/fsm/components/scripts/gateways/modules/balancer-http.js index 1f79b7490..59e598f9c 100644 --- a/charts/fsm/components/scripts/gateways/modules/forward-http.js +++ b/charts/fsm/components/scripts/gateways/modules/balancer-http.js @@ -1,64 +1,37 @@ -import makeHealthCheck from './health-check.js' +import makeBackend from './backend.js' import makeBackendTLS from './backend-tls.js' import makeSessionPersistence from './session-persistence.js' -import { stringifyHTTPHeaders, findPolicies } from '../utils.js' -import { log } from '../log.js' - -var backends = {} +import makeHealthCheck from './health-check.js' +import { log, stringifyHTTPHeaders, findPolicies } from '../utils.js' var $ctx var $session -export default function (config, backendRef, backendResource, isHTTP2) { +export default function (backendRef, backendResource, isHTTP2) { var name = backendResource.metadata.name - var hc = makeHealthCheck(config, backendRef, backendResource) - var tls = makeBackendTLS(config, backendRef, backendResource) - - var targets = backendResource.spec.targets.map(t => { - var port = t.port || backendRef.port - var address = `${t.address}:${port}` - var weight = t.weight - return { address, weight } - }) - - var backend = (backends[name] ??= { - name, - concurrency: 0, - targets: Object.fromEntries( - targets.map(({ address, weight }) => [ - address, { - weight, - concurrency: 0, - } - ]) - ) - }) - - var loadBalancer = new algo.LoadBalancer( - targets, { - key: t => t.address, - weight: t => t.weight, - } - ) + var backend = makeBackend(name) + var balancer = backend.balancer + var hc = makeHealthCheck(backendRef, backendResource) + var tls = makeBackendTLS(backendRef, backendResource) - var backendLBPolicies = findPolicies(config, 'BackendLBPolicy', backendResource) + var backendLBPolicies = findPolicies('BackendLBPolicy', backendResource) var sessionPersistenceConfig = backendLBPolicies.find(r => r.spec.sessionPersistence)?.spec?.sessionPersistence var sessionPersistence = sessionPersistenceConfig && makeSessionPersistence(sessionPersistenceConfig) - var retryPolices = findPolicies(config, 'RetryPolicy', backendResource) + var retryPolices = findPolicies('RetryPolicy', backendResource) var retryConfig = retryPolices?.[0]?.spec?.retry if (sessionPersistence) { var restoreSession = sessionPersistence.restore var targetSelector = function (req) { - $session = loadBalancer.allocate( + $session = balancer.allocate( restoreSession(req.head), target => hc.isHealthy(target.address) ) } } else { var targetSelector = function () { - $session = loadBalancer.allocate(null, target => hc.isHealthy(target.address)) + $session = balancer.allocate(null, target => hc.isHealthy(target.address)) } } @@ -187,14 +160,12 @@ export default function (config, backendRef, backendResource, isHTTP2) { function connect($) { $.onStart(() => { - var t = backends[$session.target.address] - if (t) t.concurrency++ - backend.concurrency++ + var t = (backend.targets[$session.target.address] ??= { concurrency: 0 }) + t.concurrency++ }) $.connect(() => $session.target.address) $.onEnd(() => { - var t = backends[$session.target.address] - if (t) t.concurrency-- + backend.targets[$session.target.address].concurrency-- backend.concurrency-- }) } diff --git a/charts/fsm/components/scripts/gateways/modules/forward-tcp.js b/charts/fsm/components/scripts/gateways/modules/balancer-tcp.js similarity index 60% rename from charts/fsm/components/scripts/gateways/modules/forward-tcp.js rename to charts/fsm/components/scripts/gateways/modules/balancer-tcp.js index c94846448..df9d97dc2 100644 --- a/charts/fsm/components/scripts/gateways/modules/forward-tcp.js +++ b/charts/fsm/components/scripts/gateways/modules/balancer-tcp.js @@ -1,32 +1,22 @@ +import makeBackend from './backend.js' import makeBackendTLS from './backend-tls.js' -import { log } from '../log.js' +import { log } from '../utils.js' var $ctx var $selection -export default function (config, backendRef, backendResource) { - var tls = makeBackendTLS(config, backendRef, backendResource) - - var targets = backendResource ? backendResource.spec.targets.map(t => { - var port = t.port || backendRef.port - var address = `${t.address}:${port}` - var weight = t.weight - return { address, weight } - }) : [] - - var loadBalancer = new algo.LoadBalancer( - targets, { - key: t => t.address, - weight: t => t.weight, - } - ) +export default function (backendRef, backendResource) { + var name = backendResource.metadata.name + var backend = makeBackend(name) + var balancer = backend.balancer + var tls = makeBackendTLS(backendRef, backendResource) var isHealthy = (target) => true return pipeline($=>$ .onStart(c => { $ctx = c - $selection = loadBalancer.allocate(null, isHealthy) + $selection = balancer.allocate(null, isHealthy) log?.( `Inb #${$ctx.inbound.id}`, `target ${$selection?.target?.address}` diff --git a/charts/fsm/components/scripts/gateways/modules/balancer-udp.js b/charts/fsm/components/scripts/gateways/modules/balancer-udp.js new file mode 100644 index 000000000..cbf47eab2 --- /dev/null +++ b/charts/fsm/components/scripts/gateways/modules/balancer-udp.js @@ -0,0 +1,28 @@ +import makeBackend from './backend.js' +import { log } from '../utils.js' + +var $ctx +var $selection + +export default function (backendRef, backendResource) { + var name = backendResource.metadata.name + var backend = makeBackend(name) + var balancer = backend.balancer + + var isHealthy = (target) => true + + return pipeline($=>$ + .onStart(c => { + $ctx = c + $selection = balancer.allocate(null, isHealthy) + log?.( + `Inb #${$ctx.inbound.id}`, + `target ${$selection?.target?.address}` + ) + }) + .pipe(() => $selection ? 'pass' : 'deny', { + 'pass': $=>$.connect(() => $selection.target.address, { protocol: 'udp' }).onEnd(() => $selection.free()), + 'deny': $=>$.replaceStreamStart(new StreamEnd), + }) + ) +} diff --git a/charts/fsm/components/scripts/gateways/modules/forward-udp.js b/charts/fsm/components/scripts/gateways/modules/forward-udp.js deleted file mode 100644 index 432fb8eb6..000000000 --- a/charts/fsm/components/scripts/gateways/modules/forward-udp.js +++ /dev/null @@ -1,37 +0,0 @@ -import { log } from '../log.js' - -var $ctx -var $selection - -export default function (config, backendRef, backendResource) { - var targets = backendResource ? backendResource.spec.targets.map(t => { - var port = t.port || backendRef.port - var address = `${t.address}:${port}` - var weight = t.weight - return { address, weight } - }) : [] - - var loadBalancer = new algo.LoadBalancer( - targets, { - key: t => t.address, - weight: t => t.weight, - } - ) - - var isHealthy = (target) => true - - return pipeline($=>$ - .onStart(c => { - $ctx = c - $selection = loadBalancer.allocate(null, isHealthy) - log?.( - `Inb #${$ctx.inbound.id}`, - `target ${$selection?.target?.address}` - ) - }) - .pipe(() => $selection ? 'pass' : 'deny', { - 'pass': $=>$.connect(() => $selection.target.address, { protocol: 'udp' }).onEnd(() => $selection.free()), - 'deny': $=>$.replaceStreamStart(new StreamEnd), - }) - ) -} diff --git a/charts/fsm/components/scripts/gateways/modules/health-check.js b/charts/fsm/components/scripts/gateways/modules/health-check.js index e09f15b65..ebfb46f4f 100644 --- a/charts/fsm/components/scripts/gateways/modules/health-check.js +++ b/charts/fsm/components/scripts/gateways/modules/health-check.js @@ -1,8 +1,7 @@ -import { findPolicies } from '../utils.js' -import { log } from '../log.js' +import { log, findPolicies } from '../utils.js' -export default function (config, backendRef, backendResource) { - var healthCheckPolicies = findPolicies(config, 'HealthCheckPolicy', backendResource) +export default function (backendRef, backendResource) { + var healthCheckPolicies = findPolicies('HealthCheckPolicy', backendResource) if (healthCheckPolicies.length === 0) { return { isHealthy: () => true } } diff --git a/charts/fsm/components/scripts/gateways/modules/route-http.js b/charts/fsm/components/scripts/gateways/modules/router-http.js similarity index 77% rename from charts/fsm/components/scripts/gateways/modules/route-http.js rename to charts/fsm/components/scripts/gateways/modules/router-http.js index 95612e933..27b45a3ef 100644 --- a/charts/fsm/components/scripts/gateways/modules/route-http.js +++ b/charts/fsm/components/scripts/gateways/modules/router-http.js @@ -1,8 +1,11 @@ +import resources from '../resources.js' import makeBackendSelector from './backend-selector.js' -import makeForwarder from './forward-http.js' +import makeBalancer from './balancer-http.js' import makeSessionPersistence from './session-persistence.js' -import { stringifyHTTPHeaders } from '../utils.js' -import { log } from '../log.js' +import { log, stringifyHTTPHeaders } from '../utils.js' + +var response404 = pipeline($=>$.replaceMessage(new Message({ status: 404 }))) +var response500 = pipeline($=>$.replaceMessage(new Message({ status: 500 }))) var $ctx var $hostname @@ -11,10 +14,77 @@ var $matchedRoute var $matchedRule var $selection -export default function (config, listener, routeResources) { - var response404 = pipeline($=>$.replaceMessage(new Message({ status: 404 }))) - var response500 = pipeline($=>$.replaceMessage(new Message({ status: 500 }))) +export default function (routerKey, listener, routeResources) { + var router = makeRouter(listener, routeResources) + + resources.addUpdater(routerKey, (listener, routeResources) => { + router = makeRouter(listener, routeResources) + }) + + var handleRequest = pipeline($=>$ + .handleMessageStart( + function (msg) { + router(msg) + $ctx = { + parent: $ctx, + id: ++$ctx.messageCount, + host: $hostname, + path: msg.head.path, + head: msg.head, + headTime: Date.now(), + tail: null, + tailTime: 0, + sendTime: 0, + response: { + head: null, + headTime: 0, + tail: null, + tailTime: 0, + }, + basePath: $basePath, + routeResource: $matchedRoute, + routeRule: $matchedRule, + backendResource: $selection?.target?.backendResource, + backend: null, + target: '', + retries: [], + } + } + ) + .handleMessageEnd( + function (msg) { + $ctx.tail = msg.tail + $ctx.tailTime = Date.now() + } + ) + .pipe(() => $selection ? $selection.target.pipeline : response404) + .handleMessageStart( + function (msg) { + var r = $ctx.response + r.head = msg.head + r.headTime = Date.now() + } + ) + .handleMessageEnd( + function (msg) { + var r = $ctx.response + r.tail = msg.tail + r.tailTime = Date.now() + } + ) + ) + + var handleStream = pipeline($=>$ + .demuxHTTP().to(handleRequest) + ) + return pipeline($=>$ + .onStart(c => void ($ctx = c)) + .pipe(evt => evt instanceof MessageStart ? handleRequest : handleStream) + ) +} + +function makeRouter(listener, routeResources) { var hostFullnames = {} var hostPostfixes = {} @@ -40,7 +110,7 @@ export default function (config, listener, routeResources) { .sort((a, b) => b[0].length - a[0].length) .map(([k, v]) => [k, makeRuleSelector(v)]) - function route(msg) { + return function (msg) { var head = msg.head var host = head.headers.host if (host) { @@ -111,7 +181,7 @@ export default function (config, listener, routeResources) { switch (kind) { case 'HTTPRoute': - matches = matches.map(([m, backendSelector, resource, rule], i) => { + matches = matches.map(([m, backendSelector, resource, rule]) => { var matchMethod = makeMethodMatcher(m.method) var matchPath = makePathMatcher(m.path) var matchHeaders = makeObjectMatcher(m.headers) @@ -221,14 +291,14 @@ export default function (config, listener, routeResources) { var sessionPersistenceConfig = rule.sessionPersistence var sessionPersistence = sessionPersistenceConfig && makeSessionPersistence(sessionPersistenceConfig) var selector = makeBackendSelector( - config, 'http', listener, rule, + 'http', listener, rule, function (backendRef, backendResource, filters) { - if (!backendResource) return response500 - var forwarder = makeForwarder(config, backendRef, backendResource, isHTTP2) + if (!backendResource && filters.length === 0) return response500 + var forwarder = backendResource ? [makeBalancer(backendRef, backendResource, isHTTP2)] : [] if (sessionPersistence) { var preserveSession = sessionPersistence.preserve return pipeline($=>$ - .pipe([...filters, forwarder], () => $ctx) + .pipe([...filters, ...forwarder], () => $ctx) .handleMessageStart( msg => preserveSession(msg.head, $selection?.target?.backendRef?.name) ) @@ -236,7 +306,7 @@ export default function (config, listener, routeResources) { ) } else { return pipeline($=>$ - .pipe([...filters, forwarder], () => $ctx) + .pipe([...filters, ...forwarder], () => $ctx) .onEnd(() => $selection.free?.()) ) } @@ -248,60 +318,4 @@ export default function (config, listener, routeResources) { } return () => selector() } - - return pipeline($=>$ - .onStart(c => void ($ctx = c)) - .demuxHTTP().to($=>$ - .handleMessageStart( - function (msg) { - route(msg) - $ctx = { - parent: $ctx, - id: ++$ctx.messageCount, - host: $hostname, - path: msg.head.path, - head: msg.head, - headTime: Date.now(), - tail: null, - tailTime: 0, - sendTime: 0, - response: { - head: null, - headTime: 0, - tail: null, - tailTime: 0, - }, - basePath: $basePath, - routeResource: $matchedRoute, - routeRule: $matchedRule, - backendResource: $selection?.target?.backendResource, - backend: null, - target: '', - retries: [], - } - } - ) - .handleMessageEnd( - function (msg) { - $ctx.tail = msg.tail - $ctx.tailTime = Date.now() - } - ) - .pipe(() => $selection ? $selection.target.pipeline : response404) - .handleMessageStart( - function (msg) { - var r = $ctx.response - r.head = msg.head - r.headTime = Date.now() - } - ) - .handleMessageEnd( - function (msg) { - var r = $ctx.response - r.tail = msg.tail - r.tailTime = Date.now() - } - ) - ) - ) } diff --git a/charts/fsm/components/scripts/gateways/modules/route-tcp.js b/charts/fsm/components/scripts/gateways/modules/router-tcp.js similarity index 50% rename from charts/fsm/components/scripts/gateways/modules/route-tcp.js rename to charts/fsm/components/scripts/gateways/modules/router-tcp.js index aa22a8c65..cf1d6323a 100644 --- a/charts/fsm/components/scripts/gateways/modules/route-tcp.js +++ b/charts/fsm/components/scripts/gateways/modules/router-tcp.js @@ -1,18 +1,35 @@ +import resources from '../resources.js' import makeBackendSelector from './backend-selector.js' -import makeForwarder from './forward-tcp.js' -import { log } from '../log.js' +import makeBalancer from './balancer-tcp.js' +import { log } from '../utils.js' + +var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) var $ctx var $selection -export default function (config, listener, routeResources) { - var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) +export default function (routerKey, listener, routeResources) { + var router = makeRouter(listener, routeResources) + + resources.addUpdater(routerKey, (listener, routeResources) => { + router = makeRouter(listener, routeResources) + }) + return pipeline($=>$ + .onStart(c => { + $ctx = c + router() + }) + .pipe(() => $selection ? $selection.target.pipeline : shutdown) + ) +} + +function makeRouter(listener, routeResources) { var selector = makeBackendSelector( - config, 'tcp', listener, + 'tcp', listener, routeResources[0]?.spec?.rules?.[0], function (backendRef, backendResource, filters) { - var forwarder = backendResource ? makeForwarder(config, backendRef, backendResource) : shutdown + var forwarder = backendResource ? makeBalancer(backendRef, backendResource) : shutdown return pipeline($=>$ .pipe([...filters, forwarder], () => $ctx) .onEnd(() => $selection.free?.()) @@ -20,19 +37,11 @@ export default function (config, listener, routeResources) { } ) - function route() { + return function () { $selection = selector() log?.( `Inb #${$ctx.inbound.id}`, `backend ${$selection?.target?.backendRef?.name}` ) } - - return pipeline($=>$ - .onStart(c => { - $ctx = c - route() - }) - .pipe(() => $selection ? $selection.target.pipeline : shutdown) - ) } diff --git a/charts/fsm/components/scripts/gateways/modules/route-tls.js b/charts/fsm/components/scripts/gateways/modules/router-tls.js similarity index 72% rename from charts/fsm/components/scripts/gateways/modules/route-tls.js rename to charts/fsm/components/scripts/gateways/modules/router-tls.js index 4b8e9cd21..7f90f4fd8 100644 --- a/charts/fsm/components/scripts/gateways/modules/route-tls.js +++ b/charts/fsm/components/scripts/gateways/modules/router-tls.js @@ -1,14 +1,46 @@ +import resources from '../resources.js' import makeBackendSelector from './backend-selector.js' -import makeForwarder from './forward-tcp.js' -import { log } from '../log.js' +import makeBalancer from './balancer-tcp.js' +import { log } from '../utils.js' + +var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) var $ctx var $proto var $selection -export default function (config, listener, routeResources) { - var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) +export default function (routerKey, listener, routeResources) { + var router = makeRouter(listener, routeResources) + + resources.addUpdater(routerKey, (listener, routeResources) => { + router = makeRouter(listener, routeResources) + }) + + return pipeline($=>$ + .onStart(c => void ($ctx = c)) + .detectProtocol(proto => void ($proto = proto)) + .pipe( + () => { + if ($proto !== undefined) { + log?.(`Inb #${$ctx.inbound.id} protocol ${$proto || 'unknown'}`) + return $proto === 'TLS' ? 'pass' : 'deny' + } + }, { + 'pass': ($=>$ + .handleTLSClientHello(router) + .pipe(() => { + if ($selection !== undefined) { + return $selection ? $selection.target.pipeline : shutdown + } + }) + ), + 'deny': $=>$.replaceStreamStart(new StreamEnd), + } + ) + ) +} +function makeRouter(listener, routeResources) { var hostFullnames = {} var hostPostfixes = [] @@ -16,9 +48,9 @@ export default function (config, listener, routeResources) { var hostnames = r.spec.hostnames || ['*'] hostnames.forEach(name => { var selector = makeBackendSelector( - config, 'tcp', listener, r.spec.rules?.[0], + 'tcp', listener, r.spec.rules?.[0], function (backendRef, backendResource, filters) { - var forwarder = backendResource ? makeForwarder(config, backendRef, backendResource) : shutdown + var forwarder = backendResource ? makeBalancer(backendRef, backendResource) : shutdown return pipeline($=>$ .pipe([...filters, forwarder], () => $ctx) .onEnd(() => $selection.free?.()) @@ -36,7 +68,7 @@ export default function (config, listener, routeResources) { hostPostfixes.sort((a, b) => b[0].length - a[0].length) - function route(hello) { + return function (hello) { var sni = hello.serverNames[0] || '' var name = sni.toLowerCase() var selector = hostFullnames[name] || ( @@ -51,27 +83,4 @@ export default function (config, listener, routeResources) { `backend ${$selection?.target?.backendRef?.name}` ) } - - return pipeline($=>$ - .onStart(c => void ($ctx = c)) - .detectProtocol(proto => void ($proto = proto)) - .pipe( - () => { - if ($proto !== undefined) { - log?.(`Inb #${$ctx.inbound.id} protocol ${$proto || 'unknown'}`) - return $proto === 'TLS' ? 'pass' : 'deny' - } - }, { - 'pass': ($=>$ - .handleTLSClientHello(route) - .pipe(() => { - if ($selection !== undefined) { - return $selection ? $selection.target.pipeline : shutdown - } - }) - ), - 'deny': $=>$.replaceStreamStart(new StreamEnd), - } - ) - ) } diff --git a/charts/fsm/components/scripts/gateways/modules/route-udp.js b/charts/fsm/components/scripts/gateways/modules/router-udp.js similarity index 50% rename from charts/fsm/components/scripts/gateways/modules/route-udp.js rename to charts/fsm/components/scripts/gateways/modules/router-udp.js index 63a0389f0..927274230 100644 --- a/charts/fsm/components/scripts/gateways/modules/route-udp.js +++ b/charts/fsm/components/scripts/gateways/modules/router-udp.js @@ -1,18 +1,35 @@ +import resources from '../resources.js' import makeBackendSelector from './backend-selector.js' -import makeForwarder from './forward-udp.js' -import { log } from '../log.js' +import makeBalancer from './balancer-udp.js' +import { log } from '../utils.js' + +var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) var $ctx var $selection -export default function (config, listener, routeResources) { - var shutdown = pipeline($=>$.replaceStreamStart(new StreamEnd)) +export default function (routerKey, listener, routeResources) { + var router = makeRouter(listener, routeResources) + + resources.addUpdater(routerKey, (listener, routeResources) => { + router = makeRouter(listener, routeResources) + }) + return pipeline($=>$ + .onStart(c => { + $ctx = c + router() + }) + .pipe(() => $selection ? $selection.target.pipeline : shutdown) + ) +} + +function makeRouter(listener, routeResources) { var selector = makeBackendSelector( - config, 'udp', listener, + 'udp', listener, routeResources[0]?.spec?.rules?.[0], function (backendRef, backendResource, filters) { - var forwarder = backendResource ? makeForwarder(config, backendRef, backendResource) : shutdown + var forwarder = backendResource ? makeBalancer(backendRef, backendResource) : shutdown return pipeline($=>$ .pipe([...filters, forwarder], () => $ctx) .onEnd(() => $selection.free?.()) @@ -20,19 +37,11 @@ export default function (config, listener, routeResources) { } ) - function route() { + return function () { $selection = selector() log?.( `Inb #${$ctx.inbound.id}`, `backend ${$selection?.target?.backendRef?.name}` ) } - - return pipeline($=>$ - .onStart(c => { - $ctx = c - route() - }) - .pipe(() => $selection ? $selection.target.pipeline : shutdown) - ) } diff --git a/charts/fsm/components/scripts/gateways/modules/terminate-tls.js b/charts/fsm/components/scripts/gateways/modules/terminate-tls.js index 6f5fc36e9..a32689d07 100644 --- a/charts/fsm/components/scripts/gateways/modules/terminate-tls.js +++ b/charts/fsm/components/scripts/gateways/modules/terminate-tls.js @@ -1,16 +1,17 @@ -import { log } from '../log.js' +import resources from '../resources.js' +import { log } from '../utils.js' var $ctx var $proto var $hello = false -export default function (config, listener) { +export default function (listener) { var fullnames = {} var postfixes = [] listener.tls?.certificates?.forEach?.(c => { - var crtFile = config.secrets[c['tls.crt']] - var keyFile = config.secrets[c['tls.key']] + var crtFile = resources.secrets[c['tls.crt']] + var keyFile = resources.secrets[c['tls.key']] var crt = new crypto.Certificate(crtFile) var key = new crypto.PrivateKey(keyFile) var certificate = { cert: crt, key } @@ -30,7 +31,7 @@ export default function (config, listener) { if (frontendValidation) { trusted = [] frontendValidation.caCertificates?.forEach?.(c => { - var crtFile = config.secrets[c['ca.crt']] + var crtFile = resources.secrets[c['ca.crt']] var crt = new crypto.Certificate(crtFile) trusted.push(crt) }) diff --git a/charts/fsm/components/scripts/gateways/resources.js b/charts/fsm/components/scripts/gateways/resources.js new file mode 100644 index 000000000..324f6b3b2 --- /dev/null +++ b/charts/fsm/components/scripts/gateways/resources.js @@ -0,0 +1,153 @@ +import { log } from './utils.js' + +var DEFAULT_CONFIG_PATH = '/etc/fgw' + +var files = {} +var kinds = {} +var secrets = {} + +var notifyCreate = () => {} +var notifyDelete = () => {} +var notifyUpdate = () => {} + +function init(pathname, onChange) { + var configFile = pipy.load('/config.json') || pipy.load('/config.yaml') + var configDir = pipy.list('/config') + var hasBuiltinConfig = (configDir.length > 0 || Boolean(configFile)) + + if (pathname || !hasBuiltinConfig) { + pathname = os.path.resolve(pathname || DEFAULT_CONFIG_PATH) + var s = os.stat(pathname) + if (!s) { + throw `configuration file or directory does not exist: ${pathname}` + } + if (s.isDirectory()) { + pipy.mount('config', pathname) + configFile = null + } else if (s.isFile()) { + configFile = os.read(pathname) + } + } + + if (configFile) { + var config + try { + try { + config = JSON.decode(configFile) + } catch { + config = YAML.decode(configFile) + } + } catch { + throw 'cannot parse configuration file as JSON or YAML' + } + + config.resources.forEach(r => { + if (r.kind) { + list(r.kind).push(r) + } + }) + + Object.entries(config.secrets || {}).forEach(([k, v]) => secrets[k] = v) + + } else { + notifyCreate = function (resource) { onChange(resource, null) } + notifyDelete = function (resource) { onChange(null, resource) } + notifyUpdate = function (resource, old) { onChange(resource, old) } + + pipy.list('/config').forEach( + pathname => addFile(pathname) + ) + } +} + +function list(kind) { + return (kinds[kind] ??= []) +} + +function readFile(pathname) { + try { + if (isJSON(pathname)) { + return JSON.decode(pipy.load(pathname)) + } else if (isYAML(pathname)) { + return YAML.decode(pipy.load(pathname)) + } else if (isSecret(pathname)) { + var name = os.path.basename(pathname) + secrets[name] = pipy.load(pathname) + } + } catch { + console.error(`Cannot load or parse file: ${pathname}, skpped.`) + } +} + +function addFile(pathname) { + var data = readFile(pathname) + if (data && data.kind && data.spec) { + log?.(`Load resource file: ${pathname}`) + files[pathname] = data + var resources = list(data.kind) + var name = data.metadata?.name + if (name) { + var i = resources.find(r => r.metadata?.name === name) + if (i >= 0) { + var old = resources[i] + resources[i] = data + notifyUpdate(data, old) + } else { + resources.push(data) + notifyCreate(data) + } + } else { + resources.push(data) + notifyCreate(data) + } + } +} + +function isJSON(filename) { + return filename.endsWith('.json') +} + +function isYAML(filename) { + return filename.endsWith('.yaml') || filename.endsWith('.yml') +} + +function isSecret(filename) { + return filename.endsWith('.crt') || filename.endsWith('.key') +} + +var updaterLists = [] + +function findUpdaterKey(key) { + if (key instanceof Array) { + updaterLists.findIndex(([k]) => ( + k.length === key.length && + !k.some((v, i) => (v !== key[i])) + )) + } else { + updaterLists.findIndex(([k]) => (k === key)) + } +} + +function addUpdater(key, updater) { + var i = findUpdaterKey(key) + if (i >= 0) { + updaterLists[i][1].push(updater) + } else { + if (key instanceof Array) key = [...key] + updaterLists.push([key, [updater]]) + } +} + +function getUpdaters(key) { + var i = findUpdaterKey(key) + if (i >= 0) return updaterLists[i][1] + return [] +} + +export default { + init, + list, + secrets, + addUpdater, + getUpdaters, +} diff --git a/charts/fsm/components/scripts/gateways/utils.js b/charts/fsm/components/scripts/gateways/utils.js index 0cf0bb256..70c3517e5 100644 --- a/charts/fsm/components/scripts/gateways/utils.js +++ b/charts/fsm/components/scripts/gateways/utils.js @@ -1,3 +1,30 @@ +import resources from './resources.js' + +export var log + +var logFunc = function (a, b, c, d, e, f) { + var n = 6 + if (f === undefined) n-- + if (e === undefined) n-- + if (d === undefined) n-- + if (c === undefined) n-- + if (b === undefined) n-- + if (a === undefined) n-- + switch (n) { + case 0: console.log(); break + case 1: console.log(a); break + case 2: console.log(a, b); break + case 3: console.log(a, b, c); break + case 4: console.log(a, b, c, d); break + case 5: console.log(a, b, c, d, e); break + case 6: console.log(a, b, c, d, e, f); break + } +} + +export function logEnable(on) { + log = on ? logFunc : null +} + export function stringifyHTTPHeaders(headers) { return Object.entries(headers).flatMap( ([k, v]) => { @@ -10,16 +37,37 @@ export function stringifyHTTPHeaders(headers) { ).join(' ') } -export function findPolicies(config, kind, targetResource) { - return config.resources.filter( - r => { - if (r.kind !== kind) return false - return r.spec.targetRefs.some( - ref => ( - ref.kind === targetResource.kind && - ref.name === targetResource.metadata.name - ) +export function findPolicies(kind, targetResource) { + return resources.list(kind).filter( + r => r.spec.targetRefs.some( + ref => ( + ref.kind === targetResource.kind && + ref.name === targetResource.metadata.name ) + ) + ) +} +export function makeFilters(protocol, filters) { + if (!filters) return [] + return filters.map( + config => { + var maker = ( + importFilter(`./config/filters/${protocol}/${config.type}.js`) || + importFilter(`./filters/${protocol}/${config.type}.js`) + ) + if (!maker) throw `${protocol} filter not found: ${config.type}` + if (typeof maker !== 'function') throw `filter ${config.type} is not a function` + return maker(config, resources) } ) } + +function importFilter(pathname) { + if (!pipy.load(pathname)) return null + try { + var filter = pipy.import(pathname) + return filter.default + } catch { + return null + } +}