Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
SHELL := bash
NAME := app-registry
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk
+448
View File
@@ -0,0 +1,448 @@
# App Registry
The `app-registry` service is the single point where all apps register themselves and their respective supported mime types.
Administrators can set default applications on a per MIME type basis and also allow the creation of new files for certain MIME types. This per MIME type configuration also features a description, file extension option and an icon.
## MIME Type Configuration / Creation Allow List
The apps will register their supported MIME types automatically, so that users can open supported files with them.
Administrators can set default applications for each MIME type and also allow the creation of new files for certain mime types. This, per MIME type configuration, also features a description, file extension option and an icon.
### MIME Type Configuration
Modifing the MIME type config can only be achieved via a yaml configuration. Using environment variables is not possible. The following is a brief structure and a field description:
**Structure**
```yaml
app_registry:
mimetypes:
- mime_type: application/vnd.oasis.opendocument.spreadsheet
extension: ods
name: OpenSpreadsheet
description: OpenDocument spreadsheet document
icon: https://some-website.test/opendocument-spreadsheet-icon.png
default_app: Collabora
allow_creation: true
- mime_type: ...
```
**Fields**
* `mime_type`\
The MIME type you want to configure.
* `extension`\
The file extension to be used for new files.
* `name`\
The name of the file / MIME type.
* `description`\
The human-readable description of the file / MIME type.
* `icon`\
The URL to an icon which should be used for that MIME type.
* `default_app`\
The name of the default app which opens this MIME type if the user doesnt specify one.
* `allow_creation`\
Whether a user should be able to create new files of that MIME type (true or false).
## Endpoint Access
### Listing available apps and mime types
Clients, for example КуСфера Web, need to offer users the available apps to open files and mime types for new file creation. This information can be obtained from this endpoint.
**Endpoint**: specified in the capabilities in `apps_url`, currently `/app/list`
**Method**: HTTP GET
**Authentication**: None
**Request example**:
```bash
curl 'https://qsfera.test/app/list'
```
**Response example**:
HTTP status code: 200
```json
{
"mime-types": [
{
"mime_type": "application/pdf",
"ext": "pdf",
"app_providers": [
{
"name": "OnlyOffice",
"icon": "https://some-website.test/onlyoffice-pdf-icon.png"
}
],
"name": "PDF",
"description": "PDF document"
},
{
"mime_type": "application/vnd.oasis.opendocument.text",
"ext": "odt",
"app_providers": [
{
"name": "Collabora",
"icon": "https://some-website.test/collabora-odt-icon.png"
},
{
"name": "OnlyOffice",
"icon": "https://some-website.test/onlyoffice-odt-icon.png"
}
],
"name": "OpenDocument",
"icon": "https://some-website.test/opendocument-text-icon.png",
"description": "OpenDocument text document",
"allow_creation": true,
"default_application": "Collabora"
},
{
"mime_type": "text/markdown",
"ext": "md",
"app_providers": [
{
"name": "CodiMD",
"icon": "https://some-website.test/codimd-md-icon.png"
}
],
"name": "Markdown file",
"description": "Markdown file",
"allow_creation": true,
"default_application": "CodiMD"
},
{
"mime_type": "application/vnd.ms-word.document.macroenabled.12",
"app_providers": [
{
"name": "Collabora",
"icon": "https://some-website.test/collabora-word-icon.png"
},
{
"name": "OnlyOffice",
"icon": "https://some-website.test/onlyoffice-word-icon.png"
}
]
},
{
"mime_type": "application/vnd.ms-powerpoint.template.macroenabled.12",
"app_providers": [
{
"name": "Collabora",
"icon": "https://some-website.test/collabora-powerpoint-icon.png"
}
]
}
]
}
```
### Open a File With КуСфера Web
**Endpoint**: specified in the capabilities in `open_web_url`, currently `/app/open-with-web`
**Method**: HTTP POST
**Authentication** (one of them):
- `Authorization` header with OIDC Bearer token for authenticated users or basic auth credentials (if enabled in КуСфера)
- `X-Access-Token` header with a REVA token for authenticated users
**Query parameters**:
- `file_id` (mandatory): id of the file to be opened
- `app_name` (optional)
- default (not given): default app for mime type
- possible values depend on the app providers for a mimetype from the `/app/open` endpoint
**Request examples**:
```bash
curl -X POST 'https://qsfera.test/app/open-with-web?file_id=ZmlsZTppZAo='
curl -X POST 'https://qsfera.test/app/open-with-web?file_id=ZmlsZTppZAo=&app_name=Collabora'
```
**Response examples**:
The URI from the response JSON is intended to be opened with a GET request in a browser. If the user has not yet a session in the browser, a login flow is handled by КуСфера Web.
HTTP status code: 200
```json
{
"uri": "https://....."
}
```
**Example responses (error case)**:
See error cases for [Open a file with the app provider](#open-a-file-with-the-app-provider)
### Open a File With the App Provider
**Endpoint**: specified in the capabilities in `open_url`, currently `/app/open`
**Method**: HTTP POST
**Authentication** (one of them):
- `Authorization` header with OIDC Bearer token for authenticated users or basic auth credentials (if enabled in КуСфера)
- `Public-Token` header with public link token for public links
- `X-Access-Token` header with a REVA token for authenticated users
**Query parameters**:
- `file_id` (mandatory): id of the file to be opened
- `app_name` (optional)
- default (not given): default app for mime type
- possible values depend on the app providers for a mimetype from the `/app/open` endpoint
- `view_mode` (optional)
- default (not given): highest possible view mode, depending on the file permissions
- possible values:
- `write`: user can edit and download in the opening app
- `read`: user can view and download from the opening app
- `view`: user can view in the opening app (download is not possible)
- `lang` (optional)
- default (not given): default language of the application (which might maybe use the browser language)
- possible value is any ISO 639-1 language code. Examples:
- de
- en
- es
- ...
**Request examples**:
```bash
curl -X POST 'https://qsfera.test/app/open?file_id=ZmlsZTppZAo='
curl -X POST 'https://qsfera.test/app/open?file_id=ZmlsZTppZAo=&lang=de'
curl -X POST 'https://qsfera.test/app/open?file_id=ZmlsZTppZAo=&app_name=Collabora'
curl -X POST 'https://qsfera.test/app/open?file_id=ZmlsZTppZAo=&view_mode=read'
curl -X POST 'https://qsfera.test/app/open?file_id=ZmlsZTppZAo=&app_name=Collabora&view_mode=write'
```
**Response examples**:
All apps are expected to be opened in an iframe and the response will give some parameters for that action.
There are apps, which need to be opened in the iframe with a form post. The form post must include all form parameters included in the response. For these apps the response will look like this:
HTTP status code: 200
```json
{
"app_url": "https://.....",
"method": "POST",
"form_parameters": {
"access_token": "eyJ0...",
"access_token_ttl": "1634300912000",
"arbitrary_param": "lorem-ipsum"
}
}
```
There are apps, which need to be opened in the iframe with a GET request. The GET request must have set all headers included in the response. For these apps the response will look like this:
HTTP status code: 200
```json
{
"app_url": "https://...",
"method": "GET",
"headers": {
"access_token": "eyJ0e...",
"access_token_ttl": "1634300912000",
"arbitrary_header": "lorem-ipsum"
}
}
```
**Example responses (error case)**:
- missing `file_id`
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "missing file ID"
}
```
- wrong `view_mode`
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "invalid view mode"
}
```
- unknown `app_name`
HTTP status code: 404
```json
{
"code": "RESOURCE_NOT_FOUND",
"message": "error: not found: app 'Collabora' not found"
}
```
- wrong / invalid file id
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "invalid file ID"
}
```
- file id does not point to a file
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "the given file id does not point to a file"
}
```
- file does not exist / unauthorized to open the file
HTTP status code: 404
```json
{
"code": "RESOURCE_NOT_FOUND",
"message": "file does not exist"
}
```
- invalid language code
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "lang parameter does not contain a valid ISO 639-1 language code"
}
```
### Creating a File With the App Provider
**Endpoint**: specified in the capabilities in `new_file_url`, currently `/app/new`
**Method**: HTTP POST
**Authentication** (one of them):
- `Authorization` header with OIDC Bearer token for authenticated users or basic auth credentials (if enabled in КуСфера)
- `Public-Token` header with public link token for public links
- `X-Access-Token` header with a REVA token for authenticated users
**Query parameters**:
- `parent_container_id` (mandatory): ID of the folder in which the file will be created
- `filename` (mandatory): name of the new file
- `template` (optional): not yet implemented
**Request examples**:
```bash
curl -X POST 'https://qsfera.test/app/new?parent_container_id=c2lkOmNpZAo=&filename=test.odt'
```
**Response example**:
You will receive a file id of the freshly created file, which you can use to open the file in an editor.
```json
{
"file_id": "ZmlsZTppZAo="
}
```
**Example responses (error case)**:
- missing `parent_container_id`
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "missing parent container ID"
}
```
- missing `filename`
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "missing filename"
}
```
- parent container not found
HTTP status code: 404
```json
{
"code": "RESOURCE_NOT_FOUND",
"message": "the parent container is not accessible or does not exist"
}
```
- `parent_container_id` does not point to a container
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "the parent container id does not point to a container"
}
```
- `filename` is invalid (e.g. includes a path segment)
HTTP status code: 400
```json
{
"code": "INVALID_PARAMETER",
"message": "the filename must not contain a path segment"
}
```
- file already exists
HTTP status code: 403
```json
{
"code": "RESOURCE_ALREADY_EXISTS",
"message": "the file already exists"
}
```
@@ -0,0 +1,54 @@
package command
import (
"fmt"
"net/http"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/app-registry/pkg/config"
"github.com/qsfera/server/services/app-registry/pkg/config/parser"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "check health status",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
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
},
}
}
@@ -0,0 +1,35 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/app-registry/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return []*cobra.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 qsfera app-registry command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "app-registry",
Short: "Provide an app registry for КуСфера",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
@@ -0,0 +1,96 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/runner"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/app-registry/pkg/config"
"github.com/qsfera/server/services/app-registry/pkg/config/parser"
"github.com/qsfera/server/services/app-registry/pkg/revaconfig"
"github.com/qsfera/server/services/app-registry/pkg/server/debug"
"github.com/opencloud-eu/reva/v2/cmd/revad/runtime"
"github.com/spf13/cobra"
)
// Server is the entry point for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
gr := runner.NewGroup()
{
// run the appropriate reva servers based on the config
rCfg := revaconfig.AppRegistryConfigFromStruct(cfg, logger)
if rServer := runtime.NewDrivenHTTPServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rhttp", rServer))
}
if rServer := runtime.NewDrivenGRPCServerWithOptions(rCfg,
runtime.WithLogger(&logger.Logger),
runtime.WithRegistry(registry.GetRegistry()),
runtime.WithTraceProvider(traceProvider),
); rServer != nil {
gr.Add(runner.NewRevaServiceRunner(cfg.Service.Name+".rgrpc", rServer))
}
}
{
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(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
}
grpcSvc := registry.BuildGRPCService(cfg.GRPC.Namespace+"."+cfg.Service.Name, cfg.GRPC.Protocol, cfg.GRPC.Addr, version.GetString())
if err := registry.RegisterService(ctx, logger, grpcSvc, cfg.Debug.Addr); err != nil {
logger.Fatal().Err(err).Msg("failed to register the grpc service")
}
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,49 @@
package command
import (
"fmt"
"os"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/app-registry/pkg/config"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.GRPC.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 := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
@@ -0,0 +1,57 @@
package config
import (
"context"
"github.com/qsfera/server/pkg/shared"
)
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;APP_REGISTRY_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Debug Debug `yaml:"debug"`
GRPC GRPCConfig `yaml:"grpc"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
AppRegistry AppRegistry `yaml:"app_registry"`
Context context.Context `yaml:"-"`
}
type Service struct {
Name string `yaml:"-"`
}
type Debug struct {
Addr string `yaml:"addr" env:"APP_REGISTRY_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"APP_REGISTRY_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"APP_REGISTRY_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"APP_REGISTRY_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
type GRPCConfig struct {
Addr string `yaml:"addr" env:"APP_REGISTRY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"OC_GRPC_PROTOCOL;APP_REGISTRY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"1.0.0"`
}
type AppRegistry struct {
MimeTypeConfig []MimeTypeConfig `yaml:"mimetypes"`
}
type MimeTypeConfig struct {
MimeType string `yaml:"mime_type" mapstructure:"mime_type"`
Extension string `yaml:"extension" mapstructure:"extension"`
Name string `yaml:"name" mapstructure:"name"`
Description string `yaml:"description" mapstructure:"description"`
Icon string `yaml:"icon" mapstructure:"icon"`
DefaultApp string `yaml:"default_app" mapstructure:"default_app"`
AllowCreation bool `yaml:"allow_creation" mapstructure:"allow_creation"`
}
@@ -0,0 +1,147 @@
package defaults
import (
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/app-registry/pkg/config"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9243",
Token: "",
Pprof: false,
Zpages: false,
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9242",
Namespace: "qsfera.api",
Protocol: "tcp",
},
Service: config.Service{
Name: "app-registry",
},
Reva: shared.DefaultRevaConfig(),
}
}
func defaultMimeTypeConfig() []config.MimeTypeConfig {
return []config.MimeTypeConfig{
{
MimeType: "application/pdf",
Extension: "pdf",
Name: "PDF",
Description: "PDF document",
},
{
MimeType: "application/vnd.oasis.opendocument.text",
Extension: "odt",
Name: "Document",
Description: "OpenDocument text document",
AllowCreation: true,
},
{
MimeType: "application/vnd.oasis.opendocument.spreadsheet",
Extension: "ods",
Name: "Spreadsheet",
Description: "OpenDocument spreadsheet document",
AllowCreation: true,
},
{
MimeType: "application/vnd.oasis.opendocument.presentation",
Extension: "odp",
Name: "Presentation",
Description: "OpenDocument presentation document",
AllowCreation: true,
},
{
MimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
Extension: "docx",
Name: "Microsoft Word",
Description: "Microsoft Word document",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.form",
Extension: "docxf",
Name: "Form Document",
Description: "Form Document",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Extension: "xlsx",
Name: "Microsoft Excel",
Description: "Microsoft Excel document",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
Extension: "pptx",
Name: "Microsoft PowerPoint",
Description: "Microsoft PowerPoint document",
},
{
MimeType: "application/vnd.jupyter",
Extension: "ipynb",
Name: "Jupyter Notebook",
Description: "Jupyter Notebook",
},
{
MimeType: "text/markdown",
Extension: "md",
Name: "Markdown file",
Description: "Markdown file",
AllowCreation: true,
},
{
MimeType: "application/compressed-markdown",
Extension: "zmd",
Name: "Compressed markdown file",
Description: "Compressed markdown file",
},
{
MimeType: "application/vnd.geogebra.slides",
Extension: "ggs",
Name: "GeoGebra Slides",
Description: "GeoGebra Slides",
},
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.Reva == nil && cfg.Commons != nil {
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}
}
// Sanitize the config
func Sanitize(cfg *config.Config) {
if cfg.AppRegistry.MimeTypeConfig == nil {
cfg.AppRegistry.MimeTypeConfig = defaultMimeTypeConfig()
}
}
@@ -0,0 +1,42 @@
package parser
import (
"errors"
occfg "github.com/qsfera/server/pkg/config"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/app-registry/pkg/config"
"github.com/qsfera/server/services/app-registry/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.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 {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
return nil
}
@@ -0,0 +1,6 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;APP_REGISTRY_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,54 @@
package revaconfig
import (
"github.com/mitchellh/mapstructure"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/app-registry/pkg/config"
)
// AppRegistryConfigFromStruct will adapt a КуСфера config struct into a reva mapstructure to start a reva service.
func AppRegistryConfigFromStruct(cfg *config.Config, logger log.Logger) map[string]any {
rcfg := map[string]any{
"shared": map[string]any{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address,
"grpc_client_options": cfg.Reva.GetGRPCClientConfig(),
"multi_tenant_enabled": cfg.Commons.MultiTenantEnabled,
},
"grpc": map[string]any{
"network": cfg.GRPC.Protocol,
"address": cfg.GRPC.Addr,
"tls_settings": map[string]any{
"enabled": cfg.GRPC.TLS.Enabled,
"certificate": cfg.GRPC.TLS.Cert,
"key": cfg.GRPC.TLS.Key,
},
"services": map[string]any{
"appregistry": map[string]any{
"driver": "static",
"drivers": map[string]any{
"static": map[string]any{
"mime_types": mimetypes(cfg, logger),
},
},
},
},
"interceptors": map[string]any{
"prometheus": map[string]any{
"namespace": "qsfera",
"subsystem": "app_registry",
},
},
},
}
return rcfg
}
func mimetypes(cfg *config.Config, logger log.Logger) []map[string]any {
var m []map[string]any
if err := mapstructure.Decode(cfg.AppRegistry.MimeTypeConfig, &m); err != nil {
logger.Error().Err(err).Msg("Failed to decode appregistry mimetypes to mapstructure")
return nil
}
return m
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/app-registry/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
}
}
@@ -0,0 +1,27 @@
package debug
import (
"net/http"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/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.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
}