diff --git a/api/val_types.go b/api/val_types.go index 9bc4352..0023cbd 100644 --- a/api/val_types.go +++ b/api/val_types.go @@ -337,6 +337,21 @@ func (val *Val) IsArrayString() bool { return val.Type == ArrayString } +func (val *Val) IsEmpty() bool { + switch val.Type { + case Unknown, Nil: + return true + case String: + return val.String() == "" + case ArrayNumber: + return len(val.arrayNumVal) == 0 + case ArrayString: + return len(val.arrayStrVal) == 0 + default: + return false + } +} + // UnmarshalJSON implements the json.Unmarshaller interface. func (val *Val) UnmarshalJSON(value []byte) error { defErr := errors.New("value must be type boolean, number, string, []number, or []string; nested objects are not supported") diff --git a/components/broker/engine/broker.go b/components/broker/engine/broker.go index e819761..6e9d1f9 100644 --- a/components/broker/engine/broker.go +++ b/components/broker/engine/broker.go @@ -212,7 +212,7 @@ func (brk *broker) AuthorizeComponent(ctx context.Context, comp *core.Component, } } - // TODO is this safe? + // TODO use platform status to check if component is platform component. platformCompName := fmt.Sprintf("%s-%s", config.Platform, comp.Name) if !strings.HasSuffix(svcAccName, comp.GroupKey()) && svcAccName != platformCompName { return fmt.Errorf("service account name does not match component") @@ -233,6 +233,8 @@ func (brk *broker) AuthorizeComponent(ctx context.Context, comp *core.Component, return fmt.Errorf("unauthorized component: %s", review.Status.Error) } + // TODO make sure component exists in an AppDeployment + return nil } diff --git a/examples/hello-world/kubefox/components/backend/main.go b/examples/hello-world/kubefox/components/backend/main.go index f1ad9ef..f636e6f 100644 --- a/examples/hello-world/kubefox/components/backend/main.go +++ b/examples/hello-world/kubefox/components/backend/main.go @@ -6,7 +6,7 @@ import ( ) var ( - who kit.EnvVar + who kit.EnvVarDep ) func main() { diff --git a/examples/hello-world/kubefox/components/frontend/main.go b/examples/hello-world/kubefox/components/frontend/main.go index e9ca8e7..df87c25 100644 --- a/examples/hello-world/kubefox/components/frontend/main.go +++ b/examples/hello-world/kubefox/components/frontend/main.go @@ -8,7 +8,7 @@ import ( ) var ( - backend kit.Dependency + backend kit.ComponentDep ) func main() { diff --git a/examples/hello-world/kubefox/go.mod b/examples/hello-world/kubefox/go.mod index 63d6574..ea9762f 100644 --- a/examples/hello-world/kubefox/go.mod +++ b/examples/hello-world/kubefox/go.mod @@ -3,7 +3,7 @@ module github.com/xigxog/kubefox/examples/hello-world/kubefox go 1.21 // TODO update when kubefox is released -require github.com/xigxog/kubefox v0.2.5-alpha.0.20231206144113-48f17b47cb37 +require github.com/xigxog/kubefox v0.2.5-alpha.0.20231207161131-766472f99055 require ( github.com/golang/protobuf v1.5.3 // indirect diff --git a/examples/hello-world/kubefox/go.sum b/examples/hello-world/kubefox/go.sum index b7e28fc..947073d 100644 --- a/examples/hello-world/kubefox/go.sum +++ b/examples/hello-world/kubefox/go.sum @@ -14,6 +14,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xigxog/kubefox v0.2.5-alpha.0.20231206144113-48f17b47cb37 h1:jgXm3ga62ZnY8oejlI173Z7TfTBtXeOZbJQpArrXiw4= github.com/xigxog/kubefox v0.2.5-alpha.0.20231206144113-48f17b47cb37/go.mod h1:9qjm+/xmMZUbXct3cf37+rOsEwapMtLORwZWExzZgT0= +github.com/xigxog/kubefox v0.2.5-alpha.0.20231207161131-766472f99055 h1:xZK7obOPCJcH7PrAJUE5f4SxRBka/ujvGJYtpXTCYIM= +github.com/xigxog/kubefox v0.2.5-alpha.0.20231207161131-766472f99055/go.mod h1:9qjm+/xmMZUbXct3cf37+rOsEwapMtLORwZWExzZgT0= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/kit/graphql/graphql.go b/kit/graphql/graphql.go index 58d7326..321c29b 100644 --- a/kit/graphql/graphql.go +++ b/kit/graphql/graphql.go @@ -13,7 +13,7 @@ type Client struct { wrapped *graphql.Client } -func New(ktx kit.Kontext, dependency kit.Dependency) *Client { +func New(ktx kit.Kontext, dependency kit.ComponentDep) *Client { return &Client{ ktx: ktx, wrapped: graphql.NewClient("", &http.Client{ diff --git a/kit/kit.go b/kit/kit.go index ccca0c4..546dbbf 100644 --- a/kit/kit.go +++ b/kit/kit.go @@ -23,6 +23,13 @@ const ( maxAttempts = 5 ) +// TODO also support declarative routes? Example: +// +// kit.RouteBuilder(). +// Header("host", "google.com"). +// Query("param1", "fish"). +// Handler(myHandler) + type kit struct { spec *api.ComponentDetails @@ -112,6 +119,10 @@ func (svc *kit) Log() *logkf.Logger { return svc.log } +func (svc *kit) L() *logkf.Logger { + return svc.log +} + func (svc *kit) Title(title string) { svc.spec.Title = title } @@ -120,12 +131,6 @@ func (svc *kit) Description(description string) { svc.spec.Title = description } -// TODO also support declarative routes? Example: -// -// kit.RouteBuilder(). -// Header("host", "google.com"). -// Query("param1", "fish"). -// Handler(myHandler) func (svc *kit) Route(rule string, handler EventHandler) { r := &route{ RouteSpec: api.RouteSpec{ @@ -143,7 +148,7 @@ func (svc *kit) Default(handler EventHandler) { svc.spec.DefaultHandler = handler != nil } -func (svc *kit) EnvVar(name string, opts ...env.VarOption) EnvVar { +func (svc *kit) EnvVar(name string, opts ...env.VarOption) EnvVarDep { if name == "" { svc.log.Fatal("environment variable name is required") } @@ -160,15 +165,15 @@ func (svc *kit) EnvVar(name string, opts ...env.VarOption) EnvVar { return env.NewVar(name, schema.Type) } -func (svc *kit) Component(name string) Dependency { +func (svc *kit) Component(name string) ComponentDep { return svc.dependency(name, api.ComponentTypeKubeFox) } -func (svc *kit) HTTPAdapter(name string) Dependency { +func (svc *kit) HTTPAdapter(name string) ComponentDep { return svc.dependency(name, api.ComponentTypeHTTP) } -func (svc *kit) dependency(name string, typ api.ComponentType) Dependency { +func (svc *kit) dependency(name string, typ api.ComponentType) ComponentDep { c := &dependency{ typ: typ, name: name, @@ -263,7 +268,7 @@ func (svc *kit) recvReq(req *grpc.ComponentEvent) { log.Error(err) errEvt := core.NewErr(err, core.EventOpts{}) - if err := ktx.ForwardResp(errEvt).Send(); err != nil { + if err := ktx.Resp().Forward(errEvt); err != nil { log.Error(err) } } diff --git a/kit/kontext.go b/kit/kontext.go index a987792..9ea1955 100644 --- a/kit/kontext.go +++ b/kit/kontext.go @@ -46,16 +46,16 @@ func (k *kontext) Log() *logkf.Logger { return k.log } -func (k *kontext) Env(v EnvVar) string { +func (k *kontext) Env(v EnvVarDep) string { return k.EnvV(v).String() } -func (k *kontext) EnvV(v EnvVar) *api.Val { +func (k *kontext) EnvV(v EnvVarDep) *api.Val { val, _ := api.ValProto(k.env[v.Name()]) return val } -func (k *kontext) EnvDef(v EnvVar, def string) string { +func (k *kontext) EnvDef(v EnvVarDep, def string) string { if val := k.Env(v); val == "" { return def } else { @@ -63,25 +63,14 @@ func (k *kontext) EnvDef(v EnvVar, def string) string { } } -func (k *kontext) EnvDefV(v EnvVar, def *api.Val) *api.Val { - if val := k.EnvV(v); val == nil { +func (k *kontext) EnvDefV(v EnvVarDep, def *api.Val) *api.Val { + if val := k.EnvV(v); val.IsEmpty() { return def } else { return val } } -func (k *kontext) ForwardResp(resp core.EventReader) Resp { - return &respKontext{ - Event: core.CloneToResp(resp.(*core.Event), core.EventOpts{ - Parent: k.Event, - Source: k.kit.brk.Component, - Target: k.Event.Source, - }), - ktx: k, - } -} - func (k *kontext) Resp() Resp { return &respKontext{ Event: core.NewResp(core.EventOpts{ @@ -93,7 +82,7 @@ func (k *kontext) Resp() Resp { } } -func (k *kontext) Req(c Dependency) Req { +func (k *kontext) Req(c ComponentDep) Req { return &reqKontext{ Event: core.NewReq(core.EventOpts{ Type: c.EventType(), @@ -105,7 +94,7 @@ func (k *kontext) Req(c Dependency) Req { } } -func (k *kontext) Forward(c Dependency) Req { +func (k *kontext) Forward(c ComponentDep) Req { return &reqKontext{ Event: core.CloneToReq(k.Event, core.EventOpts{ Type: c.EventType(), @@ -117,13 +106,13 @@ func (k *kontext) Forward(c Dependency) Req { } } -func (k *kontext) HTTP(c Dependency) *http.Client { +func (k *kontext) HTTP(c ComponentDep) *http.Client { return &http.Client{ Transport: k.Transport(c), } } -func (k *kontext) Transport(c Dependency) http.RoundTripper { +func (k *kontext) Transport(c ComponentDep) http.RoundTripper { return &EventRoundTripper{ req: &reqKontext{ Event: core.NewReq(core.EventOpts{ @@ -137,9 +126,19 @@ func (k *kontext) Transport(c Dependency) http.RoundTripper { } } -func (resp *respKontext) SendStr(val string) error { +func (resp *respKontext) Forward(evt core.EventReader) error { + resp.Event = core.CloneToResp(evt.(*core.Event), core.EventOpts{ + Parent: resp.ktx.Event, + Source: resp.ktx.kit.brk.Component, + Target: resp.ktx.Event.Source, + }) + + return resp.Send() +} + +func (resp *respKontext) SendStr(val ...string) error { c := fmt.Sprintf("%s; %s", api.ContentTypePlain, api.CharSetUTF8) - return resp.SendBytes(c, []byte(val)) + return resp.SendBytes(c, []byte(strings.Join(val, ""))) } func (resp *respKontext) SendHTML(val string) error { @@ -170,6 +169,10 @@ func (resp *respKontext) SendAccepts(json any, html, str string) error { } func (resp *respKontext) SendReader(contentType string, reader io.Reader) error { + if closer, ok := reader.(io.ReadCloser); ok { + defer closer.Close() + } + bytes, err := io.ReadAll(reader) if err != nil { return err diff --git a/kit/types.go b/kit/types.go index 3dc3e2f..f47a529 100644 --- a/kit/types.go +++ b/kit/types.go @@ -11,42 +11,176 @@ import ( "github.com/xigxog/kubefox/logkf" ) -type EventHandler func(kit Kontext) error +type EventHandler func(ktx Kontext) error type Kit interface { + // Start connects to the Broker passing the Component's Service Account + // Token for authorization. Once connected Kit will accept incoming Events. + // If an error occurs the program will exit with a status code of 1. Start + // is a blocking call. Start() - Route(string, EventHandler) - Default(EventHandler) - - EnvVar(name string, opts ...env.VarOption) EnvVar - - Component(name string) Dependency - HTTPAdapter(name string) Dependency - + // Route registers a EventHandler for the specified rule. If an Event + // matches the rule the Broker will route it to the Component and Kit will + // call the EventHandler. + // + // Rules are written in a simple predicate based language that matches parts + // of an Event. Some predicates accept inputs which should be surrounded + // with back ticks. The boolean operators '&&' (and), '||' (or), and '!' + // (not) can be used to combined predicates. The following predicates are + // supported: + // + // All() + // Matches all Events. + // + // Header(`key`, `value`) + // Matches if a header `key` exists and is equal to `value`. + // + // Host(`example.com`) + // Matches if the domain (host header value) is equal to input. + // + // Method(`GET`, ...) + // Matches if the request method is one of the given methods (GET, POST, + // PUT, DELETE, PATCH, HEAD) + // + // Path(`/path`) + // Matches if the request path is equal to given input. + // + // PathPrefix(`/prefix`) + // Matches if the request path begins with given input. + // + // Query(`key`, `value`) + // Matches if a query parameter `key` exists and is equal to `value`. + // + // Type(`value`) + // Matches if Event type is equal to given input. + // + // Predicate inputs can utilize regular expressions to match and optionally + // extract parts of an Event to a named parameter. Regular expression use + // the format '{[REGEX]}' or '{[NAME]:[REGEX]}' to extract the matching part + // to a parameter. + // + // Additionally, environment variables can be utilized in predicate inputs. + // They are resolved at request time with the value specified in the + // VirtualEnv. Environment variables can be used with the format + // '{{.Env.[NAME]}}'. + // + // For example, the following will match Events of type 'http' that are + // 'GET' requests and have a path with three parts. The first part of the + // path must equal the value of th environment variable 'SUB_PATH', the + // second part must equal 'orders', and the third part can be one or more + // lower case letter or number. The third part of the path is extracted to + // the parameter 'orderId' which can be used by the EventHandler: + // + // kit.Route("Type(`http`) && Method(`GET`) && Path(`/{{.Env.SUB_PATH}}/orders/{orderId:[a-z0-9]+}`)", + // func(ktx kit.Kontext) error { + // return ktx.Resp().SendStr("The orderId is ", ktx.Param("orderId")) + // }) + Route(rule string, handler EventHandler) + + // Default registers a default EventHandler. If Kit receives an Event from + // the Broker that does not match any registered rules the default + // EventHandler is called. + Default(handler EventHandler) + + // EnvVar registers an environment variable dependency with given options. + // The returned EnvVarDep can be used by EventHandlers to retrieve the value + // of the environment variable at request time. + // + // For example: + // + // v := kit.EnvVar("SOME_VAR") + // kit.Route("Any()", func(ktx kit.Kontext) error { + // return ktx.Resp().SendStr("the value of SOME_VAR is ", ktx.Env(v)) + // }) + EnvVar(name string, opts ...env.VarOption) EnvVarDep + + // Component registers a Component dependency. The returned ComponentDep can + // be used by EventHandlers to invoke the Component at request time. + // + // For example: + // + // b := kit.Component("backend") + // kit.Route("Any()", func(ktx kit.Kontext) error { + // r, _ := ktx.Req(backend).Send() + // return ktx.Resp().SendStr("the resp from backend is ", r.Str()) + // }) + Component(name string) ComponentDep + + // HTTPAdapter registers a dependency on the HTTP Adapter. The returned + // ComponentDep can be used by EventHandlers to invoke the Adapter at + // request time. + // + // For example: + // + // h := kit.HTTPAdapter("httpbin") + // kit.Route("Any()", func(ktx kit.Kontext) error { + // r, _ := ktx.HTTP(h).Get("/anything") + // return ktx.Resp().SendReader(r.Header.Get("content-type"), r.Body) + // }) + HTTPAdapter(name string) ComponentDep + + // Title sets the Component's title. Title(title string) + + // Description sets the Component's description. Description(description string) + // Log returns a pre-configured structured logger for the Component. Log() *logkf.Logger } type Kontext interface { core.EventReader - Env(v EnvVar) string - EnvV(v EnvVar) *api.Val - EnvDef(v EnvVar, def string) string - EnvDefV(v EnvVar, def *api.Val) *api.Val - + // Env returns the value of the given environment variable as a string. If + // the environment variable does not exist or cannot be converted to a + // string, empty string is returned. To check if an environment exists use + // EnvV() and check if the returned Val's ValType is 'Nil'. + Env(v EnvVarDep) string + + // EnvV returns the value of the given environment variable as a Val. It is + // guaranteed the returned Val will not be nil. If the environment variable + // does not exist the ValType of the returned Val will be 'Nil'. + EnvV(v EnvVarDep) *api.Val + + // EnvDef returns the value of the given environment variable as a string. + // If the environment variable does not exist, is empty, or cannot be + // converted to a string, then the given 'def' string is returned. + EnvDef(v EnvVarDep, def string) string + + // EnvDefV returns the value of the given environment variable as a Val. If + // the environment variable does not exist, is an empty string, or an empty + // array, then the given 'def' Val is returned. If the environment variable + // exists and is a boolean or number, then it's value will be returned. + EnvDefV(v EnvVarDep, def *api.Val) *api.Val + + // Resp returns a Resp object that can be used to send a response to the + // source of the current request. Resp() Resp - ForwardResp(resp core.EventReader) Resp - Req(c Dependency) Req - Forward(c Dependency) Req - HTTP(c Dependency) *http.Client - Transport(c Dependency) http.RoundTripper + // Req returns an empty Req object that can be used to send a request to the + // given Component. + Req(c ComponentDep) Req + // Forward returns a Req object that can be used to send a request to the + // given Component. The Req object is a clone of the current request. + Forward(c ComponentDep) Req + + // HTTP returns a native Go http.Client. Any requests made with the client + // are sent to the given Component. The target Component should be capable + // of processing HTTP requests. + HTTP(c ComponentDep) *http.Client + + // HTTP returns a native go http.RoundTripper. This is useful to integrate + // with HTTP based libraries. + Transport(c ComponentDep) http.RoundTripper + + // Context returns a context.Context with it's duration set to the TTL of + // the current request. Context() context.Context + + // Log returns a pre-configured structured logger for the current request. Log() *logkf.Logger } @@ -64,7 +198,8 @@ type Req interface { type Resp interface { core.EventWriter - SendStr(s string) error + Forward(resp core.EventReader) error + SendStr(s ...string) error SendHTML(h string) error SendJSON(v any) error SendAccepts(json any, html, str string) error @@ -73,12 +208,12 @@ type Resp interface { Send() error } -type EnvVar interface { +type EnvVarDep interface { Name() string Type() api.EnvVarType } -type Dependency interface { +type ComponentDep interface { Name() string Type() api.ComponentType EventType() api.EventType diff --git a/matcher/event_matcher.go b/matcher/event_matcher.go index c72c91b..39c04ae 100644 --- a/matcher/event_matcher.go +++ b/matcher/event_matcher.go @@ -197,7 +197,8 @@ func (m *EventMatcher) query(key, val string) (core.EventPredicate, error) { func (m *EventMatcher) eventType(s string) core.EventPredicate { return func(e *core.Event) bool { - return e.GetType() == s + return e.GetType() == s || + strings.HasSuffix(strings.ToLower(e.GetType()), strings.ToLower(s)) } } diff --git a/matcher/event_matcher_test.go b/matcher/event_matcher_test.go index 4389f87..bd78e28 100644 --- a/matcher/event_matcher_test.go +++ b/matcher/event_matcher_test.go @@ -32,7 +32,7 @@ func TestPath(t *testing.T) { r2 := &core.Route{ RouteSpec: api.RouteSpec{ Id: 2, - Rule: "Method(`PUT`,`GET`,`POST`) && (Query(`q1`, `{q[1-2]}`) && Header(`header-one`,`{[a-z0-9]+}`)) && Host(`{{.Env.a}}.0.0.{i}`) && Path(`/customize/{{.Env.b}}/{j:[a-z]+}`)", + Rule: "Type(`http`) && Method(`PUT`,`GET`,`POST`) && (Query(`q1`, `{q[1-2]}`) && Header(`header-one`,`{[a-z0-9]+}`)) && Host(`{{.Env.a}}.0.0.{i}`) && Path(`/customize/{{.Env.b}}/{j:[a-z]+}`)", }, Component: &core.Component{}, EventContext: &core.EventContext{},