From 9e0c60c8980c8b8defdd2bd5d8874c475ec18ba5 Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Wed, 22 Jul 2020 09:59:31 -0400 Subject: [PATCH] feat: add docker support and support a workdir CLI option in case your running in a kube pod with the config directory readonly (due to it being mounted from a ConfigMap). --- docker/.gitignore | 2 + docker/Dockerfile | 17 +++++++ docker/build.sh | 15 ++++++ go.mod | 7 ++- go.sum | 5 +- graphql-gw.yaml | 6 ++- internal/cmd/config/add/upstream/cmd.go | 4 +- internal/cmd/config/cmd.go | 13 +++-- internal/cmd/root/cmd.go | 33 ++++++++++++ internal/cmd/serve/cmd.go | 68 ++++++++++++++++++++++--- internal/gateway/gateway.go | 10 ++-- internal/gateway/schema_load.go | 4 +- 12 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 docker/.gitignore create mode 100644 docker/Dockerfile create mode 100755 docker/build.sh diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..68a12c4 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +bin +etc \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..464aad6 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:alpine as build +RUN apk --no-cache add ca-certificates + +# In case we want to build the gateway in docker.... +#WORKDIR /go/src/app +#COPY . . +#RUN CGO_ENABLED=0 go-wrapper install -ldflags '-extldflags "-static"' + +FROM scratch +# FROM alpine +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +ADD bin /bin +ADD etc /etc +WORKDIR /etc/graphql-gw +EXPOSE 8080/tcp +ENTRYPOINT ["/bin/graphql-gw", "serve"] \ No newline at end of file diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..68b8bca --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e +cd -P $(dirname "${BASH_SOURCE[0]}") + +mkdir -p bin || true 2> /dev/null +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/graphql-gw ../main.go + +mkdir -p etc/graphql-gw 2> /dev/null || true +cd etc/graphql-gw +rm graphql-gw.yaml 2> /dev/null || true +go run ../../../main.go config init +cd - + +docker build -t "chirino/graphql-gw" . +# docker push chirino/graphql-gw diff --git a/go.mod b/go.mod index a4c94c3..f262e1d 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/chirino/graphql-gw require ( - github.com/chirino/graphql v0.0.0-20200620205252-3aa1055298c1 + github.com/chirino/graphql v0.0.0-20200723150749-b1992f46a318 github.com/chirino/graphql-4-apis v0.0.0-20200622132210-66059999b694 github.com/chirino/hawtgo v0.0.1 github.com/fsnotify/fsnotify v1.4.9 @@ -10,10 +10,13 @@ require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.1 github.com/golang/protobuf v1.3.1 + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd github.com/spf13/cobra v1.0.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.5.1 golang.org/x/sys v0.0.0-20200620081246-981b61492c35 // indirect google.golang.org/grpc v1.21.0 @@ -23,4 +26,6 @@ require ( go 1.13 //replace github.com/chirino/graphql => ../graphql +// replace github.com/chirino/graphql => ../graphql + //replace github.com/chirino/graphql-4-apis => ../graphql-4-apis diff --git a/go.sum b/go.sum index 32e9bbc..2e7b525 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,9 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chirino/graphql v0.0.0-20200620205252-3aa1055298c1 h1:9R1VO7zZ/jDc2uqFp7FjvwwruVGfR6TO3TaeVyDCU5I= github.com/chirino/graphql v0.0.0-20200620205252-3aa1055298c1/go.mod h1:QJryzmxY+8v+nP7+HjPsMxT4q+tfZZq387ZdG0md+kU= +github.com/chirino/graphql v0.0.0-20200723150749-b1992f46a318 h1:3BfsEyOx1rmoL9W23etW8sJ2oQV+hMa0TAFUO6ewReM= +github.com/chirino/graphql v0.0.0-20200723150749-b1992f46a318/go.mod h1:QJryzmxY+8v+nP7+HjPsMxT4q+tfZZq387ZdG0md+kU= github.com/chirino/graphql-4-apis v0.0.0-20200622132210-66059999b694 h1:WmZ0Y9xkgCft6P4DdIZ5qBwQWlMkQH2wiUy4JcsXPfE= github.com/chirino/graphql-4-apis v0.0.0-20200622132210-66059999b694/go.mod h1:JUqXBL6BnmaECzXkZnSAunwgAccI1MY0SYVEGuxSX3A= github.com/chirino/hawtgo v0.0.1 h1:BwUkArpv32ZDK5QFnG1RysagrKe82TsagXxi2Ys2flA= @@ -100,6 +101,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= diff --git a/graphql-gw.yaml b/graphql-gw.yaml index 3ef10f0..8abc32b 100644 --- a/graphql-gw.yaml +++ b/graphql-gw.yaml @@ -1,6 +1,10 @@ # Configure the host and port the service will listen on listen: 0.0.0.0:8080 +#policy-agent: +# insecure-client: true +# address: 127.0.0.1:10000 + # # Configure the GraphQL upstream servers you will be accessing upstreams: @@ -17,7 +21,6 @@ upstreams: url: https://weather.com/swagger-docs/sun/v1/sunV1DailyForecast.json api: api-key: please configure me - types: - name: Query actions: @@ -38,6 +41,7 @@ types: upstream: weather query: query {} + - name: AniCharacter actions: # mounts the root anilist query to the anime field diff --git a/internal/cmd/config/add/upstream/cmd.go b/internal/cmd/config/add/upstream/cmd.go index 4b8374e..ddb2491 100644 --- a/internal/cmd/config/add/upstream/cmd.go +++ b/internal/cmd/config/add/upstream/cmd.go @@ -107,8 +107,8 @@ func run(cmd *cobra.Command, args []string) { } // Store it's schema - os.MkdirAll(filepath.Join(c.ConfigDirectory, "upstreams"), 0755) - upstreamSchemaFile := filepath.Join(c.ConfigDirectory, "upstreams", upstreamName+".graphql") + os.MkdirAll(filepath.Join(c.WorkDirectory, "upstreams"), 0755) + upstreamSchemaFile := filepath.Join(c.WorkDirectory, "upstreams", upstreamName+".graphql") err = ioutil.WriteFile(upstreamSchemaFile, []byte(upstreamSchema.String()), 0644) if err != nil { log.Fatalf(root.Verbosity, err) diff --git a/internal/cmd/config/cmd.go b/internal/cmd/config/cmd.go index 5b9a231..63b0ef9 100644 --- a/internal/cmd/config/cmd.go +++ b/internal/cmd/config/cmd.go @@ -25,12 +25,14 @@ var ( Short: "Modifies the gateway configuration", PersistentPreRunE: PreRunLoad, } - File string - Value *Config + File string + WorkDir string + Value *Config ) func init() { Command.PersistentFlags().StringVar(&File, "config", "graphql-gw.yaml", "path to the config file to modify") + Command.PersistentFlags().StringVar(&WorkDir, "workdir", "", "working to write files to in dev mode. (default to the directory the config file is in)") root.Command.AddCommand(Command) } @@ -51,7 +53,12 @@ func Load(config *Config) error { return errors.Wrapf(err, "parsing yaml of: %s.", File) } - config.ConfigDirectory = filepath.Dir(File) + if WorkDir == "" { + config.WorkDirectory = filepath.Dir(File) + } else { + config.WorkDirectory = WorkDir + } + config.Log = gateway.SimpleLog if config.Upstreams == nil { diff --git a/internal/cmd/root/cmd.go b/internal/cmd/root/cmd.go index 1f74564..2e3aa25 100644 --- a/internal/cmd/root/cmd.go +++ b/internal/cmd/root/cmd.go @@ -3,8 +3,11 @@ package root import ( "fmt" "os" + "strings" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) var ( @@ -23,6 +26,9 @@ var ( func init() { Command.PersistentFlags().BoolVar(&Verbose, "verbose", false, "enables increased verbosity") + viper.SetEnvPrefix("GRAPHQL_GW") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() } func Main() { @@ -31,3 +37,30 @@ func Main() { os.Exit(1) } } + +func IntBind(f *pflag.FlagSet, name string, defaultValue int, usage string) func() int { + f.Int(name, defaultValue, usage) + viper.SetDefault(name, defaultValue) + viper.BindPFlag(name, f.Lookup(name)) + return func() int { + return viper.GetInt(name) + } +} + +func StringBind(f *pflag.FlagSet, name string, defaultValue string, usage string) func() string { + f.String(name, defaultValue, usage) + viper.SetDefault(name, defaultValue) + viper.BindPFlag(name, f.Lookup(name)) + return func() string { + return viper.GetString(name) + } +} + +func BoolBind(f *pflag.FlagSet, name string, defaultValue bool, usage string) func() bool { + f.Bool(name, defaultValue, usage) + viper.SetDefault(name, defaultValue) + viper.BindPFlag(name, f.Lookup(name)) + return func() bool { + return viper.GetBool(name) + } +} diff --git a/internal/cmd/serve/cmd.go b/internal/cmd/serve/cmd.go index d7d256d..1f5706e 100644 --- a/internal/cmd/serve/cmd.go +++ b/internal/cmd/serve/cmd.go @@ -2,6 +2,7 @@ package serve import ( "fmt" + "io" "net/http" "os" "os/signal" @@ -30,12 +31,13 @@ var ( Run: run, PersistentPreRunE: config.PreRunLoad, } - Production = false + Production func() bool ) func init() { Command.Flags().StringVar(&config.File, "config", "graphql-gw.yaml", "path to the config file to load") - Command.Flags().BoolVar(&Production, "production", false, "when true, the server will not download and store schemas from remote graphql endpoints.") + Command.Flags().StringVar(&config.WorkDir, "workdir", "", "working to write files to in dev mode. (default to the directory the config file is in)") + Production = root.BoolBind(Command.Flags(), "production", false, "when true, the server will not download and store schemas from remote graphql endpoints.") root.Command.AddCommand(Command) } @@ -44,6 +46,37 @@ func run(_ *cobra.Command, _ []string) { lastConfig.Log = gateway.TimestampedLog log := lastConfig.Log + if Production() { + if config.WorkDir != filepath.Dir(config.File) { + log.Fatalf("work directory cannot be configured in production mode") + } + } else { + + if config.WorkDir != filepath.Dir(config.File) { + os.MkdirAll(config.WorkDir, 0755) + + source := config.File + target := filepath.Join(config.WorkDir, "graphql-gw.yaml") + if _, err := os.Stat(target); err != nil && os.IsNotExist(err) { + err := copy(source, target) + if err != nil { + log.Fatal(err) + } + } + + // so we update and watch the config file in the work dir. + config.File = target + + // but watch the original for changes... + watchFile(source, func(in fsnotify.Event) { + err := copy(source, target) + if err != nil { + log.Printf("Could not copy the config file to the work directory: %s", err) + } + }) + } + } + err := startServer(&lastConfig) if err != nil { log.Fatalf("could not start the sever: "+root.Verbosity, err) @@ -82,7 +115,7 @@ func run(_ *cobra.Command, _ []string) { lastConfig = nextConfig } - if !Production { + if !Production() { watchFile(config.File, func(in fsnotify.Event) { log.Println("restarting due to configuration change:", in.Name) restart() @@ -165,20 +198,41 @@ func startServer(config *config.Config) error { return mountGatewayOnHttpServer(config) } +func copy(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} + func mountGatewayOnHttpServer(c *config.Config) (err error) { + c.Gateway, err = gateway.New(c.Config) if err != nil { return err } gatewayHandler := gateway.CreateHttpHandler(c.Gateway.ServeGraphQLStream).(*httpgql.Handler) // Enable pretty printed json results when in dev mode. - if !Production { + if !Production() { gatewayHandler.Indent = " " } graphqlURL := fmt.Sprintf("%s/graphql", c.Server.URL) r := chi.NewRouter() r.Use(middleware.Logger) - if !Production { + if !Production() { r.Mount("/", http.FileServer(assets.FileSystem)) r.Mount("/admin", admin.CreateHttpHandler()) } @@ -186,7 +240,7 @@ func mountGatewayOnHttpServer(c *config.Config) (err error) { r.Handle("/graphiql", graphiql.New(graphqlURL, true)) c.Server.Config.Handler = r c.Config.Log.Printf("GraphQL endpoint is running at %s", graphqlURL) - if Production { + if Production() { c.Config.Log.Printf("Gateway GraphQL IDE is running at %s/graphiql", c.Server.URL) } else { c.Config.Log.Printf("Gateway Admin UI and GraphQL IDE is running at %s", c.Server.URL) @@ -209,7 +263,7 @@ func postProcess(config *config.Config) { config.Listen = "0.0.0.0:8080" } - if Production { + if Production() { config.DisableSchemaDownloads = true config.EnabledSchemaStorage = false } else { diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 170c1bb..ad6a3d1 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -39,7 +39,7 @@ type PolicyAgentConfig struct { } type Config struct { - ConfigDirectory string `yaml:"-"` + WorkDirectory string `yaml:"-"` Log *log.Logger `yaml:"-"` DisableSchemaDownloads bool `yaml:"disable-schema-downloads,omitempty"` EnabledSchemaStorage bool `yaml:"enable-schema-storage,omitempty"` @@ -68,11 +68,11 @@ func New(config Config) (*Gateway, error) { if config.Log == nil { config.Log = NoLog } - if config.ConfigDirectory == "" { - config.ConfigDirectory = "." + if config.WorkDirectory == "" { + config.WorkDirectory = "." } if config.EnabledSchemaStorage { - os.MkdirAll(filepath.Join(config.ConfigDirectory, "upstreams"), 0755) + os.MkdirAll(filepath.Join(config.WorkDirectory, "upstreams"), 0755) } fieldResolver := resolvers.TypeAndFieldResolver{} @@ -309,7 +309,7 @@ func HaveUpstreamSchemaChanged(config Config) (bool, error) { for eid, upstream := range upstreams { // Load the old stored schema. - upstreamSchemaFile := filepath.Join(config.ConfigDirectory, "upstreams", eid+".graphql") + upstreamSchemaFile := filepath.Join(config.WorkDirectory, "upstreams", eid+".graphql") data, err := ioutil.ReadFile(upstreamSchemaFile) if err != nil { return false, err diff --git a/internal/gateway/schema_load.go b/internal/gateway/schema_load.go index b202a9c..5843dce 100644 --- a/internal/gateway/schema_load.go +++ b/internal/gateway/schema_load.go @@ -19,7 +19,7 @@ func loadEndpointSchema(config Config, upstream *upstreamServer) (*schema.Schema return Parse(schemaText) } - upstreamSchemaFile := filepath.Join(config.ConfigDirectory, "upstreams", upstream.id+".graphql") + upstreamSchemaFile := filepath.Join(config.WorkDirectory, "upstreams", upstream.id+".graphql") upstreamSchemaFileExists := false if stat, err := os.Stat(upstreamSchemaFile); err == nil && !stat.IsDir() { upstreamSchemaFileExists = true @@ -69,7 +69,7 @@ func downloadSchema(config Config, upstream *upstreamServer) (*schema.Schema, er // We may need to store it if it succeeded. if err == nil && config.EnabledSchemaStorage { - upstreamSchemaFile := filepath.Join(config.ConfigDirectory, "upstreams", upstream.id+".graphql") + upstreamSchemaFile := filepath.Join(config.WorkDirectory, "upstreams", upstream.id+".graphql") err := ioutil.WriteFile(upstreamSchemaFile, []byte(s.String()), 0644) if err != nil { return nil, errors.Wrap(err, "could not update schema")