From 8715df800faaff2fdf355e2d665e7b48cf91e50e Mon Sep 17 00:00:00 2001 From: grillz Date: Sun, 25 Mar 2018 13:59:00 -0600 Subject: [PATCH 1/7] update --- README.md | 23 +++++++++++--- config.yaml | 45 +++++++++++++++++---------- docs/developing_plugins.md | 3 ++ docs/start.md | 4 ++- integration-compose.yaml | 17 ++++++++++ pkg/context/http.go | 2 ++ pkg/template/middleware/README.tpl | 35 +++++++++++++++++++++ pkg/template/middleware/main.tpl | 40 ++++++++++++++++++++++++ pkg/template/middleware/main_test.tpl | 1 + pkg/template/middleware/spec.tpl | 3 ++ 10 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 docs/developing_plugins.md create mode 100644 pkg/template/middleware/README.tpl create mode 100644 pkg/template/middleware/main.tpl create mode 100644 pkg/template/middleware/main_test.tpl create mode 100644 pkg/template/middleware/spec.tpl diff --git a/README.md b/README.md index 9eca2be..2802d45 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,24 @@ spec: see [start.md](docs/start.md) for more details +## Plugins + +### Listener +Plugin | Description | Build | Contexts Supported +--- | --- | --- | --- + [http](github.com/aunem/transpose-plugins/tree/master/listener/http)| simple http listener | build data | http + +### Middleware +Plugin | Description | Build | Contexts Supported +--- | --- | --- | --- + +### Roundtrip +Plugin | Description | Build | Contexts Supported +--- | --- | --- | --- + [supermux](github.com/aunem/transpose-plugins/tree/master/roundtrip/supermux)| an enhanced router | build data | http, grpc + +To develop plugins see [developing_plugins.md](docs/developing_plugins.md) + ## Inspiration * Envoy [github.com/envoyproxy/envoy](github.com/envoyproxy/envoy) @@ -65,11 +83,6 @@ see [start.md](docs/start.md) for more details * Istio [github.com/istio/istio](github.com/istio/istio) * OpenFaas [github.com/openfaas/faas](github.com/openfaas/faas) -## Libs - -* Oxy [github.com/vulcand/oxy](github.com/vulcand/oxy) -* Gorrilla Mux [github.com/gorilla/mux](github.com/gorilla/mux) - ## Roadmap - [ ] Middleware plugins diff --git a/config.yaml b/config.yaml index 4d609c3..48166fe 100644 --- a/config.yaml +++ b/config.yaml @@ -8,29 +8,42 @@ spec: localBuild: false listener: - name: mylistener + name: httplistener package: github.com/aunem/transpose-plugins/listener/http@poc spec: port: 8080 ssl: false - # middleware: - # request: - # - name: myplugin - # package: github.com/aunem/transpose-plugins/middleware/auth@poc - # spec: - # authUrl: my.auth.com - # clientID: transposeClient - # response: - # - name: myplugin - # package: github.com/oscea/transpose-plugins/middleware/auth@poc - # spec: - # authUrl: my.auth.com - # clientID: transposeClient - # auditUrl: my.audit.com + middleware: + request: + - name: hydraAuth + package: github.com/aunem/transpose-plugins/middleware/hydra@poc + spec: + endpoint: http://localhost:4444 + clientID: admin + clientSecret: password + scopes: + - hydra + - core + resourceMap: + - http: + method: POST + path: "/" + action: create + resource: "rn:myserver:root" + response: + - name: hydraAuth + package: github.com/aunem/transpose-plugins/middleware/hydra@poc + spec: + endpoint: http://localhost:4444 + clientID: admin + clientSecret: password + scopes: + - hydra + - core roundtrip: - name: myroundtrip + name: muxRoundtrip package: github.com/aunem/transpose-plugins/roundtrip/supermux@poc spec: http: diff --git a/docs/developing_plugins.md b/docs/developing_plugins.md new file mode 100644 index 0000000..153bcb2 --- /dev/null +++ b/docs/developing_plugins.md @@ -0,0 +1,3 @@ +# Developing plugins + +...in progress \ No newline at end of file diff --git a/docs/start.md b/docs/start.md index 8b3a794..34be091 100644 --- a/docs/start.md +++ b/docs/start.md @@ -1 +1,3 @@ -# Getting Started \ No newline at end of file +# Getting Started + +...in progress \ No newline at end of file diff --git a/integration-compose.yaml b/integration-compose.yaml index 0a0d12a..5c87bda 100644 --- a/integration-compose.yaml +++ b/integration-compose.yaml @@ -30,3 +30,20 @@ services: working_dir: /go/src/github.com/aunem/transpose volumes: - "./:/go/src/github.com/aunem/transpose" + hydra: + image: oryd/hydra + volumes: + - hydravolume:/root + ports: + - "4444:4444" + - "4445:4445" + command: + host --dangerous-auto-logon --dangerous-force-http --disable-telemetry + environment: + - LOG_LEVEL=debug + - ISSUER=http://localhost:4444 + - CONSENT_URL=http://localhost:3000/consent + - DATABASE_URL=memory + - FORCE_ROOT_CLIENT_CREDENTIALS=admin:password + - SYSTEM_SECRET=youReallyNeedToChangeThis + restart: unless-stopped diff --git a/pkg/context/http.go b/pkg/context/http.go index 5ac68ec..5261e68 100644 --- a/pkg/context/http.go +++ b/pkg/context/http.go @@ -11,6 +11,7 @@ import ( type HTTPRequest struct { ID string Request *http.Request + RW http.ResponseWriter } // HTTPResponse is passed to a plugins response method @@ -18,6 +19,7 @@ type HTTPResponse struct { ID string Request *http.Request Response *http.Response + RW http.ResponseWriter } // NewHTTPRequest returns a new request context diff --git a/pkg/template/middleware/README.tpl b/pkg/template/middleware/README.tpl new file mode 100644 index 0000000..9dc6ccc --- /dev/null +++ b/pkg/template/middleware/README.tpl @@ -0,0 +1,35 @@ +# {{ .Name }} + +// Your overview here + +## Spec +```yaml +apiVersion: alpha.aunem.com/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: mylistener + package: github.com/aunem/transpose-plugins/listener/http + spec: + port: 80 + ssl: false + + roundtrip: + name: myroundtrip + package: github.com/aunem/transpose-plugins/roundtrip/supermux + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +``` + +## Contexts Supported +* Http + +## Test +`go test ./...` \ No newline at end of file diff --git a/pkg/template/middleware/main.tpl b/pkg/template/middleware/main.tpl new file mode 100644 index 0000000..8d28f61 --- /dev/null +++ b/pkg/template/middleware/main.tpl @@ -0,0 +1,40 @@ +package main + +type {{ .Name }}Plugin struct {} + +// MiddlewarePlugin is an interface to transpose +var MiddlewarePlugin {{ .Name }}Plugin + +// Spec holds the spec data +var Spec {{ .Name }}Spec + +func main() {} + +func (m *{{ .Name }}Plugin) ProcessRequest(req context.Request) (context.Request, error) { + return nil, nil +} + +func (m *{{ .Name }}Plugin) ProcessResponse(resp context.Response) (context.Response, error) { + return nil, nil +} + +func (m *{{ .Name }}Plugin) LoadSpec(spec interface{}) error { + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + err = yaml.Unmarshal(b, &Spec) + if err != nil { + return err + } + log.Debugf("loaded spec: %+v", Spec) + return nil +} + +func (m *{{ .Name }}Plugin) Init() error { + return nil +} + +func (m *{{ .Name }}Plugin) Stats() ([]byte, error) { + return nil, nil +} \ No newline at end of file diff --git a/pkg/template/middleware/main_test.tpl b/pkg/template/middleware/main_test.tpl new file mode 100644 index 0000000..f50e5fd --- /dev/null +++ b/pkg/template/middleware/main_test.tpl @@ -0,0 +1 @@ +package main_test \ No newline at end of file diff --git a/pkg/template/middleware/spec.tpl b/pkg/template/middleware/spec.tpl new file mode 100644 index 0000000..705b1d4 --- /dev/null +++ b/pkg/template/middleware/spec.tpl @@ -0,0 +1,3 @@ +package main + +type {{ .Name }}Spec struct {} \ No newline at end of file From bd7f6c13c3eefd98c7a72aabdee709699571cef3 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 31 Mar 2018 14:19:47 -0600 Subject: [PATCH 2/7] middleware templates working! --- README.md | 27 +++++---- cmd/plugin_init.go | 23 ++++++-- cmd/root.go | 4 +- cmd/server.go | 1 + examples/basic_http.yaml | 53 +++++++++++++++++ pkg/context/context.go | 2 + pkg/context/http.go | 12 ++++ pkg/resolve/resolve.go | 7 +++ pkg/template/middleware/README_tpl.go | 51 ++++++++++++++++ .../{README.tpl => example_config_tpl.go} | 22 +++---- pkg/template/middleware/main.tpl | 40 ------------- pkg/template/middleware/main_test.tpl | 1 - pkg/template/middleware/main_test_tpl.go | 15 +++++ pkg/template/middleware/main_tpl.go | 52 +++++++++++++++++ pkg/template/middleware/manifest.go | 10 ++++ pkg/template/middleware/spec.tpl | 3 - pkg/template/middleware/spec_tpl.go | 6 ++ pkg/template/template.go | 58 +++++++++++++++++++ 18 files changed, 311 insertions(+), 76 deletions(-) create mode 100644 examples/basic_http.yaml create mode 100644 pkg/template/middleware/README_tpl.go rename pkg/template/middleware/{README.tpl => example_config_tpl.go} (72%) delete mode 100644 pkg/template/middleware/main.tpl delete mode 100644 pkg/template/middleware/main_test.tpl create mode 100644 pkg/template/middleware/main_test_tpl.go create mode 100644 pkg/template/middleware/main_tpl.go create mode 100644 pkg/template/middleware/manifest.go delete mode 100644 pkg/template/middleware/spec.tpl create mode 100644 pkg/template/middleware/spec_tpl.go diff --git a/README.md b/README.md index 2802d45..15a46c2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,13 @@ Transpose is a cloud native composable proxy with a focus on kubernetes written * Cloud native ## Getting Started -Example config: + +##### Install +`go get -u github.com/aunem/transpose` + +Transpose depends on [dep](github.com/golang/dep) + +##### Example ```yaml apiVersion: alpha.aunem.com/v1 Kind: Transpose @@ -36,13 +42,6 @@ spec: spec: authUrl: my.auth.com clientID: transposeClient - response: - - name: hydraAuth - package: github.com/aunem/transpose-plugins/middleware/hydra - spec: - authUrl: my.auth.com - clientID: transposeClient - auditUrl: my.audit.com roundtrip: name: myroundtrip @@ -62,17 +61,20 @@ see [start.md](docs/start.md) for more details ### Listener Plugin | Description | Build | Contexts Supported --- | --- | --- | --- - [http](github.com/aunem/transpose-plugins/tree/master/listener/http)| simple http listener | build data | http +[http](github.com/aunem/transpose-plugins/listener/http)| simple http listener | build data | http ### Middleware Plugin | Description | Build | Contexts Supported --- | --- | --- | --- +[hydra](github.com/aunem/transpose-plugins/middleware/hydra)| hydra auth middleware | build data | http ### Roundtrip Plugin | Description | Build | Contexts Supported --- | --- | --- | --- - [supermux](github.com/aunem/transpose-plugins/tree/master/roundtrip/supermux)| an enhanced router | build data | http, grpc +[supermux](github.com/aunem/transpose-plugins/roundtrip/supermux)| an enhanced router | build data | http, grpc + + To develop plugins see [developing_plugins.md](docs/developing_plugins.md) ## Inspiration @@ -82,12 +84,13 @@ To develop plugins see [developing_plugins.md](docs/developing_plugins.md) * Gentleman [github.com/h2non/gentleman](github.com/h2non/gentleman) * Istio [github.com/istio/istio](github.com/istio/istio) * OpenFaas [github.com/openfaas/faas](github.com/openfaas/faas) +* Fluentd [github.com/fluent/fluentd](github.com/fluent/fluentd) ## Roadmap - [ ] Middleware plugins -- [ ] Roundtrip plugins -- [ ] Listener plugins +- [x] Roundtrip plugins +- [x] Listener plugins - [ ] HTTP/2 - [ ] GRPC - [ ] Data plane diff --git a/cmd/plugin_init.go b/cmd/plugin_init.go index b15e181..b49d360 100644 --- a/cmd/plugin_init.go +++ b/cmd/plugin_init.go @@ -3,12 +3,14 @@ package cmd import ( "fmt" + res "github.com/aunem/transpose/pkg/resolve" + "github.com/aunem/transpose/pkg/template" "github.com/aunem/transpose/pkg/utils" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -var ptyp string -var pname string +var ptyp, pname, ppackage string var initCmd = &cobra.Command{ Use: "init", @@ -19,13 +21,24 @@ var initCmd = &cobra.Command{ func init() { pluginCmd.AddCommand(initCmd) - initCmd.Flags().StringVarP(&ptyp, "type", "t", "", "type of plugin (listener, middleware, or roundtrip)") - initCmd.Flags().StringVarP(&pname, "name", "n", "", "name of plugin") + initCmd.Flags().StringVarP(&ptyp, "type", "t", "", "type of plugin (listener, middleware, or roundtrip) (required)") + initCmd.Flags().StringVarP(&pname, "name", "n", "", "name of plugin (required)") + initCmd.Flags().StringVarP(&ppackage, "package", "p", "", "golang package (required)") initCmd.MarkFlagRequired("type") initCmd.MarkFlagRequired("name") + initCmd.MarkFlagRequired("package") } // Init plugins func Init(cmd *cobra.Command, args []string) { - fmt.Println("not yet implemented") + + fmt.Println("creating templates...") + p := template.NewPlugin(pname, ppackage, ptyp) + err := p.Template() + if err != nil { + log.Fatal(err) + } + fmt.Println("resolving dependencies...") + res.Init() + fmt.Println("successfully templated plugin repo!") } diff --git a/cmd/root.go b/cmd/root.go index cd4210e..525fe69 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,14 +24,14 @@ var rootCmd = &cobra.Command{ } func init() { - cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&name, "config-name", "", "name of the kubernetes config to sync to") rootCmd.PersistentFlags().StringVar(&namespace, "config-namespace", "local", "namespace of the kubernetes config to sync to, use 'local' for a local config, leave blank for current ns") viper.BindPFlag("config-name", rootCmd.PersistentFlags().Lookup("config-name")) viper.BindPFlag("config-namespace", rootCmd.PersistentFlags().Lookup("config-namespace")) } -func initConfig() { +// InitConfig initializes the config +func InitConfig() { r := strings.NewReplacer("_", "-") viper.SetEnvKeyReplacer(r) viper.AutomaticEnv() diff --git a/cmd/server.go b/cmd/server.go index 0abd8e5..e756143 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -23,6 +23,7 @@ var serverCmd = &cobra.Command{ // Serve starts a new transpose server func Serve(cmd *cobra.Command, args []string) { + InitConfig() log.Info("finding or creating bins...") utils.MakeBins() log.Info("resolving middleware plugin...") diff --git a/examples/basic_http.yaml b/examples/basic_http.yaml new file mode 100644 index 0000000..48166fe --- /dev/null +++ b/examples/basic_http.yaml @@ -0,0 +1,53 @@ +apiVersion: alpha.aunem.com/v1 +kind: Transpose +metadata: + name: myProxy + namespace: default +spec: + debug: true + localBuild: false + + listener: + name: httplistener + package: github.com/aunem/transpose-plugins/listener/http@poc + spec: + port: 8080 + ssl: false + + middleware: + request: + - name: hydraAuth + package: github.com/aunem/transpose-plugins/middleware/hydra@poc + spec: + endpoint: http://localhost:4444 + clientID: admin + clientSecret: password + scopes: + - hydra + - core + resourceMap: + - http: + method: POST + path: "/" + action: create + resource: "rn:myserver:root" + response: + - name: hydraAuth + package: github.com/aunem/transpose-plugins/middleware/hydra@poc + spec: + endpoint: http://localhost:4444 + clientID: admin + clientSecret: password + scopes: + - hydra + - core + + roundtrip: + name: muxRoundtrip + package: github.com/aunem/transpose-plugins/roundtrip/supermux@poc + spec: + http: + - path: "/" + backend: + serviceName: server + servicePort: 80 diff --git a/pkg/context/context.go b/pkg/context/context.go index f5a0d9c..fd6a64f 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -3,12 +3,14 @@ package context // Request is a generic incomming request interface type Request interface { GetID() string + GetMeta() map[string]string GetRequest() interface{} } // Response is a generic returned response interface type Response interface { GetID() string + GetMeta() map[string]string GetRequest() interface{} GetResponse() interface{} } diff --git a/pkg/context/http.go b/pkg/context/http.go index 5261e68..66c9ca6 100644 --- a/pkg/context/http.go +++ b/pkg/context/http.go @@ -10,6 +10,7 @@ import ( // HTTPRequest is passed to a plugins request method type HTTPRequest struct { ID string + Meta map[string]string Request *http.Request RW http.ResponseWriter } @@ -17,6 +18,7 @@ type HTTPRequest struct { // HTTPResponse is passed to a plugins response method type HTTPResponse struct { ID string + Meta map[string]string Request *http.Request Response *http.Response RW http.ResponseWriter @@ -36,6 +38,11 @@ func (req *HTTPRequest) GetID() string { return req.ID } +// GetMeta returns the request metadata +func (req *HTTPRequest) GetMeta() map[string]string { + return req.Meta +} + // GetRequest returns the request func (req *HTTPRequest) GetRequest() interface{} { return req.Request @@ -55,6 +62,11 @@ func (resp *HTTPResponse) GetID() string { return resp.ID } +// GetMeta returns the response metadata +func (resp *HTTPResponse) GetMeta() map[string]string { + return resp.Meta +} + // GetRequest returns the request func (resp *HTTPResponse) GetRequest() interface{} { return resp.Request diff --git a/pkg/resolve/resolve.go b/pkg/resolve/resolve.go index 275b279..795fc5d 100644 --- a/pkg/resolve/resolve.go +++ b/pkg/resolve/resolve.go @@ -27,6 +27,13 @@ const ( RoundtripType Type = "roundtrip" ) +// Init adds a package using dep +func Init() { + cmdName := "dep" + cmdArgs := []string{"init"} + ExecCommand(cmdName, cmdArgs) +} + // AddPkg adds a package using dep func AddPkg(pkg string) { cmdName := "dep" diff --git a/pkg/template/middleware/README_tpl.go b/pkg/template/middleware/README_tpl.go new file mode 100644 index 0000000..7c222ae --- /dev/null +++ b/pkg/template/middleware/README_tpl.go @@ -0,0 +1,51 @@ +package middleware + +var readmeTpl = ` + +# {{ .Name }} + +// Your overview here + +## Spec +` + "```yaml " + ` +apiVersion: alpha.aunem.com/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: mylistener + package: github.com/aunem/transpose-plugins/listener/http + spec: + port: 80 + ssl: false + + middleware: + name: {{ .Name }} + package: {{ .Pkg }} + spec: + my: spec + + roundtrip: + name: myroundtrip + package: github.com/aunem/transpose-plugins/roundtrip/supermux + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +` + "```" + ` + +## Contexts Supported +* Http + +## Compatibility +// your compatibility data goes here + +## Dependencies +// your dependencies go here + +## Test +` + "`go test ./...`" diff --git a/pkg/template/middleware/README.tpl b/pkg/template/middleware/example_config_tpl.go similarity index 72% rename from pkg/template/middleware/README.tpl rename to pkg/template/middleware/example_config_tpl.go index 9dc6ccc..1d46264 100644 --- a/pkg/template/middleware/README.tpl +++ b/pkg/template/middleware/example_config_tpl.go @@ -1,10 +1,6 @@ -# {{ .Name }} +package middleware -// Your overview here - -## Spec -```yaml -apiVersion: alpha.aunem.com/v1 +var exampleTpl = `apiVersion: alpha.aunem.com/v1 Kind: Transpose Metadata: name: myProxy @@ -16,6 +12,12 @@ spec: spec: port: 80 ssl: false + + middleware: + name: {{ .Name }} + package: {{ .Pkg }} + spec: + my: spec roundtrip: name: myroundtrip @@ -26,10 +28,4 @@ spec: backend: serviceName: myservice servicePort: 80 -``` - -## Contexts Supported -* Http - -## Test -`go test ./...` \ No newline at end of file +` diff --git a/pkg/template/middleware/main.tpl b/pkg/template/middleware/main.tpl deleted file mode 100644 index 8d28f61..0000000 --- a/pkg/template/middleware/main.tpl +++ /dev/null @@ -1,40 +0,0 @@ -package main - -type {{ .Name }}Plugin struct {} - -// MiddlewarePlugin is an interface to transpose -var MiddlewarePlugin {{ .Name }}Plugin - -// Spec holds the spec data -var Spec {{ .Name }}Spec - -func main() {} - -func (m *{{ .Name }}Plugin) ProcessRequest(req context.Request) (context.Request, error) { - return nil, nil -} - -func (m *{{ .Name }}Plugin) ProcessResponse(resp context.Response) (context.Response, error) { - return nil, nil -} - -func (m *{{ .Name }}Plugin) LoadSpec(spec interface{}) error { - b, err := yaml.Marshal(spec) - if err != nil { - return err - } - err = yaml.Unmarshal(b, &Spec) - if err != nil { - return err - } - log.Debugf("loaded spec: %+v", Spec) - return nil -} - -func (m *{{ .Name }}Plugin) Init() error { - return nil -} - -func (m *{{ .Name }}Plugin) Stats() ([]byte, error) { - return nil, nil -} \ No newline at end of file diff --git a/pkg/template/middleware/main_test.tpl b/pkg/template/middleware/main_test.tpl deleted file mode 100644 index f50e5fd..0000000 --- a/pkg/template/middleware/main_test.tpl +++ /dev/null @@ -1 +0,0 @@ -package main_test \ No newline at end of file diff --git a/pkg/template/middleware/main_test_tpl.go b/pkg/template/middleware/main_test_tpl.go new file mode 100644 index 0000000..291760c --- /dev/null +++ b/pkg/template/middleware/main_test_tpl.go @@ -0,0 +1,15 @@ +package middleware + +var mainTestTpl = ` +package main_test + +func TestMain(m *testing.M) { + m.Run() +} + +func TestProcessRequest(t *testing.T) { +} + +func TestProcessResponse(t *testing.T) { +} +` diff --git a/pkg/template/middleware/main_tpl.go b/pkg/template/middleware/main_tpl.go new file mode 100644 index 0000000..b3c55e9 --- /dev/null +++ b/pkg/template/middleware/main_tpl.go @@ -0,0 +1,52 @@ +package middleware + +var mainTpl = `package main + +import ( + "fmt" + "path" + + "github.com/aunem/transpose/pkg/context" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +type {{ .Name }}Plugin struct {} + +// MiddlewarePlugin exports the plugin struct +var MiddlewarePlugin {{ .Name }}Plugin + +// Spec exports the spec data +var Spec {{ .Name }}Spec + +func main() {} + +func (p *{{ .Name }}Plugin) ProcessRequest(req context.Request) (context.Request, error) { + return nil, nil +} + +func (p *{{ .Name }}Plugin) ProcessResponse(resp context.Response) (context.Response, error) { + return nil, nil +} + +func (p *{{ .Name }}Plugin) LoadSpec(spec interface{}) error { + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + err = yaml.Unmarshal(b, &Spec) + if err != nil { + return err + } + log.Debugf("loaded spec: %+v", Spec) + return nil +} + +func (p *{{ .Name }}Plugin) Init() error { + return nil +} + +func (p *{{ .Name }}Plugin) Stats() ([]byte, error) { + return nil, nil +} +` diff --git a/pkg/template/middleware/manifest.go b/pkg/template/middleware/manifest.go new file mode 100644 index 0000000..882d2d9 --- /dev/null +++ b/pkg/template/middleware/manifest.go @@ -0,0 +1,10 @@ +package middleware + +// Manifest represents the variables to inclued in the templating +var Manifest = map[string]string{ + "main.go": mainTpl, + "main_test.go": mainTestTpl, + "example-config.yaml": exampleTpl, + "README.md": readmeTpl, + "spec.go": specTpl, +} diff --git a/pkg/template/middleware/spec.tpl b/pkg/template/middleware/spec.tpl deleted file mode 100644 index 705b1d4..0000000 --- a/pkg/template/middleware/spec.tpl +++ /dev/null @@ -1,3 +0,0 @@ -package main - -type {{ .Name }}Spec struct {} \ No newline at end of file diff --git a/pkg/template/middleware/spec_tpl.go b/pkg/template/middleware/spec_tpl.go new file mode 100644 index 0000000..67aeaa4 --- /dev/null +++ b/pkg/template/middleware/spec_tpl.go @@ -0,0 +1,6 @@ +package middleware + +var specTpl = `package main + +type {{ .Name }}Spec struct {} +` diff --git a/pkg/template/template.go b/pkg/template/template.go index 38cdfe4..4b77087 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -1 +1,59 @@ package template + +import ( + "fmt" + "os" + tpl "text/template" + + mw "github.com/aunem/transpose/pkg/template/middleware" +) + +// TemplateDir represents a templated directory +type TemplateDir string + +var middlewaredir TemplateDir = "pkg/template/middleware" +var roundtripdir TemplateDir = "pkg/template/roundtrip" +var listenerdir TemplateDir = "pkg/template/listener" + +// Plugin represents the template data for a plugin +type Plugin struct { + Name, Pkg, Typ string +} + +// NewPlugin creates a new plugin +func NewPlugin(name, pkg, typ string) *Plugin { + return &Plugin{ + Name: name, + Pkg: pkg, + Typ: typ, + } +} + +// Template will take the plugin type and template the appropriate files +func (p *Plugin) Template() error { + switch p.Typ { + case "middleware": + for k, v := range mw.Manifest { + t, err := tpl.New("tpl").Parse(v) + if err != nil { + return err + } + f, err := os.Create(k) + if err != nil { + return err + } + defer f.Close() + t.Execute(f, p) + if err != nil { + return err + } + } + case "listener": + + case "roundtrip": + + default: + return fmt.Errorf("plugin typ uknown: %+v", p.Typ) + } + return nil +} From 7633a4d36b9e25cd65afd97c5a47cc6b63220750 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 31 Mar 2018 14:34:44 -0600 Subject: [PATCH 3/7] add docs, remove loadPlugin --- docs/developing_plugins.md | 20 ++++++++++++++++++++ pkg/listener/manager.go | 4 ++-- pkg/listener/plugin.go | 4 ++-- pkg/middleware/manager.go | 12 ++---------- pkg/middleware/plugin.go | 6 ++---- pkg/roundtrip/manager.go | 9 ++------- pkg/roundtrip/plugin.go | 6 ++---- pkg/template/middleware/main_tpl.go | 6 +----- 8 files changed, 33 insertions(+), 34 deletions(-) diff --git a/docs/developing_plugins.md b/docs/developing_plugins.md index 153bcb2..e31a3ae 100644 --- a/docs/developing_plugins.md +++ b/docs/developing_plugins.md @@ -1,3 +1,23 @@ # Developing plugins +The fastest way to start developing a plugin is to use the CLI tool. + +install: +`go install github.com/aunem/transpose` + +check out the help command: +`transpose plugin init --help` + +example: + +```bash +PLUGIN_NAME="mynewplugin" +PLUGIN_PACKAGE="github.com/myuser/myrepo" +PLUGIN_TYPE="middleware" # middleware, listener, or roundtrip allowed + +transpose plugin init -n ${PLUGIN_NAME} -p ${PLUGIN_PACKAGE} -t ${PLUGIN_TYPE} +``` +this will scaffold you out a good start for your plugin, now lets walk though the files present... + + ...in progress \ No newline at end of file diff --git a/pkg/listener/manager.go b/pkg/listener/manager.go index 65f9eef..49d69ad 100644 --- a/pkg/listener/manager.go +++ b/pkg/listener/manager.go @@ -47,8 +47,8 @@ func NewManager(spec config.TransposeSpec, mw *middleware.Manager, rt *roundtrip if err != nil { return nil, err } - log.Debug("loading spec...") - err = rtp.LoadSpec(spec.Listener.Spec) + log.Debug("running init function...") + err = rtp.Init(spec.Listener.Spec) if err != nil { return nil, err } diff --git a/pkg/listener/plugin.go b/pkg/listener/plugin.go index b4788fc..d348416 100644 --- a/pkg/listener/plugin.go +++ b/pkg/listener/plugin.go @@ -9,8 +9,8 @@ import ( type Plugin interface { // Listen Listen(mw *middleware.Manager, rt *roundtrip.Manager) error - // LoadSpec loads the spec on initialization and config updates - LoadSpec(spec interface{}) error + // Init is called on initialization and config updates + Init(spec interface{}) error // Stats can return arbitrary json stats Stats() ([]byte, error) } diff --git a/pkg/middleware/manager.go b/pkg/middleware/manager.go index dab2af2..6a1aa61 100644 --- a/pkg/middleware/manager.go +++ b/pkg/middleware/manager.go @@ -44,11 +44,7 @@ func NewManager(spec config.TransposeSpec) (*Manager, error) { if err != nil { return nil, err } - err = mw.Plugin.LoadSpec(plugin.Spec) - if err != nil { - return nil, err - } - err = mw.Plugin.Init() + err = mw.Plugin.Init(plugin.Spec) if err != nil { return nil, err } @@ -59,11 +55,7 @@ func NewManager(spec config.TransposeSpec) (*Manager, error) { if err != nil { return nil, err } - err = mw.Plugin.LoadSpec(plugin.Spec) - if err != nil { - return nil, err - } - err = mw.Plugin.Init() + err = mw.Plugin.Init(plugin.Spec) if err != nil { return nil, err } diff --git a/pkg/middleware/plugin.go b/pkg/middleware/plugin.go index b3112af..09edfb1 100644 --- a/pkg/middleware/plugin.go +++ b/pkg/middleware/plugin.go @@ -8,10 +8,8 @@ type Plugin interface { ProcessRequest(req context.Request) (context.Request, error) // ProcessResponse will be called on incoming responses ProcessResponse(resp context.Response) (context.Response, error) - // Init is ran once on plugin initialization - Init() error - // LoadSpec loads the spec on initialization and config updates - LoadSpec(spec interface{}) error + // Init is called on initialization and config updates + Init(spec interface{}) error // Stats can return arbitrary json stats Stats() (error, []byte) } diff --git a/pkg/roundtrip/manager.go b/pkg/roundtrip/manager.go index bd83792..a87ab06 100644 --- a/pkg/roundtrip/manager.go +++ b/pkg/roundtrip/manager.go @@ -42,13 +42,8 @@ func NewManager(spec config.TransposeSpec) (*Manager, error) { if err != nil { return nil, err } - log.Debug("loading spec...") - err = rtp.LoadSpec(spec.Roundtrip.Spec) - if err != nil { - return nil, err - } - log.Debug("running init...") - err = rtp.Init() + log.Debug("running init function...") + err = rtp.Init(spec.Roundtrip.Spec) if err != nil { return nil, err } diff --git a/pkg/roundtrip/plugin.go b/pkg/roundtrip/plugin.go index 9992546..503ff87 100644 --- a/pkg/roundtrip/plugin.go +++ b/pkg/roundtrip/plugin.go @@ -6,10 +6,8 @@ import "github.com/aunem/transpose/pkg/context" type Plugin interface { // Roundtrip handles interactions with the downstream service/s Roundtrip(req context.Request) (context.Response, error) - // Init is ran once on plugin initialization - Init() error - // LoadSpec loads the spec on initialization and config updates - LoadSpec(spec interface{}) error + // Init is called on initialization and config updates + Init(spec interface{}) error // Stats can return arbitrary json stats Stats() ([]byte, error) } diff --git a/pkg/template/middleware/main_tpl.go b/pkg/template/middleware/main_tpl.go index b3c55e9..0a6e8fb 100644 --- a/pkg/template/middleware/main_tpl.go +++ b/pkg/template/middleware/main_tpl.go @@ -29,7 +29,7 @@ func (p *{{ .Name }}Plugin) ProcessResponse(resp context.Response) (context.Resp return nil, nil } -func (p *{{ .Name }}Plugin) LoadSpec(spec interface{}) error { +func (p *{{ .Name }}Plugin) Init(spec interface{}) error { b, err := yaml.Marshal(spec) if err != nil { return err @@ -42,10 +42,6 @@ func (p *{{ .Name }}Plugin) LoadSpec(spec interface{}) error { return nil } -func (p *{{ .Name }}Plugin) Init() error { - return nil -} - func (p *{{ .Name }}Plugin) Stats() ([]byte, error) { return nil, nil } From 940c2829b02a468a4005b813acfe511ed7513161 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 7 Apr 2018 11:55:50 -0600 Subject: [PATCH 4/7] circle-ci --- README.md | 7 ++++++- docs/img/Gopher_logo.png | Bin 0 -> 39231 bytes pkg/template/middleware/example_config_tpl.go | 1 + pkg/template/middleware/manifest.go | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100755 docs/img/Gopher_logo.png diff --git a/README.md b/README.md index 15a46c2..cf4c98d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![logo](docs/img/Gopher_logo.png) + # Transpose Transpose is a cloud native composable proxy with a focus on kubernetes written in go. @@ -100,4 +102,7 @@ To develop plugins see [developing_plugins.md](docs/developing_plugins.md) Development happens out of the [Makefile](./Makefile). The targets are fairly simple but if you have any issues contact us. -## Contact +## Contact and Questions + +Chat with us on discord: +https://discord.gg/q9mnWtd diff --git a/docs/img/Gopher_logo.png b/docs/img/Gopher_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..3a2a1cbbecbabaa436a86a34f77b5f69c253ec8d GIT binary patch literal 39231 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 literal 0 HcmV?d00001 diff --git a/pkg/template/middleware/example_config_tpl.go b/pkg/template/middleware/example_config_tpl.go index 1d46264..0493bc7 100644 --- a/pkg/template/middleware/example_config_tpl.go +++ b/pkg/template/middleware/example_config_tpl.go @@ -16,6 +16,7 @@ spec: middleware: name: {{ .Name }} package: {{ .Pkg }} + # Add your spec data here spec: my: spec diff --git a/pkg/template/middleware/manifest.go b/pkg/template/middleware/manifest.go index 882d2d9..dc9f8b6 100644 --- a/pkg/template/middleware/manifest.go +++ b/pkg/template/middleware/manifest.go @@ -1,6 +1,6 @@ package middleware -// Manifest represents the variables to inclued in the templating +// Manifest is a map of file name to template string var Manifest = map[string]string{ "main.go": mainTpl, "main_test.go": mainTestTpl, From 211a4e07ad202e5c1cf46f6ecc6b5901f791e088 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 7 Apr 2018 12:04:25 -0600 Subject: [PATCH 5/7] circle --- .circleci/config.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..1669c0a --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +# Golang CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-go/ for more details +version: 2 +jobs: + build: + docker: + # specify the version + - image: circleci/golang:1.9 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + #### TEMPLATE_NOTE: go expects specific checkout path representing url + #### expecting it in the form of + #### /go/src/github.com/circleci/go-tool + #### /go/src/bitbucket.org/circleci/go-tool + working_directory: /go/src/github.com/aunem/transpose + steps: + - checkout + + # specify any bash command here prefixed with `run: ` + - run: go get -u github.com/golang/dep/cmd/dep + run: dep ensure + - run: go test -v ./... From f97db4e4dc09d1a80ef07620b4dd4593427e7a96 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 7 Apr 2018 13:46:07 -0600 Subject: [PATCH 6/7] ci, templates, and more! --- .circleci/config.yml | 26 ++++--- Gopkg.lock | 28 ++++---- MAINTAINERS.txt | 3 +- Makefile | 18 +++-- cmd/plugin_resolve.go | 8 +-- config.yaml | 54 +++++++------- docker-compose.yaml | 71 +++++++++++++++++++ integration-compose.yaml | 49 ------------- integration/test/main.go | 66 +++++++++++++++++ pkg/template/listener/README_tpl.go | 45 ++++++++++++ pkg/template/listener/example_config_tpl.go | 25 +++++++ pkg/template/listener/main_test_tpl.go | 12 ++++ pkg/template/listener/main_tpl.go | 45 ++++++++++++ pkg/template/listener/manifest.go | 10 +++ pkg/template/listener/spec_tpl.go | 6 ++ pkg/template/middleware/README_tpl.go | 4 +- pkg/template/middleware/example_config_tpl.go | 2 +- pkg/template/roundtrip/README_tpl.go | 45 ++++++++++++ pkg/template/roundtrip/example_config_tpl.go | 25 +++++++ pkg/template/roundtrip/main_test_tpl.go | 12 ++++ pkg/template/roundtrip/main_tpl.go | 44 ++++++++++++ pkg/template/roundtrip/manifest.go | 10 +++ pkg/template/roundtrip/spec_tpl.go | 6 ++ pkg/template/template.go | 36 +++++++++- pkg/utils/retry.go | 26 +++++++ 25 files changed, 564 insertions(+), 112 deletions(-) delete mode 100644 integration-compose.yaml create mode 100644 integration/test/main.go create mode 100644 pkg/template/listener/README_tpl.go create mode 100644 pkg/template/listener/example_config_tpl.go create mode 100644 pkg/template/listener/main_test_tpl.go create mode 100644 pkg/template/listener/main_tpl.go create mode 100644 pkg/template/listener/manifest.go create mode 100644 pkg/template/listener/spec_tpl.go create mode 100644 pkg/template/roundtrip/README_tpl.go create mode 100644 pkg/template/roundtrip/example_config_tpl.go create mode 100644 pkg/template/roundtrip/main_test_tpl.go create mode 100644 pkg/template/roundtrip/main_tpl.go create mode 100644 pkg/template/roundtrip/manifest.go create mode 100644 pkg/template/roundtrip/spec_tpl.go create mode 100644 pkg/utils/retry.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 1669c0a..458a549 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,15 +13,25 @@ jobs: # documented at https://circleci.com/docs/2.0/circleci-images/ # - image: circleci/postgres:9.4 - #### TEMPLATE_NOTE: go expects specific checkout path representing url - #### expecting it in the form of - #### /go/src/github.com/circleci/go-tool - #### /go/src/bitbucket.org/circleci/go-tool working_directory: /go/src/github.com/aunem/transpose steps: - checkout + + - run: + name: Install Docker Compose + command: | + curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose + chmod +x ~/docker-compose + sudo mv ~/docker-compose /usr/local/bin/docker-compose + + - setup_remote_docker + + - run: + name: Install deps + command: | + make deps - # specify any bash command here prefixed with `run: ` - - run: go get -u github.com/golang/dep/cmd/dep - run: dep ensure - - run: go test -v ./... + - run: + name: Integration test + command: | + make integration diff --git a/Gopkg.lock b/Gopkg.lock index 4a1b71f..b5580ba 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -5,7 +5,7 @@ branch = "poc" name = "github.com/aunem/transpose-plugins" packages = ["listener/http"] - revision = "fc37803b59edbbb30495388380b606bee226a36c" + revision = "fea78728c3d7f254246c5f6d3c70d60946e95eda" [[projects]] name = "github.com/davecgh/go-spew" @@ -84,7 +84,7 @@ "json/scanner", "json/token" ] - revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" + revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" [[projects]] name = "github.com/inconshreveable/mousetrap" @@ -134,8 +134,8 @@ ".", "mem" ] - revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" - version = "v1.0.2" + revision = "63644898a8da0bc22138abf860edaf5277b6102e" + version = "v1.1.0" [[projects]] name = "github.com/spf13/cast" @@ -146,8 +146,8 @@ [[projects]] name = "github.com/spf13/cobra" packages = ["."] - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" + revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4" + version = "v0.0.2" [[projects]] branch = "master" @@ -177,7 +177,7 @@ branch = "master" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - revision = "88942b9c40a4c9d203b82b3731787b672d6e809b" + revision = "b2aa35443fbc700ab74c586ae79b81c171851023" [[projects]] branch = "master" @@ -191,7 +191,7 @@ "lex/httplex", "trace" ] - revision = "6078986fec03a1dcc236c34816c71b0e05018fda" + revision = "61147c48b25b599e5b561d2e9c4f3e1ef489ca41" [[projects]] branch = "master" @@ -200,7 +200,7 @@ "unix", "windows" ] - revision = "91ee8cde435411ca3f1cd365e8f20131aed4d0a1" + revision = "3b87a42e500a6dc65dae1a55d0b641295971163e" [[projects]] name = "golang.org/x/text" @@ -230,7 +230,7 @@ "googleapis/api/annotations", "googleapis/rpc/status" ] - revision = "ab0870e398d5dd054b868c0db1481ab029b9a9f2" + revision = "ce84044298496ef4b54b4a0a0909ba593cc60e30" [[projects]] name = "google.golang.org/grpc" @@ -259,14 +259,14 @@ "tap", "transport" ] - revision = "8e4536a86ab602859c20df5ebfd0bd4228d08655" - version = "v1.10.0" + revision = "d89cded64628466c4ab532d1f0ba5c220459ebe8" + version = "v1.11.2" [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] - revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" - version = "v2.1.1" + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" [solve-meta] analyzer-name = "dep" diff --git a/MAINTAINERS.txt b/MAINTAINERS.txt index 10364df..a4902ba 100644 --- a/MAINTAINERS.txt +++ b/MAINTAINERS.txt @@ -1 +1,2 @@ -Patrick Barker -- grillz \ No newline at end of file + | | +Patrick Barker | @grillz | patrickbarkerco@gmail.com \ No newline at end of file diff --git a/Makefile b/Makefile index d28cf48..30ae16f 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,28 @@ .PHONY: deps deps: + go get -u github.com/golang/dep/cmd/dep docker-compose run --rm deps .PHONY: build - docker-compose run -rm build +build: + docker-compose run --rm build + +.PHONY: plugins +plugins: + docker-compose run --rm plugins .PHONY: integration -integration: - docker-compose -f integration-compose.yaml up +integration: plugins + docker-compose run --rm integration .PHONY: integration-down integration-down: - docker-compose -f integration-compose.yaml down + docker-compose down + +.PHONY: clean +clean: + go run main.go plugin clean .PHONY: proto-gateway proto-gateway: diff --git a/cmd/plugin_resolve.go b/cmd/plugin_resolve.go index bac4256..fe6e86c 100644 --- a/cmd/plugin_resolve.go +++ b/cmd/plugin_resolve.go @@ -16,7 +16,7 @@ import ( var localBuild bool var build bool -var reolveCmd = &cobra.Command{ +var resolveCmd = &cobra.Command{ Use: "resolve", Short: "resolve plugins", Long: utils.GetArt(), @@ -24,9 +24,9 @@ var reolveCmd = &cobra.Command{ } func init() { - pluginCmd.AddCommand(reolveCmd) - initCmd.Flags().BoolVarP(&localBuild, "local", "l", false, "build plugin from local gopath") - initCmd.Flags().BoolVarP(&build, "build", "b", false, "resolve all plugins anew") + pluginCmd.AddCommand(resolveCmd) + resolveCmd.Flags().BoolVarP(&localBuild, "local", "l", false, "build plugin from local gopath") + resolveCmd.Flags().BoolVarP(&build, "build", "b", false, "resolve all plugins anew") } // Resolve plugins diff --git a/config.yaml b/config.yaml index 48166fe..f82ec23 100644 --- a/config.yaml +++ b/config.yaml @@ -14,33 +14,33 @@ spec: port: 8080 ssl: false - middleware: - request: - - name: hydraAuth - package: github.com/aunem/transpose-plugins/middleware/hydra@poc - spec: - endpoint: http://localhost:4444 - clientID: admin - clientSecret: password - scopes: - - hydra - - core - resourceMap: - - http: - method: POST - path: "/" - action: create - resource: "rn:myserver:root" - response: - - name: hydraAuth - package: github.com/aunem/transpose-plugins/middleware/hydra@poc - spec: - endpoint: http://localhost:4444 - clientID: admin - clientSecret: password - scopes: - - hydra - - core + # middleware: + # request: + # - name: hydraAuth + # package: github.com/aunem/transpose-plugins/middleware/hydra@poc + # spec: + # endpoint: http://localhost:4444 + # clientID: admin + # clientSecret: password + # scopes: + # - hydra + # - core + # resourceMap: + # - http: + # method: POST + # path: "/" + # action: create + # resource: "rn:myserver:root" + # response: + # - name: hydraAuth + # package: github.com/aunem/transpose-plugins/middleware/hydra@poc + # spec: + # endpoint: http://localhost:4444 + # clientID: admin + # clientSecret: password + # scopes: + # - hydra + # - core roundtrip: name: muxRoundtrip diff --git a/docker-compose.yaml b/docker-compose.yaml index 8ce5cc0..e72c29f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ version: '3' services: + # resolve deps deps: build: context: ./ @@ -8,6 +9,8 @@ services: - "./:/go/src/github.com/aunem/transpose" command: dep ensure working_dir: /go/src/github.com/aunem/transpose + + # build binary build: build: context: ./ @@ -16,3 +19,71 @@ services: - "./:/go/src/github.com/aunem/transpose" command: go build . working_dir: /go/src/github.com/aunem/transpose + + # resolve plugins + plugins: + build: + context: ./ + dockerfile: Dockerfile-integration + volumes: + - "./:/go/src/github.com/aunem/transpose" + command: go run main.go plugin resolve --build + working_dir: /go/src/github.com/aunem/transpose + + # transpose server + transpose: + build: + context: ./ + dockerfile: Dockerfile-integration + command: go run main.go serve + links: + - server + working_dir: /go/src/github.com/aunem/transpose + volumes: + - "./:/go/src/github.com/aunem/transpose" + expose: + - "80" + + # test server + server: + build: + context: ./ + dockerfile: Dockerfile-integration + command: go run integration/server/main.go + working_dir: /go/src/github.com/aunem/transpose + volumes: + - "./:/go/src/github.com/aunem/transpose" + + # integration tests + integration: + build: + context: ./ + dockerfile: Dockerfile-integration + command: go run integration/test/main.go + depends_on: + - transpose + - server + links: + - transpose + working_dir: /go/src/github.com/aunem/transpose + volumes: + - "./:/go/src/github.com/aunem/transpose" + +# # hydra auth middleware +# hydra: +# image: oryd/hydra +# volumes: +# - hydravolume:/root +# ports: +# - "4444:4444" +# - "4445:4445" +# command: +# host --dangerous-auto-logon --dangerous-force-http --disable-telemetry +# environment: +# - LOG_LEVEL=debug +# - ISSUER=http://localhost:4444 +# - CONSENT_URL=http://localhost:3000/consent +# - DATABASE_URL=memory +# - FORCE_ROOT_CLIENT_CREDENTIALS=admin:password +# - SYSTEM_SECRET=youReallyNeedToChangeThis +# restart: unless-stopped \ No newline at end of file diff --git a/integration-compose.yaml b/integration-compose.yaml deleted file mode 100644 index 5c87bda..0000000 --- a/integration-compose.yaml +++ /dev/null @@ -1,49 +0,0 @@ -version: '3' -services: - transpose: - build: - context: ./ - dockerfile: Dockerfile-integration - command: go run main.go serve - links: - - server - working_dir: /go/src/github.com/aunem/transpose - volumes: - - "./:/go/src/github.com/aunem/transpose" - expose: - - "80" - server: - build: - context: ./ - dockerfile: Dockerfile-integration - command: go run integration/server/main.go - working_dir: /go/src/github.com/aunem/transpose - volumes: - - "./:/go/src/github.com/aunem/transpose" - client: - build: - context: ./ - dockerfile: Dockerfile-integration - command: go run integration/client/main.go - links: - - transpose - working_dir: /go/src/github.com/aunem/transpose - volumes: - - "./:/go/src/github.com/aunem/transpose" - hydra: - image: oryd/hydra - volumes: - - hydravolume:/root - ports: - - "4444:4444" - - "4445:4445" - command: - host --dangerous-auto-logon --dangerous-force-http --disable-telemetry - environment: - - LOG_LEVEL=debug - - ISSUER=http://localhost:4444 - - CONSENT_URL=http://localhost:3000/consent - - DATABASE_URL=memory - - FORCE_ROOT_CLIENT_CREDENTIALS=admin:password - - SYSTEM_SECRET=youReallyNeedToChangeThis - restart: unless-stopped diff --git a/integration/test/main.go b/integration/test/main.go new file mode 100644 index 0000000..6043ab7 --- /dev/null +++ b/integration/test/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "net/http" + "os" + "time" +) + +func main() { + c := http.DefaultClient + fmt.Println("connecting to service...") + err := retry(30, 3*time.Second, func() error { + err0 := makeRequest(c) + return err0 + }) + if err != nil { + log.Fatal("could not connect to service: ", err) + } + fmt.Println("connected to service!") + fmt.Println("testing...") + for i := 0; i < 10; i++ { + err := makeRequest(c) + if err != nil { + log.Fatal(err) + } + time.Sleep(1 * time.Second) + } + os.Exit(0) +} + +func makeRequest(c *http.Client) error { + fmt.Println("making request...") + var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`) + r, err := http.NewRequest(http.MethodPost, "http://transpose:8080", bytes.NewBuffer(jsonStr)) + if err != nil { + return err + } + log.Printf("request: %+v", r) + resp, err := c.Do(r) + if err != nil { + return err + } + fmt.Printf("resp: %+v", resp) + return nil +} + +func retry(attempts int, sleep time.Duration, callback func() error) (err error) { + for i := 0; ; i++ { + err = callback() + if err == nil { + return + } + + if i >= (attempts - 1) { + break + } + + time.Sleep(sleep) + + log.Println("retrying after error:", err) + } + return fmt.Errorf("after %d attempts, last error: %s", attempts, err) +} diff --git a/pkg/template/listener/README_tpl.go b/pkg/template/listener/README_tpl.go new file mode 100644 index 0000000..c32fba9 --- /dev/null +++ b/pkg/template/listener/README_tpl.go @@ -0,0 +1,45 @@ +package listener + +var readmeTpl = ` + +# {{ .Name }} + +// Your overview here + +## Spec +` + "```yaml " + ` +apiVersion: alpha.aunem.io/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: {{ .Name }} + package: {{ .Pkg }} + spec: + port: 80 + ssl: false + + roundtrip: + name: myroundtrip + package: github.com/aunem/transpose-plugins/roundtrip/supermux + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +` + "```" + ` + +## Contexts Supported +* Http + +## Compatibility +// your compatibility data goes here + +## Dependencies +// your dependencies go here + +## Test +` + "`go test ./...`" diff --git a/pkg/template/listener/example_config_tpl.go b/pkg/template/listener/example_config_tpl.go new file mode 100644 index 0000000..f655158 --- /dev/null +++ b/pkg/template/listener/example_config_tpl.go @@ -0,0 +1,25 @@ +package listener + +var exampleTpl = `apiVersion: alpha.aunem.io/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: {{ .Name }} + package: {{ .Repo }} + spec: + port: 80 + ssl: false + + roundtrip: + name: myroundtrip + package: github.com/aunem/transpose-plugins/roundtrip/supermux + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +` diff --git a/pkg/template/listener/main_test_tpl.go b/pkg/template/listener/main_test_tpl.go new file mode 100644 index 0000000..2f91717 --- /dev/null +++ b/pkg/template/listener/main_test_tpl.go @@ -0,0 +1,12 @@ +package listener + +var mainTestTpl = ` +package main_test + +func TestMain(m *testing.M) { + m.Run() +} + +func TestListen(t *testing.T) { +} +` diff --git a/pkg/template/listener/main_tpl.go b/pkg/template/listener/main_tpl.go new file mode 100644 index 0000000..2af4041 --- /dev/null +++ b/pkg/template/listener/main_tpl.go @@ -0,0 +1,45 @@ +package listener + +var mainTpl = `package main + +import ( + "fmt" + "path" + + "github.com/aunem/transpose/pkg/context" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +type {{ .Name }}Plugin struct {} + +// ListenerPlugin exports the plugin struct +var ListenerPlugin {{ .Name }}Plugin + +// Spec exports the spec data +var Spec {{ .Name }}Spec + +func main() {} + +// Listen implements the listener plugin inerface +func (h *{{ .Name }}Listener) Listen(mw *middleware.Manager, rt *roundtrip.Manager) error { + // Implement listener code here +} + +func (p *{{ .Name }}Plugin) Init(spec interface{}) error { + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + err = yaml.Unmarshal(b, &Spec) + if err != nil { + return err + } + log.Debugf("loaded spec: %+v", Spec) + return nil +} + +func (p *{{ .Name }}Plugin) Stats() ([]byte, error) { + return nil, nil +} +` diff --git a/pkg/template/listener/manifest.go b/pkg/template/listener/manifest.go new file mode 100644 index 0000000..a495096 --- /dev/null +++ b/pkg/template/listener/manifest.go @@ -0,0 +1,10 @@ +package listener + +// Manifest is a map of file name to template string +var Manifest = map[string]string{ + "main.go": mainTpl, + "main_test.go": mainTestTpl, + "example-config.yaml": exampleTpl, + "README.md": readmeTpl, + "spec.go": specTpl, +} diff --git a/pkg/template/listener/spec_tpl.go b/pkg/template/listener/spec_tpl.go new file mode 100644 index 0000000..3e41b97 --- /dev/null +++ b/pkg/template/listener/spec_tpl.go @@ -0,0 +1,6 @@ +package listener + +var specTpl = `package main + +type {{ .Name }}Spec struct {} +` diff --git a/pkg/template/middleware/README_tpl.go b/pkg/template/middleware/README_tpl.go index 7c222ae..09040e5 100644 --- a/pkg/template/middleware/README_tpl.go +++ b/pkg/template/middleware/README_tpl.go @@ -8,7 +8,7 @@ var readmeTpl = ` ## Spec ` + "```yaml " + ` -apiVersion: alpha.aunem.com/v1 +apiVersion: alpha.aunem.io/v1 Kind: Transpose Metadata: name: myProxy @@ -17,7 +17,7 @@ spec: listener: name: mylistener package: github.com/aunem/transpose-plugins/listener/http - spec: + spec: port: 80 ssl: false diff --git a/pkg/template/middleware/example_config_tpl.go b/pkg/template/middleware/example_config_tpl.go index 0493bc7..b155c25 100644 --- a/pkg/template/middleware/example_config_tpl.go +++ b/pkg/template/middleware/example_config_tpl.go @@ -1,6 +1,6 @@ package middleware -var exampleTpl = `apiVersion: alpha.aunem.com/v1 +var exampleTpl = `apiVersion: alpha.aunem.io/v1 Kind: Transpose Metadata: name: myProxy diff --git a/pkg/template/roundtrip/README_tpl.go b/pkg/template/roundtrip/README_tpl.go new file mode 100644 index 0000000..d4141cc --- /dev/null +++ b/pkg/template/roundtrip/README_tpl.go @@ -0,0 +1,45 @@ +package roundtrip + +var readmeTpl = ` + +# {{ .Name }} + +// Your overview here + +## Spec +` + "```yaml " + ` +apiVersion: alpha.aunem.io/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: mylistener + package: github.com/aunem/transpose-plugins/listener/http + spec: + port: 80 + ssl: false + + roundtrip: + name: {{ .Name }} + package: {{ .Pkg }} + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +` + "```" + ` + +## Contexts Supported +* Http + +## Compatibility +// your compatibility data goes here + +## Dependencies +// your dependencies go here + +## Test +` + "`go test ./...`" diff --git a/pkg/template/roundtrip/example_config_tpl.go b/pkg/template/roundtrip/example_config_tpl.go new file mode 100644 index 0000000..6e07a9c --- /dev/null +++ b/pkg/template/roundtrip/example_config_tpl.go @@ -0,0 +1,25 @@ +package roundtrip + +var exampleTpl = `apiVersion: alpha.aunem.io/v1 +Kind: Transpose +Metadata: + name: myProxy + namespace: default +spec: + listener: + name: mylistener + package: github.com/aunem/transpose-plugins/listener/http + spec: + port: 80 + ssl: false + + roundtrip: + name: {{ .Name }} + package: {{ .Pkg }} + spec: + http: + - path: "/" + backend: + serviceName: myservice + servicePort: 80 +` diff --git a/pkg/template/roundtrip/main_test_tpl.go b/pkg/template/roundtrip/main_test_tpl.go new file mode 100644 index 0000000..d68ed98 --- /dev/null +++ b/pkg/template/roundtrip/main_test_tpl.go @@ -0,0 +1,12 @@ +package roundtrip + +var mainTestTpl = ` +package main_test + +func TestMain(m *testing.M) { + m.Run() +} + +func TestRoundtrip(t *testing.T) { +} +` diff --git a/pkg/template/roundtrip/main_tpl.go b/pkg/template/roundtrip/main_tpl.go new file mode 100644 index 0000000..bef51d0 --- /dev/null +++ b/pkg/template/roundtrip/main_tpl.go @@ -0,0 +1,44 @@ +package roundtrip + +var mainTpl = `package main + +import ( + "fmt" + "path" + + "github.com/aunem/transpose/pkg/context" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +type {{ .Name }}Plugin struct {} + +// RoundtripPlugin exports the plugin struct +var RoundtripPlugin {{ .Name }}Plugin + +// Spec exports the spec data +var Spec {{ .Name }}Spec + +func main() {} + +func (s *{{ .Name }}Plugin) Roundtrip(req context.Request) (context.Response, error) { + // your roundtrip code goes here +} + +func (p *{{ .Name }}Plugin) Init(spec interface{}) error { + b, err := yaml.Marshal(spec) + if err != nil { + return err + } + err = yaml.Unmarshal(b, &Spec) + if err != nil { + return err + } + log.Debugf("loaded spec: %+v", Spec) + return nil +} + +func (p *{{ .Name }}Plugin) Stats() ([]byte, error) { + return nil, nil +} +` diff --git a/pkg/template/roundtrip/manifest.go b/pkg/template/roundtrip/manifest.go new file mode 100644 index 0000000..2ed7649 --- /dev/null +++ b/pkg/template/roundtrip/manifest.go @@ -0,0 +1,10 @@ +package roundtrip + +// Manifest is a map of file name to template string +var Manifest = map[string]string{ + "main.go": mainTpl, + "main_test.go": mainTestTpl, + "example-config.yaml": exampleTpl, + "README.md": readmeTpl, + "spec.go": specTpl, +} diff --git a/pkg/template/roundtrip/spec_tpl.go b/pkg/template/roundtrip/spec_tpl.go new file mode 100644 index 0000000..bfd9aa8 --- /dev/null +++ b/pkg/template/roundtrip/spec_tpl.go @@ -0,0 +1,6 @@ +package roundtrip + +var specTpl = `package main + +type {{ .Name }}Spec struct {} +` diff --git a/pkg/template/template.go b/pkg/template/template.go index 4b77087..7de54fb 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -5,9 +5,13 @@ import ( "os" tpl "text/template" + list "github.com/aunem/transpose/pkg/template/listener" mw "github.com/aunem/transpose/pkg/template/middleware" + rt "github.com/aunem/transpose/pkg/template/roundtrip" ) +//TODO: test this + // TemplateDir represents a templated directory type TemplateDir string @@ -49,9 +53,37 @@ func (p *Plugin) Template() error { } } case "listener": - + for k, v := range list.Manifest { + t, err := tpl.New("tpl").Parse(v) + if err != nil { + return err + } + f, err := os.Create(k) + if err != nil { + return err + } + defer f.Close() + t.Execute(f, p) + if err != nil { + return err + } + } case "roundtrip": - + for k, v := range rt.Manifest { + t, err := tpl.New("tpl").Parse(v) + if err != nil { + return err + } + f, err := os.Create(k) + if err != nil { + return err + } + defer f.Close() + t.Execute(f, p) + if err != nil { + return err + } + } default: return fmt.Errorf("plugin typ uknown: %+v", p.Typ) } diff --git a/pkg/utils/retry.go b/pkg/utils/retry.go new file mode 100644 index 0000000..83dffb5 --- /dev/null +++ b/pkg/utils/retry.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "log" + "time" +) + +// Retry is a basic retry function +func Retry(attempts int, sleep time.Duration, callback func() error) (err error) { + for i := 0; ; i++ { + err = callback() + if err == nil { + return + } + + if i >= (attempts - 1) { + break + } + + time.Sleep(sleep) + + log.Println("retrying after error:", err) + } + return fmt.Errorf("after %d attempts, last error: %s", attempts, err) +} From ba7d6042d74a382d8c86a55da87a191997e72dc4 Mon Sep 17 00:00:00 2001 From: grillz Date: Sat, 7 Apr 2018 13:48:42 -0600 Subject: [PATCH 7/7] debug ci --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 458a549..a05bd14 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,6 +29,7 @@ jobs: - run: name: Install deps command: | + ls make deps - run: