This commit is contained in:
Thomas Müller
2019-12-04 17:15:11 +01:00
commit d9e7f1a986
64 changed files with 6442 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
package command
import (
"fmt"
"net/http"
"github.com/micro/cli"
"github.com/micro/go-micro/util/log"
"github.com/owncloud/ocis-graph/pkg/config"
"github.com/owncloud/ocis-graph/pkg/flagset"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) cli.Command {
return cli.Command{
Name: "health",
Usage: "Check health status",
Flags: flagset.HealthWithConfig(cfg),
Action: func(c *cli.Context) error {
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
log.Fatalf("Failed to request health check: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
log.Fatalf("Health check responds with [%d]", resp.StatusCode)
}
log.Debugf("Health got good state with [%d]", resp.StatusCode)
return nil
},
}
}
+107
View File
@@ -0,0 +1,107 @@
package command
import (
"os"
"strings"
"github.com/micro/cli"
"github.com/micro/go-micro/util/log"
"github.com/owncloud/ocis-graph/pkg/config"
"github.com/owncloud/ocis-graph/pkg/flagset"
"github.com/owncloud/ocis-graph/pkg/version"
"github.com/spf13/viper"
)
// Execute is the entry point for the ocis-graph command.
func Execute() error {
cfg := config.New()
app := &cli.App{
Name: "ocis-graph",
Version: version.String,
Usage: "Example service for Reva/oCIS",
Compiled: version.Compiled(),
Authors: []cli.Author{
{
Name: "ownCloud GmbH",
Email: "support@owncloud.com",
},
},
Flags: flagset.RootWithConfig(cfg),
Before: func(c *cli.Context) error {
NewLogger(cfg)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.SetEnvPrefix("graph")
viper.AutomaticEnv()
if c.IsSet("config-file") {
viper.SetConfigFile(c.String("config-file"))
} else {
viper.SetConfigName("graph")
viper.AddConfigPath("/etc/ocis")
viper.AddConfigPath("$HOME/.ocis")
viper.AddConfigPath("./config")
}
if err := viper.ReadInConfig(); err != nil {
switch err.(type) {
case viper.ConfigFileNotFoundError:
log.Info("Continue without config")
case viper.UnsupportedConfigError:
log.Fatalf("Unsupported config type: %w", err)
default:
log.Fatalf("Failed to read config: %w", err)
}
}
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatalf("Failed to parse config: %w", err)
}
return nil
},
Commands: []cli.Command{
Server(cfg),
Health(cfg),
},
}
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
cli.VersionFlag = &cli.BoolFlag{
Name: "version,v",
Usage: "Print the version",
}
return app.Run(os.Args)
}
func NewLogger(cfg *config.Config) {
switch strings.ToLower(cfg.Log.Level) {
case "fatal":
log.SetLevel(log.LevelFatal)
case "error":
log.SetLevel(log.LevelError)
case "info":
log.SetLevel(log.LevelInfo)
case "warn":
log.SetLevel(log.LevelWarn)
case "debug":
log.SetLevel(log.LevelDebug)
case "trace":
log.SetLevel(log.LevelTrace)
default:
log.SetLevel(log.LevelInfo)
}
log.Name("graph")
}
+185
View File
@@ -0,0 +1,185 @@
package command
import (
"context"
"os"
"os/signal"
"time"
"contrib.go.opencensus.io/exporter/jaeger"
"contrib.go.opencensus.io/exporter/ocagent"
"contrib.go.opencensus.io/exporter/zipkin"
"github.com/micro/cli"
"github.com/micro/go-micro/util/log"
"github.com/oklog/run"
openzipkin "github.com/openzipkin/zipkin-go"
zipkinhttp "github.com/openzipkin/zipkin-go/reporter/http"
"github.com/owncloud/ocis-graph/pkg/config"
"github.com/owncloud/ocis-graph/pkg/flagset"
"github.com/owncloud/ocis-graph/pkg/server/debug"
"github.com/owncloud/ocis-graph/pkg/server/http"
"go.opencensus.io/stats/view"
"go.opencensus.io/trace"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) cli.Command {
return cli.Command{
Name: "server",
Usage: "Start integrated server",
Flags: flagset.ServerWithConfig(cfg),
Action: func(c *cli.Context) error {
if cfg.Tracing.Enabled {
switch t := cfg.Tracing.Type; t {
case "agent":
exporter, err := ocagent.NewExporter(
ocagent.WithReconnectionPeriod(5*time.Second),
ocagent.WithAddress(cfg.Tracing.Endpoint),
ocagent.WithServiceName(cfg.Tracing.Service),
)
if err != nil {
log.Error(
"Failed to create agent tracing on [%s] endpoint and [%s] collector: %w",
cfg.Tracing.Endpoint,
cfg.Tracing.Collector,
err,
)
return err
}
trace.RegisterExporter(exporter)
view.RegisterExporter(exporter)
case "jaeger":
exporter, err := jaeger.NewExporter(
jaeger.Options{
AgentEndpoint: cfg.Tracing.Endpoint,
CollectorEndpoint: cfg.Tracing.Collector,
ServiceName: cfg.Tracing.Service,
},
)
if err != nil {
log.Error(
"Failed to create jaeger tracing on [%s] endpoint and [%s] collector: %w",
cfg.Tracing.Endpoint,
cfg.Tracing.Collector,
err,
)
return err
}
trace.RegisterExporter(exporter)
case "zipkin":
endpoint, err := openzipkin.NewEndpoint(
cfg.Tracing.Service,
cfg.Tracing.Endpoint,
)
if err != nil {
log.Error(
"Failed to create zipkin tracing on [%s] endpoint and [%s] collector: %w",
cfg.Tracing.Endpoint,
cfg.Tracing.Collector,
err,
)
return err
}
exporter := zipkin.NewExporter(
zipkinhttp.NewReporter(
cfg.Tracing.Collector,
),
endpoint,
)
trace.RegisterExporter(exporter)
default:
log.Warnf("Unknown tracing backend [%s]", t)
}
trace.ApplyConfig(
trace.Config{
DefaultSampler: trace.AlwaysSample(),
},
)
} else {
log.Debug("Tracing is not enabled")
}
var (
gr = run.Group{}
ctx, cancel = context.WithCancel(context.Background())
)
{
server, err := http.Server(
http.Context(ctx),
http.Config(cfg),
)
if err != nil {
log.Errorf("Server [http] failed to initialize: %w", err)
return err
}
gr.Add(func() error {
return server.Run()
}, func(_ error) {
log.Infof("Server [http] shutting down")
cancel()
})
}
{
server, err := debug.Server(
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
log.Errorf("Server [debug] failed to initialize: %w", err)
return err
}
gr.Add(func() error {
return server.ListenAndServe()
}, func(_ error) {
ctx, timeout := context.WithTimeout(ctx, 5*time.Second)
defer timeout()
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Errorf("Server [debug] shutdown failed: %w", err)
} else {
log.Infof("Server [debug] shutting down")
}
})
}
{
stop := make(chan os.Signal, 1)
gr.Add(func() error {
signal.Notify(stop, os.Interrupt)
<-stop
return nil
}, func(err error) {
close(stop)
cancel()
})
}
return gr.Run()
},
}
}
+43
View File
@@ -0,0 +1,43 @@
package config
type Log struct {
Level string
}
type Debug struct {
Addr string
Token string
Pprof bool
Zpages bool
}
type HTTP struct {
Addr string
Root string
}
type GRPC struct {
Addr string
Root string
}
type Tracing struct {
Enabled bool
Type string
Endpoint string
Collector string
Service string
}
type Config struct {
File string
Log Log
Debug Debug
HTTP HTTP
GRPC GRPC
Tracing Tracing
}
func New() *Config {
return &Config{}
}
+112
View File
@@ -0,0 +1,112 @@
package flagset
import (
"github.com/micro/cli"
"github.com/owncloud/ocis-graph/pkg/config"
)
// RootWithConfig applies cfg to the root flagset
func RootWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "config-file",
Value: "",
Usage: "Path to config file",
EnvVar: "GRAPH_CONFIG_FILE",
Destination: &cfg.File,
},
&cli.StringFlag{
Name: "log-level",
Value: "info",
Usage: "Set logging level",
EnvVar: "GRAPH_LOG_LEVEL",
Destination: &cfg.Log.Level,
},
}
}
// HealthWithConfig applies cfg to the root flagset
func HealthWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "debug-addr",
Value: "0.0.0.0:8390",
Usage: "Address to debug endpoint",
EnvVar: "GRAPH_DEBUG_ADDR",
Destination: &cfg.Debug.Addr,
},
}
}
// ServerWithConfig applies cfg to the root flagset
func ServerWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: "tracing-enabled",
Usage: "Enable sending traces",
EnvVar: "GRAPH_TRACING_ENABLED",
Destination: &cfg.Tracing.Enabled,
},
&cli.StringFlag{
Name: "tracing-type",
Value: "jaeger",
Usage: "Tracing backend type",
EnvVar: "GRAPH_TRACING_TYPE",
Destination: &cfg.Tracing.Type,
},
&cli.StringFlag{
Name: "tracing-endpoint",
Value: "",
Usage: "Endpoint for the agent",
EnvVar: "GRAPH_TRACING_ENDPOINT",
Destination: &cfg.Tracing.Endpoint,
},
&cli.StringFlag{
Name: "tracing-collector",
Value: "",
Usage: "Endpoint for the collector",
EnvVar: "GRAPH_TRACING_COLLECTOR",
Destination: &cfg.Tracing.Collector,
},
&cli.StringFlag{
Name: "tracing-service",
Value: "graph",
Usage: "Service name for tracing",
EnvVar: "GRAPH_TRACING_SERVICE",
Destination: &cfg.Tracing.Service,
},
&cli.StringFlag{
Name: "debug-addr",
Value: "0.0.0.0:8390",
Usage: "Address to bind debug server",
EnvVar: "GRAPH_DEBUG_ADDR",
Destination: &cfg.Debug.Addr,
},
&cli.StringFlag{
Name: "debug-token",
Value: "",
Usage: "Token to grant metrics access",
EnvVar: "GRAPH_DEBUG_TOKEN",
Destination: &cfg.Debug.Token,
},
&cli.BoolFlag{
Name: "debug-pprof",
Usage: "Enable pprof debugging",
EnvVar: "GRAPH_DEBUG_PPROF",
Destination: &cfg.Debug.Pprof,
},
&cli.BoolFlag{
Name: "debug-zpages",
Usage: "Enable zpages debugging",
EnvVar: "GRAPH_DEBUG_ZPAGES",
Destination: &cfg.Debug.Zpages,
},
&cli.StringFlag{
Name: "http-addr",
Value: "0.0.0.0:8380",
Usage: "Address to bind http server",
EnvVar: "GRAPH_HTTP_ADDR",
Destination: &cfg.HTTP.Addr,
},
}
}
+60
View File
@@ -0,0 +1,60 @@
package middleware
import (
"net/http"
"time"
"github.com/owncloud/ocis-graph/pkg/version"
)
// Cache writes required cache headers to all requests.
func Cache(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
next.ServeHTTP(w, r)
})
}
// Cors writes required cors headers to all requests.
func Cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "OPTIONS" {
next.ServeHTTP(w, r)
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
w.Header().Set("Allow", "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.WriteHeader(http.StatusOK)
}
})
}
// Secure writes required access headers to all requests.
func Secure(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1; mode=block")
if r.TLS != nil {
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
}
next.ServeHTTP(w, r)
})
}
// Version writes the current version to the headers.
func Version(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-graph-VERSION", version.String)
next.ServeHTTP(w, r)
})
}
+27
View File
@@ -0,0 +1,27 @@
package middleware
import (
"github.com/owncloud/ocis-graph/pkg/config"
)
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
type Option func(o *Options)
type Options struct {
Config *config.Config
}
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+18
View File
@@ -0,0 +1,18 @@
package middleware
import (
"net/http"
"github.com/tomasen/realip"
)
// RealIP is a middleware that sets a http.Request RemoteAddr.
func RealIP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ip := realip.RealIP(r); ip != "" {
r.RemoteAddr = ip
}
next.ServeHTTP(w, r)
})
}
+12
View File
@@ -0,0 +1,12 @@
package middleware
import (
"net/http"
"github.com/ascarter/requestid"
)
// RequestID is a convenient middleware to inject a request id.
func RequestID(next http.Handler) http.Handler {
return requestid.RequestIDHandler(next)
}
+37
View File
@@ -0,0 +1,37 @@
package middleware
import (
"errors"
"net/http"
)
var (
// ErrInvalidToken is returned when the request token is invalid.
ErrInvalidToken = errors.New("invalid or missing token")
)
// Token provides a middleware to check access secured by a static token.
func Token(token string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token == "" {
next.ServeHTTP(w, r)
return
}
header := r.Header.Get("Authorization")
if header == "" {
http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized)
return
}
if header != "Bearer "+token {
http.Error(w, ErrInvalidToken.Error(), http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
+36
View File
@@ -0,0 +1,36 @@
package debug
import (
"context"
"github.com/owncloud/ocis-graph/pkg/config"
)
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
type Option func(o *Options)
type Options struct {
Context context.Context
Config *config.Config
}
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+76
View File
@@ -0,0 +1,76 @@
package debug
import (
"io"
"net/http"
"net/http/pprof"
"github.com/justinas/alice"
"github.com/micro/go-micro/util/log"
"github.com/owncloud/ocis-graph/pkg/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opencensus.io/zpages"
)
type Handler struct {
*http.ServeMux
}
func (h *Handler) healthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, http.StatusText(http.StatusOK))
}
func (h *Handler) readyz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
io.WriteString(w, http.StatusText(http.StatusOK))
}
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
log.Infof("Server [debug] listening on [%s]", options.Config.Debug.Addr)
mux := http.NewServeMux()
handler := &Handler{mux}
handler.Handle("/metrics", alice.New(
middleware.Token(
options.Config.Debug.Token,
),
).Then(
promhttp.Handler(),
))
handler.HandleFunc("/healthz", handler.healthz)
handler.HandleFunc("/readyz", handler.readyz)
if options.Config.Debug.Pprof {
handler.HandleFunc("/debug/pprof/", pprof.Index)
handler.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
handler.HandleFunc("/debug/pprof/profile", pprof.Profile)
handler.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
handler.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
if options.Config.Debug.Zpages {
zpages.Handle(mux, "/debug")
}
return &http.Server{
Addr: options.Config.Debug.Addr,
Handler: alice.New(
middleware.RealIP,
middleware.RequestID,
middleware.Cache,
middleware.Cors,
middleware.Secure,
middleware.Version,
).Then(
handler,
),
}, nil
}
+36
View File
@@ -0,0 +1,36 @@
package http
import (
"context"
"github.com/owncloud/ocis-graph/pkg/config"
)
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
type Option func(o *Options)
type Options struct {
Context context.Context
Config *config.Config
}
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+39
View File
@@ -0,0 +1,39 @@
package http
import (
"time"
"github.com/micro/go-micro/util/log"
"github.com/micro/go-micro/web"
"github.com/owncloud/ocis-graph/pkg/config"
"github.com/owncloud/ocis-graph/pkg/flagset"
"github.com/owncloud/ocis-graph/pkg/version"
)
func Server(opts ...Option) (web.Service, error) {
options := newOptions(opts...)
log.Infof("Server [http] listening on [%s]", options.Config.HTTP.Addr)
// &cli.StringFlag{
// Name: "http-addr",
// Value: "0.0.0.0:8380",
// Usage: "Address to bind http server",
// EnvVar: "GRAPH_HTTP_ADDR",
// Destination: &cfg.HTTP.Addr,
// },
service := web.NewService(
web.Name("go.micro.web.graph"),
web.Version(version.String),
web.RegisterTTL(time.Second*30),
web.RegisterInterval(time.Second*10),
web.Context(options.Context),
web.Flags(append(
flagset.RootWithConfig(config.New()),
flagset.ServerWithConfig(config.New())...,
)...),
)
service.Init()
return service, nil
}
+18
View File
@@ -0,0 +1,18 @@
package version
import (
"time"
)
var (
// String gets defined by the build system.
String = "0.0.0"
// Date indicates the build date.
Date = "00000000"
)
func Compiled() time.Time {
t, _ := time.Parse("20060102", Date)
return t
}