From f5f2ac1a036a64a79a7f677957d7f41a27a981b2 Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 15 Sep 2022 15:04:35 +0200 Subject: [PATCH] Merge all the server components --- cmd/{servers.go => server.go} | 42 ++---- cmd/update_prefix_config.go | 6 +- cmd/wdcli/main.go | 3 +- internal/component/dis/dis.go | 9 +- internal/component/dis/info.go | 26 ++++ internal/component/dis/resolver.go | 61 ++++++++ internal/component/dis/self.go | 137 ++++++++++++++++++ internal/component/dis/server.go | 39 +++++ internal/component/dis/stack/Dockerfile | 2 +- .../component/dis/stack/docker-compose.yml | 1 - internal/component/installable.go | 2 +- internal/component/instances/wisski_create.go | 7 +- internal/component/resolver/resolver.env | 10 -- internal/component/resolver/resolver.go | 109 -------------- internal/component/resolver/stack/Dockerfile | 5 - .../resolver/stack/docker-compose.yml | 29 ---- internal/component/self/self.env | 7 - internal/component/self/self.go | 42 ------ .../component/self/stack/docker-compose.yml | 28 ---- internal/config/config.go | 2 +- internal/config/domains.go | 16 ++ internal/wisski/component.go | 66 ++++----- internal/wisski/server.go | 33 ----- pkg/lazy/lazy.go | 27 ++++ pkg/stringparser/stringparser.go | 8 +- 25 files changed, 365 insertions(+), 352 deletions(-) rename cmd/{servers.go => server.go} (52%) create mode 100644 internal/component/dis/info.go create mode 100644 internal/component/dis/resolver.go create mode 100644 internal/component/dis/self.go create mode 100644 internal/component/dis/server.go delete mode 100644 internal/component/resolver/resolver.env delete mode 100644 internal/component/resolver/resolver.go delete mode 100644 internal/component/resolver/stack/Dockerfile delete mode 100644 internal/component/resolver/stack/docker-compose.yml delete mode 100644 internal/component/self/self.env delete mode 100644 internal/component/self/self.go delete mode 100644 internal/component/self/stack/docker-compose.yml delete mode 100644 internal/wisski/server.go create mode 100644 pkg/lazy/lazy.go diff --git a/cmd/servers.go b/cmd/server.go similarity index 52% rename from cmd/servers.go rename to cmd/server.go index 579145e..23d3949 100644 --- a/cmd/servers.go +++ b/cmd/server.go @@ -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 } diff --git a/cmd/update_prefix_config.go b/cmd/update_prefix_config.go index 2b31eaf..8a7dcb0 100644 --- a/cmd/update_prefix_config.go +++ b/cmd/update_prefix_config.go @@ -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) } diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go index 23d11f3..bc53c8c 100644 --- a/cmd/wdcli/main.go +++ b/cmd/wdcli/main.go @@ -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. diff --git a/internal/component/dis/dis.go b/internal/component/dis/dis.go index 49d7a03..2181454 100644 --- a/internal/component/dis/dis.go +++ b/internal/component/dis/dis.go @@ -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}, }) } diff --git a/internal/component/dis/info.go b/internal/component/dis/info.go new file mode 100644 index 0000000..7fd8e1b --- /dev/null +++ b/internal/component/dis/info.go @@ -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 +} diff --git a/internal/component/dis/resolver.go b/internal/component/dis/resolver.go new file mode 100644 index 0000000..967ebc9 --- /dev/null +++ b/internal/component/dis/resolver.go @@ -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 +} diff --git a/internal/component/dis/self.go b/internal/component/dis/self.go new file mode 100644 index 0000000..214d37c --- /dev/null +++ b/internal/component/dis/self.go @@ -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) +} diff --git a/internal/component/dis/server.go b/internal/component/dis/server.go new file mode 100644 index 0000000..765fb45 --- /dev/null +++ b/internal/component/dis/server.go @@ -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 +} diff --git a/internal/component/dis/stack/Dockerfile b/internal/component/dis/stack/Dockerfile index 7e87236..94134c9 100644 --- a/internal/component/dis/stack/Dockerfile +++ b/internal/component/dis/stack/Dockerfile @@ -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"] \ No newline at end of file +CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","server","--bind","0.0.0.0:8888"] \ No newline at end of file diff --git a/internal/component/dis/stack/docker-compose.yml b/internal/component/dis/stack/docker-compose.yml index ebec445..8192431 100644 --- a/internal/component/dis/stack/docker-compose.yml +++ b/internal/component/dis/stack/docker-compose.yml @@ -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} diff --git a/internal/component/installable.go b/internal/component/installable.go index c503e7a..70af86b 100644 --- a/internal/component/installable.go +++ b/internal/component/installable.go @@ -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! diff --git a/internal/component/instances/wisski_create.go b/internal/component/instances/wisski_create.go index 412c23a..ab34fd6 100644 --- a/internal/component/instances/wisski_create.go +++ b/internal/component/instances/wisski_create.go @@ -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 } diff --git a/internal/component/resolver/resolver.env b/internal/component/resolver/resolver.env deleted file mode 100644 index b9778fc..0000000 --- a/internal/component/resolver/resolver.env +++ /dev/null @@ -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} \ No newline at end of file diff --git a/internal/component/resolver/resolver.go b/internal/component/resolver/resolver.go deleted file mode 100644 index b8b17de..0000000 --- a/internal/component/resolver/resolver.go +++ /dev/null @@ -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 -} diff --git a/internal/component/resolver/stack/Dockerfile b/internal/component/resolver/stack/Dockerfile deleted file mode 100644 index bd8ce40..0000000 --- a/internal/component/resolver/stack/Dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/internal/component/resolver/stack/docker-compose.yml b/internal/component/resolver/stack/docker-compose.yml deleted file mode 100644 index c879869..0000000 --- a/internal/component/resolver/stack/docker-compose.yml +++ /dev/null @@ -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 diff --git a/internal/component/self/self.env b/internal/component/self/self.env deleted file mode 100644 index f93d03e..0000000 --- a/internal/component/self/self.env +++ /dev/null @@ -1,7 +0,0 @@ -VIRTUAL_HOST=${VIRTUAL_HOST} - -LETSENCRYPT_HOST=${LETSENCRYPT_HOST} -LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL} - -TARGET=${TARGET} -OVERRIDES_FILE=${OVERRIDES_FILE} diff --git a/internal/component/self/self.go b/internal/component/self/self.go deleted file mode 100644 index e9aef1e..0000000 --- a/internal/component/self/self.go +++ /dev/null @@ -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, - }, - }) -} diff --git a/internal/component/self/stack/docker-compose.yml b/internal/component/self/stack/docker-compose.yml deleted file mode 100644 index a61e245..0000000 --- a/internal/component/self/stack/docker-compose.yml +++ /dev/null @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index fb8872b..2aabbc3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) diff --git a/internal/config/domains.go b/internal/config/domains.go index 8f68dc8..906c0f5 100644 --- a/internal/config/domains.go +++ b/internal/config/domains.go @@ -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 "" +} diff --git a/internal/wisski/component.go b/internal/wisski/component.go index 498529f..4547948 100644 --- a/internal/wisski/component.go +++ b/internal/wisski/component.go @@ -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() }) diff --git a/internal/wisski/server.go b/internal/wisski/server.go deleted file mode 100644 index da06a36..0000000 --- a/internal/wisski/server.go +++ /dev/null @@ -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") - } -} diff --git a/pkg/lazy/lazy.go b/pkg/lazy/lazy.go new file mode 100644 index 0000000..9ac2517 --- /dev/null +++ b/pkg/lazy/lazy.go @@ -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 +} diff --git a/pkg/stringparser/stringparser.go b/pkg/stringparser/stringparser.go index 951f729..bd56ce7 100644 --- a/pkg/stringparser/stringparser.go +++ b/pkg/stringparser/stringparser.go @@ -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)