rename folder extensions -> services

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-06-27 14:05:36 +02:00
parent 1aea93d8cd
commit 78064e6bab
926 changed files with 0 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package assets
import (
"net/http"
"github.com/owncloud/ocis/v2/extensions/web"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/assetsfs"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// New returns a new http filesystem to serve assets.
func New(opts ...Option) http.FileSystem {
options := newOptions(opts...)
return assetsfs.New(web.Assets, options.Config.Asset.Path, options.Logger)
}
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+80
View File
@@ -0,0 +1,80 @@
package assets
import (
"bytes"
"golang.org/x/net/html"
"io"
"mime"
"net/http"
"path"
"path/filepath"
)
type fileServer struct {
root http.FileSystem
}
func FileServer(root http.FileSystem) http.Handler {
return &fileServer{root}
}
func (f *fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
upath := path.Clean(path.Join("/", r.URL.Path))
r.URL.Path = upath
asset, err := f.root.Open(upath)
if err != nil {
r.URL.Path = "/index.html"
f.ServeHTTP(w, r)
return
}
defer asset.Close()
s, _ := asset.Stat()
if s.IsDir() {
r.URL.Path = "/index.html"
f.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(s.Name())))
buf := new(bytes.Buffer)
switch s.Name() {
case "index.html", "oidc-callback.html", "oidc-silent-redirect.html":
_ = withBase(buf, asset, "/")
default:
_, _ = buf.ReadFrom(asset)
}
_, _ = w.Write(buf.Bytes())
}
func withBase(w io.Writer, r io.Reader, base string) error {
doc, _ := html.Parse(r)
var parse func(*html.Node)
parse = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "head" {
n.InsertBefore(&html.Node{
Type: html.ElementNode,
Data: "base",
Attr: []html.Attribute{
{
Key: "href",
Val: base,
},
},
}, n.FirstChild)
return
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
parse(c)
}
}
parse(doc)
return html.Render(w, doc)
}
+57
View File
@@ -0,0 +1,57 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config/parser"
"github.com/owncloud/ocis/v2/extensions/web/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
err := parser.ParseConfig(cfg)
if err != nil {
fmt.Printf("%v", err)
}
return err
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
+64
View File
@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/thejerf/suture/v4"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the web command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "web",
Usage: "Serve ownCloud Web for oCIS",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the web command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new web.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.Web.Commons = cfg.Commons
return SutureService{
cfg: cfg.Web,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package command
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config/parser"
"github.com/owncloud/ocis/v2/extensions/web/pkg/logging"
"github.com/owncloud/ocis/v2/extensions/web/pkg/metrics"
"github.com/owncloud/ocis/v2/extensions/web/pkg/server/debug"
"github.com/owncloud/ocis/v2/extensions/web/pkg/server/http"
"github.com/owncloud/ocis/v2/extensions/web/pkg/tracing"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
err := parser.ParseConfig(cfg)
if err != nil {
fmt.Printf("%v", err)
os.Exit(1)
}
return err
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
// actually read the contents of the config file and override defaults
if cfg.File != "" {
contents, err := ioutil.ReadFile(cfg.File)
if err != nil {
logger.Err(err).Msg("error opening config file")
return err
}
if err = json.Unmarshal(contents, &cfg.Web.Config); err != nil {
logger.Fatal().Err(err).Msg("error unmarshalling config file")
return err
}
}
var (
gr = run.Group{}
ctx, cancel = func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
metrics = metrics.New()
)
defer cancel()
{
server, err := http.Server(
http.Logger(logger),
http.Context(ctx),
http.Namespace(cfg.HTTP.Namespace),
http.Config(cfg),
http.Metrics(metrics),
)
if err != nil {
logger.Info().
Err(err).
Str("transport", "http").
Msg("Failed to initialize server")
return err
}
gr.Add(func() error {
err := server.Run()
if err != nil {
logger.Error().
Err(err).
Str("transport", "http").
Msg("Failed to start server")
}
return err
}, func(_ error) {
logger.Info().
Str("transport", "http").
Msg("Shutting down server")
cancel()
})
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(server.ListenAndServe, func(_ error) {
_ = server.Shutdown(ctx)
cancel()
})
}
return gr.Run()
},
}
}
+50
View File
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
+79
View File
@@ -0,0 +1,79 @@
package config
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
HTTP HTTP `yaml:"http"`
Asset Asset `yaml:"asset"`
File string `yaml:"file" env:"WEB_UI_CONFIG"` // TODO: rename this to a more self explaining string
Web Web `yaml:"web"`
Context context.Context `yaml:"-"`
}
// Asset defines the available asset configuration.
type Asset struct {
Path string `yaml:"path" env:"WEB_ASSET_PATH"`
}
// WebConfig defines the available web configuration for a dynamically rendered config.json.
type WebConfig struct {
Server string `json:"server,omitempty" yaml:"server" env:"OCIS_URL;WEB_UI_CONFIG_SERVER"`
Theme string `json:"theme,omitempty" yaml:"theme" env:""`
Version string `json:"version,omitempty" yaml:"version" env:"WEB_UI_CONFIG_VERSION"`
OpenIDConnect OIDC `json:"openIdConnect,omitempty" yaml:"oids"`
Apps []string `json:"apps" yaml:"apps"`
ExternalApps []ExternalApp `json:"external_apps,omitempty" yaml:"external_apps"`
Options map[string]interface{} `json:"options,omitempty" yaml:"options"`
}
// OIDC defines the available oidc configuration
type OIDC struct {
MetadataURL string `json:"metadata_url,omitempty" yaml:"metadata_url" env:"WEB_OIDC_METADATA_URL"`
Authority string `json:"authority,omitempty" yaml:"authority" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEB_OIDC_AUTHORITY" desc:"URL of the OIDC issuer. It defaults to URL of the builtin IDP."`
ClientID string `json:"client_id,omitempty" yaml:"client_id" env:"WEB_OIDC_CLIENT_ID"`
ResponseType string `json:"response_type,omitempty" yaml:"response_type" env:"WEB_OIDC_RESPONSE_TYPE"`
Scope string `json:"scope,omitempty" yaml:"scope" env:"WEB_OIDC_SCOPE"`
}
// ExternalApp defines an external web app.
// {
// "name": "hello",
// "path": "http://localhost:9105/hello.js",
// "config": {
// "url": "http://localhost:9105"
// }
// }
type ExternalApp struct {
ID string `json:"id,omitempty" yaml:"id"`
Path string `json:"path,omitempty" yaml:"path"`
// Config is completely dynamic, because it depends on the extension
Config map[string]interface{} `json:"config,omitempty" yaml:"config"`
}
// ExternalAppConfig defines an external web app configuration.
type ExternalAppConfig struct {
URL string `json:"url,omitempty" yaml:"url" env:""`
}
// Web defines the available web configuration.
type Web struct {
Path string `yaml:"path" env:"WEB_UI_PATH"`
ThemeServer string `yaml:"theme_server" env:"OCIS_URL;WEB_UI_THEME_SERVER"` // used to build Theme in WebConfig
ThemePath string `yaml:"theme_path" env:"WEB_UI_THEME_PATH"` // used to build Theme in WebConfig
Config WebConfig `yaml:"config"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"WEB_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."`
Token string `yaml:"token" env:"WEB_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint"`
Pprof bool `yaml:"pprof" env:"WEB_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling"`
Zpages bool `yaml:"zpages" env:"WEB_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."`
}
@@ -0,0 +1,94 @@
package defaults
import (
"strings"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
)
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9104",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9100",
Root: "/",
Namespace: "com.owncloud.web",
CacheTTL: 604800, // 7 days
},
Service: config.Service{
Name: "web",
},
Asset: config.Asset{
Path: "",
},
Web: config.Web{
Path: "",
ThemeServer: "https://localhost:9200",
ThemePath: "/themes/owncloud/theme.json",
Config: config.WebConfig{
Server: "https://localhost:9200",
Theme: "",
Version: "0.1.0",
OpenIDConnect: config.OIDC{
MetadataURL: "",
Authority: "https://localhost:9200",
ClientID: "web",
ResponseType: "code",
Scope: "openid profile email",
},
Apps: []string{"files", "search", "preview", "text-editor", "pdf-viewer", "external", "user-management"},
Options: map[string]interface{}{
"hideSearchBar": false,
},
},
},
}
}
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
}
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimRight(cfg.HTTP.Root, "/")
}
// build well known openid-configuration endpoint if it is not set
if cfg.Web.Config.OpenIDConnect.MetadataURL == "" {
cfg.Web.Config.OpenIDConnect.MetadataURL = strings.TrimRight(cfg.Web.Config.OpenIDConnect.Authority, "/") + "/.well-known/openid-configuration"
}
}
+9
View File
@@ -0,0 +1,9 @@
package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"WEB_HTTP_ADDR"`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"WEB_HTTP_ROOT"`
CacheTTL int `yaml:"cache_ttl" env:"WEB_CACHE_TTL"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;WEB_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;WEB_LOG_PRETTY" desc:"Activates pretty log output."`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;WEB_LOG_COLOR" desc:"Activates colorized log output."`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;WEB_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."`
}
+37
View File
@@ -0,0 +1,37 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config/defaults"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
return nil
}
+6
View File
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;WEB_TRACING_ENABLED" desc:"Activates tracing."`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;WEB_TRACING_TYPE" desc:"The type of tracing. Defaults to \"\", which is the same as \"jaeger\". Allowed tracing types are \"jaeger\" and \"\" as of now."`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;WEB_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;WEB_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."`
}
+17
View File
@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}
+32
View File
@@ -0,0 +1,32 @@
package metrics
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "web"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
}
// prometheus.Register(
// m.Counter,
// )
return m
}
@@ -0,0 +1,13 @@
package middleware
import (
"net/http"
)
// SilentRefresh allows the oidc client lib to silently refresh the token in an iframe
func SilentRefresh(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
next.ServeHTTP(w, r)
})
}
+50
View File
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+59
View File
@@ -0,0 +1,59 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(health(options.Config)),
debug.Ready(ready(options.Config)),
), nil
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
+76
View File
@@ -0,0 +1,76 @@
package http
import (
"context"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/extensions/web/pkg/metrics"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/urfave/cli/v2"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
Flags []cli.Flag
Namespace string
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// Flags provides a function to set the flags option.
func Flags(val []cli.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, val...)
}
}
// Namespace provides a function to set the Namespace option.
func Namespace(val string) Option {
return func(o *Options) {
o.Namespace = val
}
}
+57
View File
@@ -0,0 +1,57 @@
package http
import (
chimiddleware "github.com/go-chi/chi/v5/middleware"
webmid "github.com/owncloud/ocis/v2/extensions/web/pkg/middleware"
svc "github.com/owncloud/ocis/v2/extensions/web/pkg/service/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"go-micro.dev/v4"
)
// Server initializes the http service and server.
func Server(opts ...Option) (http.Service, error) {
options := newOptions(opts...)
service := http.NewService(
http.Logger(options.Logger),
http.Namespace(options.Namespace),
http.Name("web"),
http.Version(version.GetString()),
http.Address(options.Config.HTTP.Addr),
http.Context(options.Context),
http.Flags(options.Flags...),
)
handle := svc.NewService(
svc.Logger(options.Logger),
svc.Config(options.Config),
svc.Middleware(
chimiddleware.RealIP,
chimiddleware.RequestID,
middleware.NoCache,
middleware.Secure,
webmid.SilentRefresh,
middleware.Version(
"web",
version.GetString(),
),
middleware.Logger(
options.Logger,
),
),
)
{
handle = svc.NewInstrument(handle, options.Metrics)
handle = svc.NewLogging(handle, options.Logger)
handle = svc.NewTracing(handle)
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, err
}
return service, nil
}
+30
View File
@@ -0,0 +1,30 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/v2/extensions/web/pkg/metrics"
)
// NewInstrument returns a service that instruments metrics.
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
return instrument{
next: next,
metrics: metrics,
}
}
type instrument struct {
next Service
metrics *metrics.Metrics
}
// ServeHTTP implements the Service interface.
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
i.next.ServeHTTP(w, r)
}
// Config implements the Service interface.
func (i instrument) Config(w http.ResponseWriter, r *http.Request) {
i.next.Config(w, r)
}
+30
View File
@@ -0,0 +1,30 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// NewLogging returns a service that logs messages.
func NewLogging(next Service, logger log.Logger) Service {
return logging{
next: next,
logger: logger,
}
}
type logging struct {
next Service
logger log.Logger
}
// ServeHTTP implements the Service interface.
func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
l.next.ServeHTTP(w, r)
}
// Config implements the Service interface.
func (l logging) Config(w http.ResponseWriter, r *http.Request) {
l.next.Config(w, r)
}
+50
View File
@@ -0,0 +1,50 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Middleware provides a function to set the middleware option.
func Middleware(val ...func(http.Handler) http.Handler) Option {
return func(o *Options) {
o.Middleware = val
}
}
+168
View File
@@ -0,0 +1,168 @@
package svc
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/v2/extensions/web/pkg/assets"
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
var (
// ErrConfigInvalid is returned when the config parse is invalid.
ErrConfigInvalid = `Invalid or missing config`
)
// Service defines the extension handlers.
type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request)
Config(http.ResponseWriter, *http.Request)
}
// NewService returns a service implementation for Service.
func NewService(opts ...Option) Service {
options := newOptions(opts...)
m := chi.NewMux()
m.Use(options.Middleware...)
svc := Web{
logger: options.Logger,
config: options.Config,
mux: m,
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Get("/config.json", svc.Config)
r.Mount("/", svc.Static(options.Config.HTTP.CacheTTL))
})
return svc
}
// Web defines implements the business logic for Service.
type Web struct {
logger log.Logger
config *config.Config
mux *chi.Mux
}
// ServeHTTP implements the Service interface.
func (p Web) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.mux.ServeHTTP(w, r)
}
func (p Web) getPayload() (payload []byte, err error) {
if p.config.Web.Path == "" {
// render dynamically using config
// build theme url
if themeServer, err := url.Parse(p.config.Web.ThemeServer); err == nil {
p.config.Web.Config.Theme = themeServer.String() + p.config.Web.ThemePath
} else {
p.config.Web.Config.Theme = p.config.Web.ThemePath
}
if p.config.Web.Config.ExternalApps == nil {
p.config.Web.Config.ExternalApps = []config.ExternalApp{
{
ID: "settings",
Path: "/settings.js",
},
}
}
// make apps render as empty array if it is empty
// TODO remove once https://github.com/golang/go/issues/27589 is fixed
if len(p.config.Web.Config.Apps) == 0 {
p.config.Web.Config.Apps = make([]string, 0)
}
return json.Marshal(p.config.Web.Config)
}
// try loading from file
if _, err = os.Stat(p.config.Web.Path); os.IsNotExist(err) {
p.logger.Fatal().
Err(err).
Str("config", p.config.Web.Path).
Msg("web config doesn't exist")
}
payload, err = ioutil.ReadFile(p.config.Web.Path)
if err != nil {
p.logger.Fatal().
Err(err).
Str("config", p.config.Web.Path).
Msg("failed to read custom config")
}
return
}
// Config implements the Service interface.
func (p Web) Config(w http.ResponseWriter, r *http.Request) {
payload, err := p.getPayload()
if err != nil {
http.Error(w, ErrConfigInvalid, http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(payload); err != nil {
p.logger.Error().Err(err).Msg("could not write config response")
}
}
// Static simply serves all static files.
func (p Web) Static(ttl int) http.HandlerFunc {
rootWithSlash := p.config.HTTP.Root
if !strings.HasSuffix(rootWithSlash, "/") {
rootWithSlash = rootWithSlash + "/"
}
static := http.StripPrefix(
rootWithSlash,
assets.FileServer(
assets.New(
assets.Logger(p.logger),
assets.Config(p.config),
),
),
)
lastModified := time.Now().UTC().Format(http.TimeFormat)
expires := time.Now().Add(time.Second * time.Duration(ttl)).UTC().Format(http.TimeFormat)
return func(w http.ResponseWriter, r *http.Request) {
if rootWithSlash != "/" && r.URL.Path == p.config.HTTP.Root {
http.Redirect(
w,
r,
rootWithSlash,
http.StatusMovedPermanently,
)
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s, must-revalidate", strconv.Itoa(ttl)))
w.Header().Set("Expires", expires)
w.Header().Set("Last-Modified", lastModified)
w.Header().Set("SameSite", "Strict")
static.ServeHTTP(w, r)
}
}
+26
View File
@@ -0,0 +1,26 @@
package svc
import (
"net/http"
)
// NewTracing returns a service that instruments traces.
func NewTracing(next Service) Service {
return tracing{
next: next,
}
}
type tracing struct {
next Service
}
// ServeHTTP implements the Service interface.
func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t.next.ServeHTTP(w, r)
}
// Config implements the Service interface.
func (t tracing) Config(w http.ResponseWriter, r *http.Request) {
t.next.Config(w, r)
}
+23
View File
@@ -0,0 +1,23 @@
package tracing
import (
"github.com/owncloud/ocis/v2/extensions/web/pkg/config"
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the web service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config) error {
var err error
if cfg.Tracing.Enabled {
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
return err
}
}
return nil
}