diff --git a/internal/component/dis/info.go b/internal/component/dis/info.go index 7fd8e1b..e9cf567 100644 --- a/internal/component/dis/info.go +++ b/internal/component/dis/info.go @@ -1,26 +1,70 @@ package dis import ( + "encoding/json" "net/http" + "github.com/FAU-CDI/wisski-distillery/internal/component/instances" "github.com/tkw1536/goprogram/stream" + "golang.org/x/sync/errgroup" ) -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 +func (dis *Dis) info(io stream.IOStream) (http.Handler, error) { + return http.HandlerFunc(dis.handleDis), nil +} + +const disLimit = 2 + +func (dis *Dis) handleDis(w http.ResponseWriter, r *http.Request) { + // make sure the user is authorized + if !dis.authDis(r) { + w.Header().Add("WWW-Authenticate", `Basic realm="WissKI Distillery Admin"`) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + return + } + + // create a new error group + var errgroup errgroup.Group + errgroup.SetLimit(disLimit) + + // list all the instances + all, err := dis.Instances.All() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return + } + + // get all of their info! + infos := make([]instances.Info, len(all)) + for i, instance := range all { + { + i := i + instance := instance + + errgroup.Go(func() (err error) { + infos[i], err = instance.Info() + return err + }) + } + } + + // if some info call failed + if err := errgroup.Wait(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + w.Write([]byte("\n")) + return + } + + // and return the json + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(infos) +} + +func (dis *Dis) authDis(r *http.Request) bool { + user, pass, ok := r.BasicAuth() + return ok && user == dis.Config.DisAdminUser && pass == dis.Config.DisAdminPassword } diff --git a/internal/component/dis/stack/Dockerfile b/internal/component/dis/stack/Dockerfile index 94134c9..272586a 100644 --- a/internal/component/dis/stack/Dockerfile +++ b/internal/component/dis/stack/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/alpine +FROM docker.io/library/docker:20.10-cli COPY wdcli /wdcli EXPOSE 8888 diff --git a/internal/component/dis/stack/docker-compose.yml b/internal/component/dis/stack/docker-compose.yml index 8192431..b0d028f 100644 --- a/internal/component/dis/stack/docker-compose.yml +++ b/internal/component/dis/stack/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.7" services: - wdresolve: + dis: build: . restart: always environment: @@ -16,6 +16,8 @@ services: LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} volumes: + # TODO: Mount docker socket properly! + - "/var/run/docker.sock:/var/run/docker.sock" - "${CONFIG_PATH}:${CONFIG_PATH}:ro" - "${DEPLOY_ROOT}:${DEPLOY_ROOT}:ro" - "${GLOBAL_AUTHORIZED_KEYS_FILE}:${GLOBAL_AUTHORIZED_KEYS_FILE}:ro" diff --git a/internal/component/instances/wisski_status.go b/internal/component/instances/wisski_status.go new file mode 100644 index 0000000..b8ddff4 --- /dev/null +++ b/internal/component/instances/wisski_status.go @@ -0,0 +1,22 @@ +package instances + +import "github.com/tkw1536/goprogram/stream" + +// Info represents some info about this WissKI +type Info struct { + Slug string // The slug of the instance + + Running bool // is the instance running? +} + +// Info returns info about this instance +func (wisski *WissKI) Info() (info Info, err error) { + info.Slug = wisski.Slug + + ps, err := wisski.Barrel().Ps(stream.FromNil()) + if err != nil { + return + } + info.Running = len(ps) > 0 + return +} diff --git a/internal/component/stack.go b/internal/component/stack.go index 3039ba1..c0a943d 100644 --- a/internal/component/stack.go +++ b/internal/component/stack.go @@ -2,6 +2,8 @@ package component import ( + "bufio" + "bytes" "errors" "github.com/FAU-CDI/wisski-distillery/pkg/execx" @@ -119,6 +121,38 @@ func (ds Stack) Restart(io stream.IOStream) error { return nil } +var errStackPs = errors.New("Stack.Ps: Down returned non-zero exit code") + +// Ps returns the ids of the containers currently running +func (ds Stack) Ps(io stream.IOStream) ([]string, error) { + // create a buffer + var buffer bytes.Buffer + + // read the ids from the command! + code, err := ds.compose(io.Streams(&buffer, nil, nil, 0), "ps", "-q") + if err != nil { + return nil, err + } + if code != 0 { + return nil, errStackPs + } + + // scan each of the lines + var results []string + scanner := bufio.NewScanner(&buffer) + for scanner.Scan() { + if text := scanner.Text(); text != "" { + results = append(results, text) + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + + // return them! + return results, nil +} + var errStackDown = errors.New("Stack.Down: Down returned non-zero exit code") // Down stops and removes all containers in this Stack. diff --git a/internal/config/config.go b/internal/config/config.go index 2aabbc3..bb46338 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -74,7 +74,11 @@ type Config struct { // admin credentials for the Mysql database MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"` - MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"admin" parser:"nonempty"` + MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"" parser:"nonempty"` + + // admin credentials for the dis server + DisAdminUser string `env:"DIS_ADMIN_USER" default:"admin" parser:"nonempty"` + DisAdminPassword string `env:"DIS_ADMIN_PASSWORD" default:"" parser:"nonempty"` // ConfigPath is the path this configuration was loaded from (if any) ConfigPath string diff --git a/internal/config/config_template b/internal/config/config_template index 7484748..bd61ba4 100644 --- a/internal/config/config_template +++ b/internal/config/config_template @@ -58,6 +58,10 @@ GLOBAL_AUTHORIZED_KEYS_FILE=${AUTHORIZED_KEYS_FILE} GRAPHDB_ADMIN_USER=${GRAPHDB_ADMIN_USER} GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD} -# The admin password to use for access to mysql +# The admin user and password of the MySQL interface, to be used for provisioning MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER} MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD} + +# The admin user and password required to access the /dis/ server and api +DIS_ADMIN_USER=${DIS_ADMIN_USER} +DIS_ADMIN_PASSWORD=${DIS_ADMIN_PASSWORD} diff --git a/internal/config/template.go b/internal/config/template.go index f3cee91..2195c90 100644 --- a/internal/config/template.go +++ b/internal/config/template.go @@ -14,7 +14,7 @@ import ( _ "embed" ) -// Template is a template for the cofiguration file +// Template is a template for the configuration file type Template struct { DeployRoot string `env:"DEPLOY_ROOT"` DefaultDomain string `env:"DEFAULT_DOMAIN"` @@ -24,6 +24,8 @@ type Template struct { TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD"` MysqlAdminUsername string `env:"MYSQL_ADMIN_USER"` MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD"` + DisAdminUsername string `env:"DIS_ADMIN_USER"` + DisAdminPassword string `env:"DIS_ADMIN_PASSWORD"` } // SetDefaults sets defaults on the template @@ -66,6 +68,17 @@ func (tpl *Template) SetDefaults() (err error) { } } + if tpl.DisAdminUsername == "" { + tpl.DisAdminUsername = "admin" + } + + if tpl.DisAdminPassword == "" { + tpl.DisAdminPassword, err = password.Password(64) + if err != nil { + return err + } + } + return nil }