Merge all the server components
This commit is contained in:
parent
85b5603d9d
commit
f5f2ac1a03
25 changed files with 365 additions and 352 deletions
|
|
@ -8,44 +8,22 @@ import (
|
|||
"github.com/tkw1536/goprogram/exit"
|
||||
)
|
||||
|
||||
// ResolverServer is the 'resolver_server' command
|
||||
var ResolverServer wisski_distillery.Command = server{
|
||||
Desc: wisski_distillery.Description{
|
||||
Requirements: core.Requirements{
|
||||
NeedsDistillery: true,
|
||||
},
|
||||
Command: "resolver_server",
|
||||
Description: "Starts a global resolver server",
|
||||
},
|
||||
Server: func(context wisski_distillery.Context) (http.Handler, error) {
|
||||
return context.Environment.Resolver().Server(context.IOStream)
|
||||
},
|
||||
}
|
||||
|
||||
// DisServer is the 'dis_server' command
|
||||
var DisServer wisski_distillery.Command = server{
|
||||
Desc: wisski_distillery.Description{
|
||||
Requirements: core.Requirements{
|
||||
NeedsDistillery: true,
|
||||
},
|
||||
Command: "dis_server",
|
||||
Description: "Starts a server with information about this distillery",
|
||||
},
|
||||
Server: func(context wisski_distillery.Context) (http.Handler, error) {
|
||||
return context.Environment.Server(), nil
|
||||
},
|
||||
}
|
||||
// Server is the 'server' command
|
||||
var Server wisski_distillery.Command = server{}
|
||||
|
||||
type server struct {
|
||||
Prefix string `short:"p" long:"prefix" description:"prefix to listen under"`
|
||||
Bind string `short:"b" long:"bind" description:"address to listen on" default:"127.0.0.1:8888"`
|
||||
|
||||
Desc wisski_distillery.Description
|
||||
Server func(context wisski_distillery.Context) (http.Handler, error)
|
||||
}
|
||||
|
||||
func (s server) Description() wisski_distillery.Description {
|
||||
return s.Desc
|
||||
return wisski_distillery.Description{
|
||||
Requirements: core.Requirements{
|
||||
NeedsDistillery: true,
|
||||
},
|
||||
Command: "server",
|
||||
Description: "Starts a server with information about this distillery",
|
||||
}
|
||||
}
|
||||
|
||||
var errServerListen = exit.Error{
|
||||
|
|
@ -54,7 +32,7 @@ var errServerListen = exit.Error{
|
|||
}
|
||||
|
||||
func (s server) Run(context wisski_distillery.Context) error {
|
||||
handler, err := s.Server(context)
|
||||
handler, err := context.Environment.Dis().Server(context.IOStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -38,8 +38,8 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error {
|
|||
return errPrefixUpdateFailed.WithMessageF(err)
|
||||
}
|
||||
|
||||
resolver := dis.Resolver()
|
||||
target := resolver.ConfigPath()
|
||||
ddis := dis.Dis()
|
||||
target := ddis.ResolverConfigPath()
|
||||
|
||||
// print the configuration
|
||||
config, err := os.OpenFile(target, os.O_WRONLY, fs.ModePerm)
|
||||
|
|
@ -70,7 +70,7 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error {
|
|||
|
||||
// and restart the resolver to apply the config!
|
||||
logging.LogMessage(context.IOStream, "restarting resolver stack")
|
||||
if err := resolver.Stack().Restart(context.IOStream); err != nil {
|
||||
if err := ddis.Stack().Restart(context.IOStream); err != nil {
|
||||
return errPrefixUpdateFailed.WithMessageF(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ func init() {
|
|||
wdcli.Register(cmd.Monday)
|
||||
|
||||
// servers
|
||||
wdcli.Register(cmd.DisServer)
|
||||
wdcli.Register(cmd.ResolverServer)
|
||||
wdcli.Register(cmd.Server)
|
||||
}
|
||||
|
||||
// an error when no arguments are provided.
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import (
|
|||
"embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
||||
)
|
||||
|
||||
type Dis struct {
|
||||
component.ComponentBase
|
||||
|
||||
Instances *instances.Instances
|
||||
|
||||
ResolverFile string
|
||||
}
|
||||
|
||||
func (dis Dis) Name() string {
|
||||
|
|
@ -35,7 +40,9 @@ func (dis Dis) Stack() component.Installable {
|
|||
"GLOBAL_AUTHORIZED_KEYS_FILE": dis.Config.GlobalAuthorizedKeysFile,
|
||||
"SELF_OVERRIDES_FILE": dis.Config.SelfOverridesFile,
|
||||
},
|
||||
CopyContextFiles: []string{dis.Config.CurrentExecutable()},
|
||||
|
||||
TouchFiles: []string{dis.ResolverFile},
|
||||
CopyContextFiles: []string{core.Executable},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
26
internal/component/dis/info.go
Normal file
26
internal/component/dis/info.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package dis
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
func (dis Dis) info(io stream.IOStream) (http.Handler, error) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
all, err := dis.Instances.All()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("internal server error"))
|
||||
io.EPrintln(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, wk := range all {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(wk.Slug))
|
||||
w.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
}), nil
|
||||
}
|
||||
61
internal/component/dis/resolver.go
Normal file
61
internal/component/dis/resolver.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package dis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/FAU-CDI/wdresolve"
|
||||
"github.com/FAU-CDI/wdresolve/resolvers"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
func (dis Dis) ResolverConfigPath() string {
|
||||
return filepath.Join(dis.Dir, dis.ResolverFile)
|
||||
}
|
||||
|
||||
func (dis Dis) resolver(io stream.IOStream) (p wdresolve.ResolveHandler, err error) {
|
||||
p.TrustXForwardedProto = true
|
||||
|
||||
fallback := &resolvers.Regexp{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
|
||||
// handle the default domain name!
|
||||
domainName := dis.Config.DefaultDomain
|
||||
if domainName != "" {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering default domain %s\n", domainName)
|
||||
}
|
||||
|
||||
// handle the extra domains!
|
||||
for _, domain := range dis.Config.SelfExtraDomains {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering legacy domain %s\n", domain)
|
||||
}
|
||||
|
||||
// open the prefix file
|
||||
prefixFile := dis.ResolverConfigPath()
|
||||
fs, err := os.Open(prefixFile)
|
||||
io.Println("loading prefixes from ", prefixFile)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
// read the prefixes
|
||||
// TODO: Do we want to load these without a file?
|
||||
prefixes, err := resolvers.ReadPrefixes(fs)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// and use that as the resolver!
|
||||
p.Resolver = resolvers.InOrder{
|
||||
prefixes,
|
||||
fallback,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
137
internal/component/dis/self.go
Normal file
137
internal/component/dis/self.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package dis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// self returns the handler for the self overrides
|
||||
func (dis Dis) self(io stream.IOStream) (redirect Redirect, err error) {
|
||||
// open the overrides file
|
||||
overrides, err := os.Open(dis.Config.SelfOverridesFile)
|
||||
io.Printf("loading overrides from %q\n", dis.Config.SelfOverridesFile)
|
||||
if err != nil {
|
||||
return redirect, err
|
||||
}
|
||||
defer overrides.Close()
|
||||
|
||||
// decode the overrides file
|
||||
if err := json.NewDecoder(overrides).Decode(&redirect.Overrides); err != nil {
|
||||
return redirect, err
|
||||
}
|
||||
|
||||
if redirect.Overrides == nil {
|
||||
redirect.Overrides = make(map[string]string)
|
||||
}
|
||||
redirect.Overrides["/"] = dis.Config.SelfRedirect.String()
|
||||
|
||||
// create a redirect server
|
||||
redirect.Fallback, err = dis.selfFallback()
|
||||
if err != nil {
|
||||
return redirect, err
|
||||
}
|
||||
redirect.Absolute = false
|
||||
redirect.Overrides = nil
|
||||
redirect.Permanent = false
|
||||
|
||||
// and return!
|
||||
return redirect, nil
|
||||
}
|
||||
|
||||
func (dis *Dis) selfFallback() (http.Handler, error) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
dis.serveFallback(w, r)
|
||||
}), nil
|
||||
}
|
||||
|
||||
var notFoundText = []byte("not found")
|
||||
|
||||
func (dis *Dis) serveFallback(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
slug := dis.Config.SlugFromHost(r.Host)
|
||||
if slug == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write(notFoundText)
|
||||
return
|
||||
}
|
||||
|
||||
if ok, _ := dis.Instances.Has(slug); !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "WissKI %q not found\n", slug)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
fmt.Fprintf(w, "WissKI %q is currently offline\n", slug)
|
||||
|
||||
}
|
||||
|
||||
// Redirect implements a redirect server that redirects all requests.
|
||||
// It implements http.Handler.
|
||||
type Redirect struct {
|
||||
// Target is the target URL to redirect to.
|
||||
Target string
|
||||
|
||||
// Fallback is used when target is the empty string.
|
||||
Fallback http.Handler
|
||||
|
||||
// Absolute determines if the request path should be appended to the target URL when redirecting.
|
||||
// By default this path is always appended, set Absolute to true to prevent this.
|
||||
Absolute bool
|
||||
|
||||
// Overrides is a map from paths to URLs that should override the default target.
|
||||
Overrides map[string]string
|
||||
|
||||
// Permanent determines if the redirect responses issued should return
|
||||
// Permanent Redirect (Status Code 308) or Temporary Redirect (Status Code 307).
|
||||
Permanent bool
|
||||
}
|
||||
|
||||
// Redirect determines the redirect URL for a specific incoming request
|
||||
// If it returns the empty string, the fallback is used.
|
||||
func (redirect Redirect) Redirect(r *http.Request) string {
|
||||
// if we have an override for this URL, use it immediatly
|
||||
url := strings.TrimSuffix(r.URL.Path, "/")
|
||||
if override, ok := redirect.Overrides[url]; ok {
|
||||
return override
|
||||
}
|
||||
|
||||
if redirect.Target == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// if we are in absolute redirect mode, always return the absolute URL
|
||||
if redirect.Absolute {
|
||||
return redirect.Target
|
||||
}
|
||||
|
||||
// return the target + the redirected URL
|
||||
dest := strings.TrimSuffix(redirect.Target, "/") + r.URL.Path
|
||||
if len(r.URL.RawQuery) > 0 {
|
||||
dest += "?" + r.URL.RawQuery
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface and redirects a single request to redirect.Target.
|
||||
func (redirect Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
dest := redirect.Redirect(r)
|
||||
if dest == "" {
|
||||
redirect.Fallback.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// determine if we are temporary or permanent redirect
|
||||
status := http.StatusTemporaryRedirect
|
||||
if redirect.Permanent {
|
||||
status = http.StatusPermanentRedirect
|
||||
}
|
||||
|
||||
// and do the redirect
|
||||
http.Redirect(w, r, dest, status)
|
||||
}
|
||||
39
internal/component/dis/server.go
Normal file
39
internal/component/dis/server.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package dis
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// Server returns an http.Mux that implements the main server instance
|
||||
func (dis Dis) Server(io stream.IOStream) (http.Handler, error) {
|
||||
// self server
|
||||
self, err := dis.self(io)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolver, err := dis.resolver(io)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := dis.info(io)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// resolver
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", self)
|
||||
|
||||
mux.Handle("/go/", resolver)
|
||||
mux.Handle("/wisski/navigate", resolver)
|
||||
|
||||
// TODO: Fix me!
|
||||
mux.Handle("/dis/", info)
|
||||
|
||||
return mux, nil
|
||||
}
|
||||
|
|
@ -2,4 +2,4 @@ FROM docker.io/library/alpine
|
|||
|
||||
COPY wdcli /wdcli
|
||||
EXPOSE 8888
|
||||
CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","dis_server","--bind","0.0.0.0:8888"]
|
||||
CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","server","--bind","0.0.0.0:8888"]
|
||||
|
|
@ -8,7 +8,6 @@ services:
|
|||
# port and hostname for this image to use
|
||||
VIRTUAL_HOST: ${VIRTUAL_HOST}
|
||||
VIRTUAL_PORT: 8888
|
||||
VIRTUAL_PATH: /dis/
|
||||
|
||||
CONFIG_PATH: ${CONFIG_PATH}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func (is Installable) Install(io stream.IOStream, context InstallationContext) e
|
|||
// find the source!
|
||||
src, ok := context[name]
|
||||
if !ok {
|
||||
return errors.Errorf("Missing file from context: %s", src)
|
||||
return errors.Errorf("Missing file from context: %q", src)
|
||||
}
|
||||
|
||||
// find the destination!
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@ var errInvalidSlug = errors.New("not a valid slug")
|
|||
//
|
||||
// It does not perform any checks if the instance already exists, or does the creation in the database.
|
||||
func (instances *Instances) Create(slug string) (wisski WissKI, err error) {
|
||||
wisski.instances = instances
|
||||
|
||||
// make sure that the slug is valid!
|
||||
if _, err := stringparser.ParseSlug(slug); err != nil {
|
||||
slug, err = stringparser.ParseSlug(slug)
|
||||
if err != nil {
|
||||
return wisski, errInvalidSlug
|
||||
}
|
||||
|
||||
wisski.Instance.Slug = slug
|
||||
wisski.Instance.FilesystemBase = filepath.Join(instances.Dir, slug)
|
||||
wisski.Instance.FilesystemBase = filepath.Join(instances.Dir, wisski.Domain())
|
||||
|
||||
wisski.Instance.OwnerEmail = ""
|
||||
wisski.Instance.AutoBlindUpdateEnabled = true
|
||||
|
|
@ -60,7 +62,6 @@ func (instances *Instances) Create(slug string) (wisski WissKI, err error) {
|
|||
}
|
||||
|
||||
// store the instance in the object and return it!
|
||||
wisski.instances = instances
|
||||
return wisski, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
VIRTUAL_HOST=${VIRTUAL_HOST}
|
||||
|
||||
LETSENCRYPT_HOST=${LETSENCRYPT_HOST}
|
||||
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
|
||||
|
||||
CONFIG_PATH=${CONFIG_PATH}
|
||||
DEPLOY_ROOT=${DEPLOY_ROOT}
|
||||
GLOBAL_AUTHORIZED_KEYS_FILE=${GLOBAL_AUTHORIZED_KEYS_FILE}
|
||||
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
|
||||
RESOLVER_CONFIG=${RESOLVER_CONFIG}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/FAU-CDI/wdresolve"
|
||||
"github.com/FAU-CDI/wdresolve/resolvers"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// TODO: Add a 'self-server' concept!
|
||||
|
||||
type Resolver struct {
|
||||
component.ComponentBase
|
||||
|
||||
ConfigName string // the name to the config file
|
||||
Executable string // path to the current executable
|
||||
}
|
||||
|
||||
func (Resolver) Name() string {
|
||||
return "resolver"
|
||||
}
|
||||
|
||||
func (resolver Resolver) ConfigPath() string {
|
||||
return filepath.Join(resolver.Dir, resolver.ConfigName)
|
||||
}
|
||||
|
||||
//go:embed all:stack resolver.env
|
||||
var resources embed.FS
|
||||
|
||||
func (resolver Resolver) Stack() component.Installable {
|
||||
return resolver.ComponentBase.MakeStack(component.Installable{
|
||||
Resources: resources,
|
||||
ContextPath: "stack",
|
||||
EnvPath: "resolver.env",
|
||||
|
||||
EnvContext: map[string]string{
|
||||
"VIRTUAL_HOST": resolver.Config.DefaultHost(),
|
||||
"LETSENCRYPT_HOST": resolver.Config.DefaultSSLHost(),
|
||||
"LETSENCRYPT_EMAIL": resolver.Config.CertbotEmail,
|
||||
|
||||
"CONFIG_PATH": resolver.Config.ConfigPath,
|
||||
"DEPLOY_ROOT": resolver.Config.DeployRoot,
|
||||
|
||||
"GLOBAL_AUTHORIZED_KEYS_FILE": resolver.Config.GlobalAuthorizedKeysFile,
|
||||
"SELF_OVERRIDES_FILE": resolver.Config.SelfOverridesFile,
|
||||
"RESOLVER_CONFIG": resolver.ConfigPath(),
|
||||
},
|
||||
TouchFiles: []string{resolver.ConfigName},
|
||||
CopyContextFiles: []string{core.Executable},
|
||||
})
|
||||
}
|
||||
|
||||
func (resolver Resolver) Context(parent component.InstallationContext) component.InstallationContext {
|
||||
return component.InstallationContext{
|
||||
core.Executable: resolver.Executable,
|
||||
}
|
||||
}
|
||||
|
||||
func (resolver Resolver) Server(io stream.IOStream) (p wdresolve.ResolveHandler, err error) {
|
||||
p.TrustXForwardedProto = true
|
||||
|
||||
fallback := &resolvers.Regexp{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
|
||||
// handle the default domain name!
|
||||
domainName := resolver.Config.DefaultDomain
|
||||
if domainName != "" {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering default domain %s\n", domainName)
|
||||
}
|
||||
|
||||
// handle the extra domains!
|
||||
for _, domain := range resolver.Config.SelfExtraDomains {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering legacy domain %s\n", domain)
|
||||
}
|
||||
|
||||
// open the prefix file
|
||||
prefixFile := resolver.ConfigPath()
|
||||
fs, err := os.Open(prefixFile)
|
||||
io.Println("loading prefixes from ", prefixFile)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
// read the prefixes
|
||||
// TODO: Do we want to load these without a file?
|
||||
prefixes, err := resolvers.ReadPrefixes(fs)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// and use that as the resolver!
|
||||
p.Resolver = resolvers.InOrder{
|
||||
prefixes,
|
||||
fallback,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
FROM docker.io/library/alpine
|
||||
|
||||
COPY wdcli /wdcli
|
||||
EXPOSE 8888
|
||||
CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","resolver_server","--bind","0.0.0.0:8888"]
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
wdresolve:
|
||||
build: .
|
||||
restart: always
|
||||
environment:
|
||||
# port and hostname for this image to use
|
||||
VIRTUAL_HOST: ${VIRTUAL_HOST}
|
||||
VIRTUAL_PORT: 8888
|
||||
VIRTUAL_PATH: /go/
|
||||
|
||||
CONFIG_PATH: ${CONFIG_PATH}
|
||||
|
||||
# optional letsencrypt email
|
||||
LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}
|
||||
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
|
||||
|
||||
volumes:
|
||||
- "${CONFIG_PATH}:${CONFIG_PATH}:ro"
|
||||
- "${DEPLOY_ROOT}:${DEPLOY_ROOT}:ro"
|
||||
- "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro"
|
||||
- "${SELF_OVERRIDES_FILE}:${SELF_OVERRIDES_FILE}:ro"
|
||||
- "${RESOLVER_CONFIG}:${RESOLVER_CONFIG}:ro"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: distillery
|
||||
external: true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
VIRTUAL_HOST=${VIRTUAL_HOST}
|
||||
|
||||
LETSENCRYPT_HOST=${LETSENCRYPT_HOST}
|
||||
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
|
||||
|
||||
TARGET=${TARGET}
|
||||
OVERRIDES_FILE=${OVERRIDES_FILE}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package self
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
)
|
||||
|
||||
type Self struct {
|
||||
component.ComponentBase
|
||||
}
|
||||
|
||||
func (Self) Name() string {
|
||||
return "self"
|
||||
}
|
||||
|
||||
//go:embed all:stack
|
||||
//go:embed self.env
|
||||
var resources embed.FS
|
||||
|
||||
func (self Self) Stack() component.Installable {
|
||||
// TODO: Move me into config!
|
||||
TARGET := "https://github.com/FAU-CDI/wisski-distillery"
|
||||
if self.Config.SelfRedirect != nil { // TODO: move to config!
|
||||
TARGET = self.Config.SelfRedirect.String()
|
||||
}
|
||||
|
||||
return self.ComponentBase.MakeStack(component.Installable{
|
||||
Resources: resources,
|
||||
|
||||
ContextPath: "stack",
|
||||
EnvPath: "self.env",
|
||||
|
||||
EnvContext: map[string]string{
|
||||
"VIRTUAL_HOST": self.Config.DefaultHost(),
|
||||
"LETSENCRYPT_HOST": self.Config.DefaultSSLHost(),
|
||||
"LETSENCRYPT_EMAIL": self.Config.CertbotEmail,
|
||||
"TARGET": TARGET,
|
||||
"OVERRIDES_FILE": self.Config.SelfOverridesFile,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
tr:
|
||||
image: ghcr.io/tkw1536/tr:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- "${OVERRIDES_FILE}:/overrides.json:ro"
|
||||
environment:
|
||||
# port and hostname for this image to use
|
||||
VIRTUAL_HOST: ${VIRTUAL_HOST}
|
||||
VIRTUAL_PORT: 8080
|
||||
VIRTUAL_PATH: /
|
||||
|
||||
# optional letsencrypt email
|
||||
LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}
|
||||
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
|
||||
|
||||
# the overrides file
|
||||
OVERRIDES: /overrides.json
|
||||
|
||||
# where to redirect to
|
||||
TARGET: ${TARGET}
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: distillery
|
||||
external: true
|
||||
|
|
@ -26,7 +26,7 @@ type Config struct {
|
|||
|
||||
// By default, the default domain redirects to the distillery repository.
|
||||
// If you want to change this, set an alternate domain name here.
|
||||
SelfRedirect *url.URL `env:"SELF_REDIRECT" default:"" parser:"https_url"`
|
||||
SelfRedirect *url.URL `env:"SELF_REDIRECT" default:"https://github.com/FAU-CDI/wisski-distillery" parser:"https_url"`
|
||||
|
||||
// By default, only the 'self' domain above is caught.
|
||||
// To catch additional domains, add them here (comma seperated)
|
||||
|
|
|
|||
|
|
@ -39,3 +39,19 @@ func (cfg Config) DefaultHost() string {
|
|||
func (cfg Config) DefaultSSLHost() string {
|
||||
return cfg.IfHttps(cfg.DefaultHost())
|
||||
}
|
||||
|
||||
// SlugFromHost returns the slug belonging to the appropriate host.
|
||||
func (cfg Config) SlugFromHost(host string) (slug string) {
|
||||
// extract an ':port' that happens to be in the host.
|
||||
domain, _, _ := strings.Cut(host, ":")
|
||||
|
||||
// check all the possible domain endings
|
||||
for _, suffix := range append([]string{cfg.DefaultDomain}, cfg.SelfExtraDomains...) {
|
||||
if strings.HasSuffix(domain, "."+suffix) {
|
||||
return domain[:len(domain)-len(suffix)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// no domain found!
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/dis"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/resolver"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/self"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/ssh"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/triplestore"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/component/web"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
|
||||
)
|
||||
|
||||
// components holds the various components of the distillery
|
||||
|
|
@ -24,16 +23,14 @@ import (
|
|||
type components struct {
|
||||
|
||||
// installable components
|
||||
web *web.Web
|
||||
self *self.Self
|
||||
resolver *resolver.Resolver
|
||||
dis *dis.Dis
|
||||
ssh *ssh.SSH
|
||||
ts *triplestore.Triplestore
|
||||
sql *sql.SQL
|
||||
web lazy.Lazy[*web.Web]
|
||||
dis lazy.Lazy[*dis.Dis]
|
||||
ssh lazy.Lazy[*ssh.SSH]
|
||||
ts lazy.Lazy[*triplestore.Triplestore]
|
||||
sql lazy.Lazy[*sql.SQL]
|
||||
|
||||
// other components
|
||||
instances *instances.Instances
|
||||
instances lazy.Lazy[*instances.Instances]
|
||||
}
|
||||
|
||||
// makeComponent makes or returns a component inside the [component] struct of the distillery
|
||||
|
|
@ -45,7 +42,7 @@ type components struct {
|
|||
// init is called with a new non-nil component to initialize it. It may be nil, to indicate no initialization is required.
|
||||
//
|
||||
// makeComponent returns the new or existing component instance
|
||||
func makeComponent[C component.Component](dis *Distillery, field *C, init func(C)) C {
|
||||
func makeComponent[C component.Component](dis *Distillery, field *lazy.Lazy[C], init func(C)) C {
|
||||
|
||||
// get the typeof C and make sure that it is a pointer type!
|
||||
typC := reflect.TypeOf((*C)(nil)).Elem()
|
||||
|
|
@ -53,32 +50,27 @@ func makeComponent[C component.Component](dis *Distillery, field *C, init func(C
|
|||
panic("makeComponent: C must be backed by a pointer")
|
||||
}
|
||||
|
||||
// if the component is non-nil, then it has already been initialized
|
||||
if !reflect.ValueOf(*field).IsNil() {
|
||||
return *field
|
||||
}
|
||||
// return the field
|
||||
return field.Get(func() (c C) {
|
||||
c = reflect.New(typC.Elem()).Interface().(C)
|
||||
if init != nil {
|
||||
init(c)
|
||||
}
|
||||
|
||||
// create a new element, and call the initializer (if requested)
|
||||
*field = reflect.New(typC.Elem()).Interface().(C)
|
||||
if init != nil {
|
||||
init(*field)
|
||||
}
|
||||
base := c.Base()
|
||||
base.Config = dis.Config
|
||||
if base.Dir == "" {
|
||||
base.Dir = filepath.Join(dis.Config.DeployRoot, "core", c.Name())
|
||||
}
|
||||
|
||||
// apply the base configuration
|
||||
base := (*field).Base()
|
||||
base.Config = dis.Config
|
||||
base.Dir = filepath.Join(dis.Config.DeployRoot, "core", (*field).Name())
|
||||
|
||||
// and eventually return it
|
||||
return *field
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// Components returns all components that have a stack function
|
||||
func (dis *Distillery) Components() []component.InstallableComponent {
|
||||
return []component.InstallableComponent{
|
||||
dis.Web(),
|
||||
dis.Self(),
|
||||
dis.Resolver(),
|
||||
dis.Dis(),
|
||||
dis.SSH(),
|
||||
dis.Triplestore(),
|
||||
|
|
@ -90,18 +82,11 @@ func (dis *Distillery) Web() *web.Web {
|
|||
return makeComponent(dis, &dis.components.web, nil)
|
||||
}
|
||||
|
||||
func (dis *Distillery) Self() *self.Self {
|
||||
return makeComponent(dis, &dis.components.self, nil)
|
||||
}
|
||||
|
||||
func (dis *Distillery) Resolver() *resolver.Resolver {
|
||||
return makeComponent(dis, &dis.components.resolver, func(resolver *resolver.Resolver) {
|
||||
resolver.ConfigName = core.PrefixConfig
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Distillery) Dis() *dis.Dis {
|
||||
return makeComponent(d, &d.components.dis, nil)
|
||||
return makeComponent(d, &d.components.dis, func(ddis *dis.Dis) {
|
||||
ddis.ResolverFile = core.PrefixConfig
|
||||
ddis.Instances = d.Instances()
|
||||
})
|
||||
}
|
||||
|
||||
func (dis *Distillery) SSH() *ssh.SSH {
|
||||
|
|
@ -126,6 +111,7 @@ func (dis *Distillery) Triplestore() *triplestore.Triplestore {
|
|||
|
||||
func (dis *Distillery) Instances() *instances.Instances {
|
||||
return makeComponent(dis, &dis.components.instances, func(instances *instances.Instances) {
|
||||
instances.Dir = filepath.Join(dis.Config.DeployRoot, "instances")
|
||||
instances.SQL = dis.SQL()
|
||||
instances.TS = dis.Triplestore()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
package wisski
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// TODO: Move this into dis!
|
||||
|
||||
// Server represents a server for this distillery
|
||||
type Server struct {
|
||||
dis *Distillery
|
||||
}
|
||||
|
||||
func (dis *Distillery) Server() *Server {
|
||||
return &Server{
|
||||
dis: dis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
instances, err := s.dis.Instances().All()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
io.WriteString(w, "Something went wrong")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
for _, instance := range instances {
|
||||
io.WriteString(w, instance.Slug+"\n")
|
||||
}
|
||||
}
|
||||
27
pkg/lazy/lazy.go
Normal file
27
pkg/lazy/lazy.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package lazy
|
||||
|
||||
import "sync"
|
||||
|
||||
// Lazy is an object that a lazily-initialized value of type T.
|
||||
//
|
||||
// A Lazy must not be copied after first use.
|
||||
type Lazy[T any] struct {
|
||||
once sync.Once
|
||||
value T
|
||||
}
|
||||
|
||||
// Get returns the value associated with this Lazy.
|
||||
//
|
||||
// If no other call to Get has started or completed an initialization, initializes the value using the init function.
|
||||
// Otherwise, it returns the initialized value.
|
||||
//
|
||||
// If init panics, the initization is considered to be completed.
|
||||
// Future calls to Get() do not invoke init, and the zero value of T is returned.
|
||||
//
|
||||
// Get may safely be called concurrently.
|
||||
func (lazy *Lazy[T]) Get(init func() T) T {
|
||||
lazy.once.Do(func() {
|
||||
lazy.value = init()
|
||||
})
|
||||
return lazy.value
|
||||
}
|
||||
|
|
@ -47,20 +47,20 @@ func ParseNonEmpty(s string) (string, error) {
|
|||
|
||||
var regexpDomain = regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||
|
||||
// ParseValidDomain checks that s is a valid domain and returns it as-is
|
||||
// ParseValidDomain checks that s is a valid domain and returns it in lowercase
|
||||
func ParseValidDomain(s string) (string, error) {
|
||||
if !regexpDomain.MatchString(s) {
|
||||
return "", errors.Errorf("%q is not a valid domain", s)
|
||||
}
|
||||
return s, nil
|
||||
return strings.ToLower(s), nil
|
||||
}
|
||||
|
||||
// ParseValidDomains checks that s is a comma-seperated list of valid domains and returns them as-is
|
||||
// ParseValidDomains checks that s is a comma-seperated list of valid domains and returns them in lower case
|
||||
func ParseValidDomains(s string) ([]string, error) {
|
||||
if len(s) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
domains := strings.Split(s, ",")
|
||||
domains := strings.Split(strings.ToLower(s), ",")
|
||||
for _, d := range domains {
|
||||
if !regexpDomain.MatchString(d) {
|
||||
return nil, errors.Errorf("%q is not a valid domain", d)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue