diff --git a/docs/services/general-info/port-ranges.md b/docs/services/general-info/port-ranges.md index fd6072e7c..317fa8b87 100644 --- a/docs/services/general-info/port-ranges.md +++ b/docs/services/general-info/port-ranges.md @@ -67,7 +67,7 @@ We also suggest to use the last port in your extensions' range as a debug/metric | 9265-9269 | FREE | | 9270-9274 | [eventhistory]({{< ref "../eventhistory/_index.md" >}}) | | 9275-9279 | FREE | -| 9280-9284 | FREE | +| 9280-9284 | [ocm]({{< ref "../ocm/_index.md" >}}) | | 9285-9289 | FREE | | 9290-9294 | FREE | | 9295-9299 | FREE | diff --git a/ocis-pkg/config/config.go b/ocis-pkg/config/config.go index bef713476..1e943abdb 100644 --- a/ocis-pkg/config/config.go +++ b/ocis-pkg/config/config.go @@ -22,6 +22,7 @@ import ( nats "github.com/owncloud/ocis/v2/services/nats/pkg/config" notifications "github.com/owncloud/ocis/v2/services/notifications/pkg/config" ocdav "github.com/owncloud/ocis/v2/services/ocdav/pkg/config" + ocm "github.com/owncloud/ocis/v2/services/ocm/pkg/config" ocs "github.com/owncloud/ocis/v2/services/ocs/pkg/config" policies "github.com/owncloud/ocis/v2/services/policies/pkg/config" postprocessing "github.com/owncloud/ocis/v2/services/postprocessing/pkg/config" @@ -98,6 +99,7 @@ type Config struct { Nats *nats.Config `yaml:"nats"` Notifications *notifications.Config `yaml:"notifications"` OCDav *ocdav.Config `yaml:"ocdav"` + OCM *ocm.Config `yaml:"ocm"` OCS *ocs.Config `yaml:"ocs"` Postprocessing *postprocessing.Config `yaml:"postprocessing"` Policies *policies.Config `yaml:"policies"` diff --git a/ocis-pkg/config/defaultconfig.go b/ocis-pkg/config/defaultconfig.go index 3d068b09b..9483bb8e0 100644 --- a/ocis-pkg/config/defaultconfig.go +++ b/ocis-pkg/config/defaultconfig.go @@ -21,6 +21,7 @@ import ( nats "github.com/owncloud/ocis/v2/services/nats/pkg/config/defaults" notifications "github.com/owncloud/ocis/v2/services/notifications/pkg/config/defaults" ocdav "github.com/owncloud/ocis/v2/services/ocdav/pkg/config/defaults" + ocm "github.com/owncloud/ocis/v2/services/ocm/pkg/config/defaults" ocs "github.com/owncloud/ocis/v2/services/ocs/pkg/config/defaults" policies "github.com/owncloud/ocis/v2/services/policies/pkg/config/defaults" postprocessing "github.com/owncloud/ocis/v2/services/postprocessing/pkg/config/defaults" @@ -70,6 +71,7 @@ func DefaultConfig() *Config { Nats: nats.DefaultConfig(), Notifications: notifications.DefaultConfig(), OCDav: ocdav.DefaultConfig(), + OCM: ocm.DefaultConfig(), OCS: ocs.DefaultConfig(), Postprocessing: postprocessing.DefaultConfig(), Policies: policies.DefaultConfig(), diff --git a/ocis/pkg/runtime/service/service.go b/ocis/pkg/runtime/service/service.go index 9687867fd..f240def01 100644 --- a/ocis/pkg/runtime/service/service.go +++ b/ocis/pkg/runtime/service/service.go @@ -39,6 +39,7 @@ import ( nats "github.com/owncloud/ocis/v2/services/nats/pkg/command" notifications "github.com/owncloud/ocis/v2/services/notifications/pkg/command" ocdav "github.com/owncloud/ocis/v2/services/ocdav/pkg/command" + ocm "github.com/owncloud/ocis/v2/services/ocm/pkg/command" ocs "github.com/owncloud/ocis/v2/services/ocs/pkg/command" policies "github.com/owncloud/ocis/v2/services/policies/pkg/command" postprocessing "github.com/owncloud/ocis/v2/services/postprocessing/pkg/command" @@ -272,6 +273,11 @@ func NewService(options ...Option) (*Service, error) { cfg.Webfinger.Commons = cfg.Commons return webfinger.Execute(cfg.Webfinger) }) + reg(opts.Config.OCM.Service.Name, func(ctx context.Context, cfg *ociscfg.Config) error { + cfg.OCM.Context = ctx + cfg.OCM.Commons = cfg.Commons + return ocm.Execute(cfg.OCM) + }) // populate optional services areg := func(name string, exec func(context.Context, *ociscfg.Config) error) { diff --git a/services/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go index 77ddcb303..89993fce2 100644 --- a/services/gateway/pkg/config/config.go +++ b/services/gateway/pkg/config/config.go @@ -43,6 +43,7 @@ type Config struct { StorageUsersEndpoint string `yaml:"-"` StorageSharesEndpoint string `yaml:"-"` AppRegistryEndpoint string `yaml:"-"` + OCMEndpoint string `yaml:"-"` StorageRegistry StorageRegistry `yaml:"storage_registry"` // TODO: should we even support switching this? diff --git a/services/gateway/pkg/config/defaults/defaultconfig.go b/services/gateway/pkg/config/defaults/defaultconfig.go index f66332a67..736638cb2 100644 --- a/services/gateway/pkg/config/defaults/defaultconfig.go +++ b/services/gateway/pkg/config/defaults/defaultconfig.go @@ -63,6 +63,7 @@ func DefaultConfig() *config.Config { StorageSharesEndpoint: "com.owncloud.api.storage-shares", StorageUsersEndpoint: "com.owncloud.api.storage-users", UsersEndpoint: "com.owncloud.api.users", + OCMEndpoint: "com.owncloud.api.ocm", StorageRegistry: config.StorageRegistry{ Driver: "spaces", diff --git a/services/gateway/pkg/revaconfig/config.go b/services/gateway/pkg/revaconfig/config.go index 97ef2b44d..858b369e8 100644 --- a/services/gateway/pkg/revaconfig/config.go +++ b/services/gateway/pkg/revaconfig/config.go @@ -50,7 +50,10 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]i // sharing is located on the sharing service "usershareprovidersvc": cfg.SharingEndpoint, "publicshareprovidersvc": cfg.SharingEndpoint, - "ocmshareprovidersvc": cfg.SharingEndpoint, + "ocmshareprovidersvc": cfg.OCMEndpoint, + "ocminvitemanagersvc": cfg.OCMEndpoint, + "ocmproviderauthorizersvc": cfg.OCMEndpoint, + "ocmcoresvc": cfg.OCMEndpoint, "commit_share_to_storage_grant": cfg.CommitShareToStorageGrant, "share_folder": cfg.ShareFolder, // ShareFolder is the location where to create shares in the recipient's storage provider. // other @@ -84,6 +87,7 @@ func GatewayConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]i "machine": cfg.AuthMachineEndpoint, "publicshares": cfg.StoragePublicLinkEndpoint, "serviceaccounts": cfg.AuthServiceEndpoint, + "ocmshares": cfg.OCMEndpoint, }, }, }, diff --git a/services/ocdav/pkg/command/server.go b/services/ocdav/pkg/command/server.go index 881aabf03..10f703345 100644 --- a/services/ocdav/pkg/command/server.go +++ b/services/ocdav/pkg/command/server.go @@ -63,6 +63,7 @@ func Server(cfg *config.Config) *cli.Command { ocdav.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins), ocdav.FilesNamespace(cfg.FilesNamespace), ocdav.WebdavNamespace(cfg.WebdavNamespace), + ocdav.OCMNamespace(cfg.OCMNamespace), ocdav.AllowDepthInfinity(cfg.AllowPropfindDepthInfinity), ocdav.SharesNamespace(cfg.SharesNamespace), ocdav.Timeout(cfg.Timeout), diff --git a/services/ocdav/pkg/config/config.go b/services/ocdav/pkg/config/config.go index a89acd388..b97086c5e 100644 --- a/services/ocdav/pkg/config/config.go +++ b/services/ocdav/pkg/config/config.go @@ -23,6 +23,7 @@ type Config struct { WebdavNamespace string `yaml:"webdav_namespace" env:"OCDAV_WEBDAV_NAMESPACE" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties."` FilesNamespace string `yaml:"files_namespace" env:"OCDAV_FILES_NAMESPACE" desc:"Jail requests to /dav/files/{username} into this CS3 namespace. Supports template layouting with CS3 User properties."` SharesNamespace string `yaml:"shares_namespace" env:"OCDAV_SHARES_NAMESPACE" desc:"The human readable path for the share jail. Relative to a users personal space root. Upcased intentionally."` + OCMNamespace string `yaml:"ocm_namespace" env:"OCDAV_OCM_NAMESPACE" desc:"The human readable path prefix for the ocm shares."` // PublicURL used to redirect /s/{token} URLs to PublicURL string `yaml:"public_url" env:"OCIS_URL;OCDAV_PUBLIC_URL" desc:"URL where oCIS is reachable for users."` diff --git a/services/ocdav/pkg/config/defaults/defaultconfig.go b/services/ocdav/pkg/config/defaults/defaultconfig.go index 07d11f6bd..cfb1a6b6b 100644 --- a/services/ocdav/pkg/config/defaults/defaultconfig.go +++ b/services/ocdav/pkg/config/defaults/defaultconfig.go @@ -81,6 +81,7 @@ func DefaultConfig() *config.Config { WebdavNamespace: "/users/{{.Id.OpaqueId}}", FilesNamespace: "/users/{{.Id.OpaqueId}}", SharesNamespace: "/Shares", + OCMNamespace: "/public", PublicURL: "https://localhost:9200", Insecure: false, Timeout: 84300, diff --git a/services/ocm/Makefile b/services/ocm/Makefile new file mode 100644 index 000000000..c53bbee76 --- /dev/null +++ b/services/ocm/Makefile @@ -0,0 +1,37 @@ +SHELL := bash +NAME := ocm + +include ../../.make/recursion.mk + +############ tooling ############ +ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI +include ../../.bingo/Variables.mk +endif + +############ go tooling ############ +include ../../.make/go.mk + +############ release ############ +include ../../.make/release.mk + +############ docs generate ############ +include ../../.make/docs.mk + +.PHONY: docs-generate +docs-generate: config-docs-generate + +############ generate ############ +include ../../.make/generate.mk + +.PHONY: ci-go-generate +ci-go-generate: # CI runs ci-node-generate automatically before this target + +.PHONY: ci-node-generate +ci-node-generate: + +############ licenses ############ +.PHONY: ci-node-check-licenses +ci-node-check-licenses: + +.PHONY: ci-node-save-licenses +ci-node-save-licenses: diff --git a/services/ocm/README.md b/services/ocm/README.md new file mode 100644 index 000000000..434070361 --- /dev/null +++ b/services/ocm/README.md @@ -0,0 +1,3 @@ +# OCM + +The `ocm` service provides federated sharing functionality based on [sciencemesh](https://sciencemesh.io/) and [ocm](https://github.com/cs3org/OCM-API). \ No newline at end of file diff --git a/services/ocm/cmd/ocm/main.go b/services/ocm/cmd/ocm/main.go new file mode 100644 index 000000000..6ac3d5a57 --- /dev/null +++ b/services/ocm/cmd/ocm/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/owncloud/ocis/v2/services/ocm/pkg/command" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config/defaults" +) + +func main() { + if err := command.Execute(defaults.DefaultConfig()); err != nil { + os.Exit(1) + } +} diff --git a/services/ocm/pkg/command/health.go b/services/ocm/pkg/command/health.go new file mode 100644 index 000000000..7f41a24b1 --- /dev/null +++ b/services/ocm/pkg/command/health.go @@ -0,0 +1,18 @@ +package command + +import ( + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" + "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", + Action: func(c *cli.Context) error { + // Not implemented + return nil + }, + } +} diff --git a/services/ocm/pkg/command/root.go b/services/ocm/pkg/command/root.go new file mode 100644 index 000000000..7187fe527 --- /dev/null +++ b/services/ocm/pkg/command/root.go @@ -0,0 +1,34 @@ +package command + +import ( + "os" + + "github.com/owncloud/ocis/v2/ocis-pkg/clihelper" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" + "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 ocm command. +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cli.App{ + Name: "ocm", + Usage: "starts ocm service", + Commands: GetCommands(cfg), + }) + + return app.Run(os.Args) +} diff --git a/services/ocm/pkg/command/server.go b/services/ocm/pkg/command/server.go new file mode 100644 index 000000000..1025d1ce7 --- /dev/null +++ b/services/ocm/pkg/command/server.go @@ -0,0 +1,110 @@ +package command + +import ( + "context" + "fmt" + "os" + "path" + + "github.com/cs3org/reva/v2/cmd/revad/runtime" + "github.com/gofrs/uuid" + "github.com/oklog/run" + "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + "github.com/owncloud/ocis/v2/ocis-pkg/registry" + "github.com/owncloud/ocis/v2/ocis-pkg/sync" + "github.com/owncloud/ocis/v2/ocis-pkg/tracing" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config/parser" + "github.com/owncloud/ocis/v2/services/ocm/pkg/logging" + "github.com/owncloud/ocis/v2/services/ocm/pkg/revaconfig" + "github.com/owncloud/ocis/v2/services/ocm/pkg/server/debug" + "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 the %s service without runtime (unsupervised mode)", cfg.Service.Name), + Category: "server", + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name) + if err != nil { + return err + } + gr := run.Group{} + ctx, cancel := defineContext(cfg) + + defer cancel() + + rCfg := revaconfig.OCMConfigFromStruct(cfg, logger) + + gr.Add(func() error { + pidFile := path.Join(os.TempDir(), "revad-"+cfg.Service.Name+"-"+uuid.Must(uuid.NewV4()).String()+".pid") + reg := registry.GetRegistry() + + runtime.RunWithOptions(rCfg, pidFile, + runtime.WithLogger(&logger.Logger), + runtime.WithRegistry(reg), + runtime.WithTraceProvider(traceProvider), + ) + + return nil + }, func(err error) { + logger.Error(). + Err(err). + Str("server", cfg.Service.Name). + Msg("Shutting down server") + + cancel() + os.Exit(1) + }) + + debugServer, err := debug.Server( + debug.Logger(logger), + debug.Context(ctx), + debug.Config(cfg), + ) + if err != nil { + logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server") + return err + } + + gr.Add(debugServer.ListenAndServe, func(_ error) { + cancel() + }) + + if !cfg.Supervised { + sync.Trap(&gr, cancel) + } + + grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, uuid.Must(uuid.NewV4()).String(), cfg.GRPC.Addr, version.GetString()) + if err := registry.RegisterService(ctx, grpcSvc, logger); err != nil { + logger.Fatal().Err(err).Msg("failed to register the grpc service") + } + + httpSvc := registry.BuildHTTPService(cfg.HTTP.Namespace+"."+cfg.Service.Name, uuid.Must(uuid.NewV4()).String(), cfg.HTTP.Addr, version.GetString()) + if err := registry.RegisterService(ctx, httpSvc, logger); err != nil { + logger.Fatal().Err(err).Msg("failed to register the http service") + } + + return gr.Run() + }, + } +} + +// defineContext sets the context for the service. If there is a context configured it will create a new child from it, +// if not, it will create a root context that can be cancelled. +func defineContext(cfg *config.Config) (context.Context, context.CancelFunc) { + return func() (context.Context, context.CancelFunc) { + if cfg.Context == nil { + return context.WithCancel(context.Background()) + } + return context.WithCancel(cfg.Context) + }() +} diff --git a/services/ocm/pkg/command/version.go b/services/ocm/pkg/command/version.go new file mode 100644 index 000000000..13639cff0 --- /dev/null +++ b/services/ocm/pkg/command/version.go @@ -0,0 +1,19 @@ +package command + +import ( + "github.com/owncloud/ocis/v2/services/ocm/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 service instances", + Category: "info", + Action: func(c *cli.Context) error { + // not implemented + return nil + }, + } +} diff --git a/services/ocm/pkg/config/config.go b/services/ocm/pkg/config/config.go new file mode 100644 index 000000000..6f9475362 --- /dev/null +++ b/services/ocm/pkg/config/config.go @@ -0,0 +1,131 @@ +package config + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + "go-micro.dev/v4/client" +) + +// 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 HTTPConfig `yaml:"http"` + Middleware Middleware `yaml:"middleware"` + GRPC GRPCConfig `yaml:"grpc"` + GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + GrpcClient client.Client `yaml:"-"` + + Reva *shared.Reva `yaml:"reva"` + OCMD OCMD `yaml:"ocmd"` + ScienceMesh ScienceMesh `yaml:"sciencemesh"` + OCMInviteManager OCMInviteManager `yaml:"ocm_invite_manager"` + OCMProviderAuthorizerDriver string `yaml:"ocm_provider_authorizer_driver" env:"SHARING_OCM_PROVIDER_AUTHORIZER_DRIVER" desc:"Driver to be used to persist ocm invites. Supported values 'json'."` + OCMProviderAuthorizerDrivers OCMProviderAuthorizerDrivers `yaml:"ocm_provider_authorizer_drivers"` + OCMShareProvider OCMShareProvider `yaml:"ocm_share_provider"` + OCMCore OCMCore `yaml:"ocm_core"` + + Supervised bool `yaml:"-"` + Context context.Context `yaml:"-"` +} + +// HTTPConfig defines the available http configuration. +type HTTPConfig struct { + Addr string `yaml:"addr" env:"OCM_HTTP_ADDR" desc:"The bind address of the HTTP service."` + Namespace string `yaml:"-"` + Protocol string `yaml:"protocol" env:"OCM_HTTP_PROTOCOL" desc:"The transport protocol of the HTTP service."` + Prefix string `yaml:"prefix" env:"OCM_HTTP_PREFIX" desc:"The Path prefix where the OCM can be accessed (defaults to /)."` + CORS CORS `yaml:"cors"` +} + +// Middleware configures reva middlewares. +type Middleware struct { + Auth Auth `yaml:"auth"` +} + +// Auth configures reva http auth middleware. +type Auth struct { + CredentialsByUserAgent map[string]string `yaml:"credentials_by_user_agent"` +} + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;OCM_CORS_ALLOW_ORIGINS" desc:"A comma-separated list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin"` + AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;OCM_CORS_ALLOW_METHODS" desc:"A comma-separated list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method"` + AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;OCM_CORS_ALLOW_HEADERS" desc:"A blank or comma-separated list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers."` + AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;OCM_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials."` +} + +// GRPCConfig defines the available grpc configuration. +type GRPCConfig struct { + Addr string `ocisConfig:"addr" env:"OCM_GRPC_ADDR" desc:"The bind address of the GRPC service."` + Namespace string `ocisConfig:"-" yaml:"-"` + TLS *shared.GRPCServiceTLS `yaml:"tls"` + Protocol string `yaml:"protocol" env:"OCM_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service."` +} + +type ScienceMesh struct { + Prefix string `yaml:"prefix" env:"OCM_SCIENCEMESH_PREFIX" desc:"URL path prefix for the ScienceMesh service. Note that the string must not start with '/'."` +} + +type OCMD struct { + Prefix string `yaml:"prefix" env:"OCM_OCMD_PREFIX" desc:"URL path prefix for the OCMD service. Note that the string must not start with '/'."` + ExposeRecipientDisplayName bool `yaml:"expose_recipient_display_name" env:"OCM_OCMD_EXPOSE_RECIPIENT_DISPLAY_NAME" desc:"Expose the display name of OCM share recipients."` +} + +type OCMInviteManager struct { + Driver string `yaml:"driver" env:"SHARING_OCM_INVITE_MANAGER_DRIVER" desc:"Driver to be used to persist ocm invites. Supported values 'json'."` + Drivers OCMInviteManagerDrivers `yaml:"drivers"` + Insecure bool `yaml:"insecure" env:"SHARING_OCM_INVITE_MANAGER_INSECURE" desc:"Disable TLS certificate validation for the OCM connections. Do not set this in production environments."` +} + +type OCMInviteManagerDrivers struct { + JSON OCMInviteManagerJSONDriver `yaml:"json"` +} + +type OCMInviteManagerJSONDriver struct { + File string `yaml:"file" env:"SHARING_OCM_INVITE_MANAGER_JSON_FILE" desc:"Path to the JSON file where ocm invites data will be stored. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/storage."` +} + +type OCMProviderAuthorizerDrivers struct { + JSON OCMProviderAuthorizerJSONDriver `yaml:"json"` +} + +type OCMProviderAuthorizerJSONDriver struct { + Providers string `yaml:"providers" env:"SHARING_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE" desc:"Path to the JSON file where ocm invites data will be stored. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/storage."` + VerifyRequestHostname bool `yaml:"verify_request_hostname" env:"SHARING_OCM_PROVIDER_AUTHORIZER_PROVIDERS_FILE"` +} + +type OCMCore struct { + Driver string `yaml:"driver" env:"SHARING_OCM_CORE_DRIVER" desc:"Driver to be used for the ocm core. Supported values 'json'."` + Drivers OCMCoreDrivers `yaml:"drivers"` +} + +type OCMCoreDrivers struct { + JSON OCMCoreJSONDriver `yaml:"json"` +} + +type OCMCoreJSONDriver struct { + File string `yaml:"file" env:"SHARING_OCM_CORE_JSON_FILE" desc:"Path to the JSON file where ocm share data will be stored. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/storage."` +} + +type OCMShareProvider struct { + Driver string `yaml:"driver" env:"SHARING_OCM_SHARE_PROVIDER_DRIVER" desc:"Driver to be used for the ocm share provider. Supported values 'json'."` + Drivers OCMShareProviderDrivers `yaml:"drivers"` + Insecure bool `yaml:"insecure" env:"SHARING_OCM_SHARE_PROVIDER_INSECURE" desc:"Disable TLS certificate validation for the OCM connections. Do not set this in production environments."` +} + +type OCMShareProviderDrivers struct { + JSON OCMShareProviderJSONDriver `yaml:"json"` +} + +type OCMShareProviderJSONDriver struct { + File string `yaml:"file" env:"SHARING_OCM_SHAREPROVIDER_JSON_FILE" desc:"Path to the JSON file where ocm share data will be stored. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/storage."` +} diff --git a/services/ocm/pkg/config/debug.go b/services/ocm/pkg/config/debug.go new file mode 100644 index 000000000..ab96107f4 --- /dev/null +++ b/services/ocm/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"OCM_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."` + Token string `yaml:"token" env:"OCM_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."` + Pprof bool `yaml:"pprof" env:"OCM_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."` + Zpages bool `yaml:"zpages" env:"OCM_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."` +} diff --git a/services/ocm/pkg/config/defaults/defaultconfig.go b/services/ocm/pkg/config/defaults/defaultconfig.go new file mode 100644 index 000000000..df1102631 --- /dev/null +++ b/services/ocm/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,169 @@ +package defaults + +import ( + "path/filepath" + + "github.com/owncloud/ocis/v2/ocis-pkg/config/defaults" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" + "github.com/owncloud/ocis/v2/ocis-pkg/structs" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" +) + +// FullDefaultConfig returns the full default config +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +// DefaultConfig return the default configuration +func DefaultConfig() *config.Config { + return &config.Config{ + Debug: config.Debug{ + Addr: "127.0.0.1:9281", + Token: "", + Pprof: false, + Zpages: false, + }, + HTTP: config.HTTPConfig{ + Addr: "127.0.0.1:9280", + Namespace: "com.owncloud.web", + Protocol: "tcp", + Prefix: "", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + "OPTIONS", + "HEAD", + "GET", + "PUT", + "POST", + "DELETE", + "MKCOL", + "PROPFIND", + "PROPPATCH", + "MOVE", + "COPY", + "REPORT", + "SEARCH", + }, + AllowedHeaders: []string{ + "Origin", + "Accept", + "Content-Type", + "Depth", + "Authorization", + "Ocs-Apirequest", + "If-None-Match", + "If-Match", + "Destination", + "Overwrite", + "X-Request-Id", + "X-Requested-With", + "Tus-Resumable", + "Tus-Checksum-Algorithm", + "Upload-Concat", + "Upload-Length", + "Upload-Metadata", + "Upload-Defer-Length", + "Upload-Expires", + "Upload-Checksum", + "Upload-Offset", + "X-HTTP-Method-Override", + "Cache-Control", + }, + AllowCredentials: true, + }, + }, + GRPC: config.GRPCConfig{ + Addr: "127.0.0.1:9282", + Namespace: "com.owncloud.api", + }, + Reva: shared.DefaultRevaConfig(), + Service: config.Service{ + Name: "ocm", + }, + ScienceMesh: config.ScienceMesh{ + Prefix: "sciencemesh", + }, + OCMD: config.OCMD{ + Prefix: "ocm", + }, + OCMInviteManager: config.OCMInviteManager{ + Driver: "json", + Drivers: config.OCMInviteManagerDrivers{ + JSON: config.OCMInviteManagerJSONDriver{ + File: filepath.Join(defaults.BaseDataPath(), "storage", "ocminvites.json"), + }, + }, + Insecure: false, + }, + OCMProviderAuthorizerDriver: "json", + OCMProviderAuthorizerDrivers: config.OCMProviderAuthorizerDrivers{ + JSON: config.OCMProviderAuthorizerJSONDriver{ + Providers: filepath.Join(defaults.BaseDataPath(), "storage", "ocmproviders.json"), + }, + }, + OCMShareProvider: config.OCMShareProvider{ + Driver: "json", + Drivers: config.OCMShareProviderDrivers{ + JSON: config.OCMShareProviderJSONDriver{ + File: filepath.Join(defaults.BaseDataPath(), "storage", "ocmshares.json"), + }, + }, + Insecure: false, + }, + OCMCore: config.OCMCore{ + Driver: "json", + Drivers: config.OCMCoreDrivers{ + JSON: config.OCMCoreJSONDriver{ + File: filepath.Join(defaults.BaseDataPath(), "storage", "ocmshares.json"), + }, + }, + }, + } +} + +// EnsureDefaults ensures the config contains default values +func EnsureDefaults(cfg *config.Config) { + // provide with defaults for shared logging, since we need a valid destination address for "envdecode". + 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{} + } + + 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{} + } + + if cfg.Reva == nil && cfg.Commons != nil { + cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva) + } + + if cfg.GRPCClientTLS == nil && cfg.Commons != nil { + cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) + } + + if cfg.GRPC.TLS == nil && cfg.Commons != nil { + cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS) + } +} + +// Sanitize sanitizes the config +func Sanitize(cfg *config.Config) { + // nothing to sanitize here atm +} diff --git a/services/ocm/pkg/config/log.go b/services/ocm/pkg/config/log.go new file mode 100644 index 000000000..5be7a2bce --- /dev/null +++ b/services/ocm/pkg/config/log.go @@ -0,0 +1,9 @@ +package config + +// Log defines the available log configuration. +type Log struct { + Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;OCM_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'."` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;OCM_LOG_PRETTY" desc:"Activates pretty log output."` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;OCM_LOG_COLOR" desc:"Activates colorized log output."` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;OCM_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."` +} diff --git a/services/ocm/pkg/config/parser/parse.go b/services/ocm/pkg/config/parser/parse.go new file mode 100644 index 000000000..223df488b --- /dev/null +++ b/services/ocm/pkg/config/parser/parse.go @@ -0,0 +1,43 @@ +package parser + +import ( + "errors" + + ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/ocis-pkg/structs" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config/defaults" + + "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) +} + +// Validate validates the config +func Validate(cfg *config.Config) error { + if cfg.GRPCClientTLS == nil && cfg.Commons != nil { + cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) + } + + return nil +} diff --git a/services/ocm/pkg/config/service.go b/services/ocm/pkg/config/service.go new file mode 100644 index 000000000..d1eac383f --- /dev/null +++ b/services/ocm/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `yaml:"-"` +} diff --git a/services/ocm/pkg/config/tracing.go b/services/ocm/pkg/config/tracing.go new file mode 100644 index 000000000..0ba9ed35a --- /dev/null +++ b/services/ocm/pkg/config/tracing.go @@ -0,0 +1,21 @@ +package config + +import "github.com/owncloud/ocis/v2/ocis-pkg/tracing" + +// Tracing defines the available tracing configuration. +type Tracing struct { + Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;OCM_TRACING_ENABLED" desc:"Activates tracing."` + Type string `yaml:"type" env:"OCIS_TRACING_TYPE;OCM_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;OCM_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."` + Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;OCM_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."` +} + +// Convert Tracing to the tracing package's Config struct. +func (t Tracing) Convert() tracing.Config { + return tracing.Config{ + Enabled: t.Enabled, + Type: t.Type, + Endpoint: t.Endpoint, + Collector: t.Collector, + } +} diff --git a/services/ocm/pkg/logging/logging.go b/services/ocm/pkg/logging/logging.go new file mode 100644 index 000000000..64747b5a8 --- /dev/null +++ b/services/ocm/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" +) + +// 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), + ) +} diff --git a/services/ocm/pkg/metrics/metrics.go b/services/ocm/pkg/metrics/metrics.go new file mode 100644 index 000000000..89eeed6da --- /dev/null +++ b/services/ocm/pkg/metrics/metrics.go @@ -0,0 +1,35 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var ( + // Namespace defines the namespace for the defines metrics. + Namespace = "ocis" + + // Subsystem defines the subsystem for the defines metrics. + Subsystem = "ocm" +) + +// Metrics defines the available metrics of this service. +type Metrics struct { + BuildInfo *prometheus.GaugeVec +} + +// New initializes the available metrics. +func New() *Metrics { + m := &Metrics{ + BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "build_info", + Help: "Build information", + }, []string{"version"}), + } + + _ = prometheus.Register( + m.BuildInfo, + ) + + // TODO: implement metrics + return m +} diff --git a/services/ocm/pkg/revaconfig/config.go b/services/ocm/pkg/revaconfig/config.go new file mode 100644 index 000000000..40a10768e --- /dev/null +++ b/services/ocm/pkg/revaconfig/config.go @@ -0,0 +1,115 @@ +package revaconfig + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" +) + +// OCMConfigFromStruct will adapt an oCIS config struct into a reva mapstructure to start a reva service. +func OCMConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]interface{} { + return map[string]interface{}{ + "shared": map[string]interface{}{ + "gatewaysvc": cfg.Reva.Address, // Todo or address? + "grpc_client_options": cfg.Reva.GetGRPCClientConfig(), + }, + "http": map[string]interface{}{ + "network": cfg.HTTP.Protocol, + "address": cfg.HTTP.Addr, + "middlewares": map[string]interface{}{ + "cors": map[string]interface{}{ + "allowed_origins": cfg.HTTP.CORS.AllowedOrigins, + "allowed_methods": cfg.HTTP.CORS.AllowedMethods, + "allowed_headers": cfg.HTTP.CORS.AllowedHeaders, + "allow_credentials": cfg.HTTP.CORS.AllowCredentials, + // currently unused + //"options_passthrough": , + //"debug": , + //"max_age": , + //"priority": , + //"exposed_headers": , + }, + "auth": map[string]interface{}{ + "credentials_by_user_agent": cfg.Middleware.Auth.CredentialsByUserAgent, + }, + "prometheus": map[string]interface{}{ + "namespace": "ocis", + "subsystem": "ocm", + }, + "requestid": map[string]interface{}{}, + }, + // TODO build services dynamically + "services": map[string]interface{}{ + "sciencemesh": map[string]interface{}{ + "prefix": cfg.ScienceMesh.Prefix, + "smtp_credentials": map[string]string{}, + "gatewaysvc": cfg.Reva.Address, + "mesh_directory_url": cfg.Commons.OcisURL, + "provider_domain": cfg.Commons.OcisURL, + }, + "ocmd": map[string]interface{}{ + "prefix": cfg.OCMD.Prefix, + "gatewaysvc": cfg.Reva.Address, + "expose_recipient_display_name": cfg.OCMD.ExposeRecipientDisplayName, + }, + }, + }, + "grpc": map[string]interface{}{ + "network": cfg.GRPC.Protocol, + "address": cfg.GRPC.Addr, + "tls_settings": map[string]interface{}{ + "enabled": cfg.GRPC.TLS.Enabled, + "certificate": cfg.GRPC.TLS.Cert, + "key": cfg.GRPC.TLS.Key, + }, + "services": map[string]interface{}{ + "ocminvitemanager": map[string]interface{}{ + "driver": cfg.OCMInviteManager.Driver, + "drivers": map[string]interface{}{ + "json": map[string]interface{}{ + "file": cfg.OCMInviteManager.Drivers.JSON.File, + }, + }, + "provider_domain": cfg.Commons.OcisURL, + "ocm_insecure": cfg.OCMInviteManager.Insecure, + }, + "ocmproviderauthorizer": map[string]interface{}{ + "driver": cfg.OCMProviderAuthorizerDriver, + "drivers": map[string]interface{}{ + "json": map[string]interface{}{ + "providers": cfg.OCMProviderAuthorizerDrivers.JSON.Providers, + "verify_request_hostname": cfg.OCMProviderAuthorizerDrivers.JSON.VerifyRequestHostname, + }, + }, + }, + "ocmshareprovider": map[string]interface{}{ + "driver": cfg.OCMShareProvider.Driver, + "drivers": map[string]interface{}{ + "json": map[string]interface{}{ + "file": cfg.OCMShareProvider.Drivers.JSON.File, + }, + }, + "gatewaysvc": cfg.Reva.Address, + "provider_domain": cfg.Commons.OcisURL, + "webdav_endpoint": cfg.Commons.OcisURL, + "client_insecure": cfg.OCMShareProvider.Insecure, + }, + "ocmcore": map[string]interface{}{ + "driver": cfg.OCMCore.Driver, + "drivers": map[string]interface{}{ + "json": map[string]interface{}{ + "file": cfg.OCMCore.Drivers.JSON.File, + }, + }, + }, + "authprovider": map[string]interface{}{ + "auth_manager": "ocmshares", + "auth_managers": map[string]interface{}{ + "ocmshares": map[string]interface{}{ + "gatewaysvc": cfg.Reva.Address, + }, + }, + }, + }, + }, + } +} diff --git a/services/ocm/pkg/server/debug/option.go b/services/ocm/pkg/server/debug/option.go new file mode 100644 index 000000000..b671376be --- /dev/null +++ b/services/ocm/pkg/server/debug/option.go @@ -0,0 +1,50 @@ +package debug + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" +) + +// 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 + } +} diff --git a/services/ocm/pkg/server/debug/server.go b/services/ocm/pkg/server/debug/server.go new file mode 100644 index 000000000..b2275b585 --- /dev/null +++ b/services/ocm/pkg/server/debug/server.go @@ -0,0 +1,63 @@ +package debug + +import ( + "io" + "net/http" + + "github.com/owncloud/ocis/v2/ocis-pkg/service/debug" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "github.com/owncloud/ocis/v2/services/ocm/pkg/config" +) + +// 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)), + //debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins), + //debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods), + //debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders), + //debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials), + ), 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) + } + } +} diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 333ab6d7a..460c0b1b3 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -143,6 +143,14 @@ func DefaultPolicies() []config.Policy { Service: "com.owncloud.web.frontend", Unprotected: true, }, + { + Endpoint: "/sciencemesh/", + Service: "com.owncloud.web.ocm", + }, + { + Endpoint: "/ocm/", + Service: "com.owncloud.web.ocm", + }, { Endpoint: "/ocs/", Service: "com.owncloud.web.frontend", diff --git a/services/proxy/pkg/middleware/authentication.go b/services/proxy/pkg/middleware/authentication.go index a726eae4c..f173e94da 100644 --- a/services/proxy/pkg/middleware/authentication.go +++ b/services/proxy/pkg/middleware/authentication.go @@ -25,6 +25,8 @@ var ( _publicPaths = [...]string{ "/dav/public-files/", + "/dav/ocm/", + "/ocm/", "/remote.php/dav/public-files/", "/ocs/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected", "/ocs/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected", diff --git a/services/sharing/pkg/config/config.go b/services/sharing/pkg/config/config.go index a00441fe8..c739cdc2c 100644 --- a/services/sharing/pkg/config/config.go +++ b/services/sharing/pkg/config/config.go @@ -105,7 +105,6 @@ type UserSharingJSONCS3Driver struct { SystemUserAPIKey string `yaml:"system_user_api_key" env:"OCIS_SYSTEM_USER_API_KEY;SHARING_USER_JSONCS3_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user."` CacheTTL int `yaml:"cache_ttl" env:"SHARING_USER_JSONCS3_CACHE_TTL" desc:"TTL for the internal caches in seconds."` } - type PublicSharingDrivers struct { JSON PublicSharingJSONDriver `yaml:"json"` JSONCS3 PublicSharingJSONCS3Driver `yaml:"jsoncs3"`