view pkg/controllers/system.go @ 3929:45be361f2d48

If the settings for sounding diffs are changed in a way that they need recalculation, flush the cache.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Thu, 11 Jul 2019 18:54:22 +0200
parents 3fcc4e11fc00
children 49564382ffff
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha Wilde <sascha.wilde@intevation.de>

package controllers

import (
	"bytes"
	"context"
	"database/sql"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
	"sync"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/pkg/auth"
	"gemma.intevation.de/gemma/pkg/config"
	"gemma.intevation.de/gemma/pkg/geoserver"
	"gemma.intevation.de/gemma/pkg/models"
)

const (
	getFeatureColourSQL = `SELECT r,g,b,a
FROM systemconf.feature_colours
WHERE feature_name = $1 AND style_attr = $2`
	setFeatureColourSQL = `UPDATE systemconf.feature_colours
SET (r, g, b, a) = ($3, $4, $5, $6)
WHERE feature_name = $1 AND style_attr = $2`

	getSettingsSQL = `
SELECT config_key, config_val
FROM sys_admin.system_config`

	getConfigSQL = `
SELECT config_val
FROM sys_admin.system_config
WHERE config_key = $1`

	updateSettingSQL = `
INSERT INTO sys_admin.system_config (config_key, config_val)
VALUES ($1, $2)
ON CONFLICT (config_key) DO UPDATE SET config_val = $2`

	deleteSoundingDiffsSQL = `
DELETE FROM caching.sounding_differences`
)

// System status end points

func showSystemLog(
	_ interface{}, req *http.Request,
	_ *sql.Conn,
) (jr JSONResult, err error) {

	serviceName := mux.Vars(req)["service"]
	fileName := mux.Vars(req)["file"]

	// The following check is currently most likely unnecessary as I wasn't
	// able to inject a verbatim '/' via the middleware, but better be on
	// the safe site...
	if strings.Contains(fileName, "/") {
		err = JSONError{http.StatusBadRequest,
			"error: no slashes allowed in file name"}
		return
	}

	var path string

	switch serviceName {
	case "apache2", "postgresql":
		path = "/var/log/" + serviceName + "/" + fileName
	default:
		err = JSONError{http.StatusBadRequest,
			"error: invalid service: " + serviceName}
		return
	}

	var txt []byte

	if txt, err = ioutil.ReadFile(path); err != nil {
		return
	}

	jr = JSONResult{
		Result: struct {
			Path    string `json:"path"`
			Content string `json:"content"`
		}{path, string(txt)},
	}
	return
}

func getSystemConfig(
	_ interface{}, req *http.Request,
	_ *sql.Conn,
) (jr JSONResult, err error) {

	cfg := config.PublishedConfig()
	if cfg == "" {
		jr = JSONResult{Result: strings.NewReader("{}")}
		return
	}

	var data []byte
	if data, err = ioutil.ReadFile(cfg); err != nil {
		return
	}

	jr = JSONResult{Result: bytes.NewReader(data)}
	return
}

func getSystemSettings(
	_ interface{},
	req *http.Request,
	conn *sql.Conn,
) (jr JSONResult, err error) {

	var rows *sql.Rows
	if rows, err = conn.QueryContext(req.Context(), getSettingsSQL); err != nil {
		return
	}
	defer rows.Close()

	settings := map[string]string{}

	for rows.Next() {
		var key, val string
		if err = rows.Scan(&key, &val); err != nil {
			return
		}
		settings[key] = val
	}
	if err = rows.Err(); err != nil {
		return
	}

	jr = JSONResult{Result: settings}
	return
}

type reconfFunc func(sql.NullString, string) (func(), error)

var (
	reconfigureFuncsMu sync.Mutex
	reconfigureFuncs   = map[string]reconfFunc{}
)

func registerReconfigureFunc(key string, fn reconfFunc) {
	reconfigureFuncsMu.Lock()
	defer reconfigureFuncsMu.Unlock()
	reconfigureFuncs[key] = fn
}

func reconfigureFunc(key string) reconfFunc {
	reconfigureFuncsMu.Lock()
	defer reconfigureFuncsMu.Unlock()
	return reconfigureFuncs[key]
}

func reconfigureClassBreaks(old sql.NullString, curr, which string, recalc func()) (func(), error) {

	// If new values are broken, don't proceed.
	currCVs, err := models.ParseColorValues(curr)
	if err != nil {
		return nil, err
	}

	doBoth := func() {
		log.Printf("info: Trigger re-calculation of %s.", which)
		geoserver.ReconfigureStyle(which)
		recalc()
	}

	if !old.Valid {
		return doBoth, nil
	}

	oldCVs, err := models.ParseColorValues(old.String)
	if err != nil {
		log.Printf("warn: old config value is broken: %v\n", err)
		return doBoth, nil
	}

	if len(currCVs) != len(oldCVs) {
		return doBoth, nil
	}

	colorChanged := false

	for i := range currCVs {
		if currCVs[i].Value != oldCVs[i].Value {
			return doBoth, nil
		}
		if currCVs[i].Color != oldCVs[i].Color {
			colorChanged = true
		}
	}

	// Only the color changed -> no expensive recalc needed.
	if colorChanged {
		log.Println("info: Only colors changed.")
		return func() { geoserver.ReconfigureStyle(which) }, nil
	}

	return nil, nil
}

func init() {
	registerReconfigureFunc("morphology_classbreaks",
		func(old sql.NullString, curr string) (func(), error) {
			return reconfigureClassBreaks(
				old, curr,
				"sounding_results_contour_lines_geoserver",
				func() {
					log.Println(
						"todo: Trigger expensive recalculation of sounding result contours.")
				})
		})
	registerReconfigureFunc("morphology_classbreaks_compare",
		func(old sql.NullString, curr string) (func(), error) {
			return reconfigureClassBreaks(
				old, curr,
				"sounding_differences",
				func() { go deleteSoundingDiffs() })
		})
}

func deleteSoundingDiffs() {
	// TODO: Better do that in import queue?
	ctx := context.Background()

	if err := auth.RunAs(ctx, "sys_admin",
		func(conn *sql.Conn) error {
			_, err := conn.ExecContext(ctx, deleteSoundingDiffsSQL)
			return err
		},
	); err != nil {
		log.Printf("error: Cleaning sounding diffs cache failed: %v\n", err)
	}
}

func setSystemSettings(
	input interface{},
	req *http.Request,
	conn *sql.Conn,
) (jr JSONResult, err error) {

	settings := input.(*map[string]string)

	ctx := req.Context()
	var tx *sql.Tx
	if tx, err = conn.BeginTx(ctx, nil); err != nil {
		return
	}
	defer tx.Rollback()

	var setStmt *sql.Stmt
	if setStmt, err = tx.PrepareContext(ctx, updateSettingSQL); err != nil {
		return
	}
	defer setStmt.Close()
	var getStmt *sql.Stmt
	if getStmt, err = tx.PrepareContext(ctx, getConfigSQL); err != nil {
		return
	}
	defer getStmt.Close()

	reconfigure := map[string]func(){}

	for key, value := range *settings {
		var old sql.NullString
		err = getStmt.QueryRowContext(ctx, key).Scan(&old)
		switch {
		case err == sql.ErrNoRows:
			old.Valid, err = false, nil
		case err != nil:
			return
		}

		if cmp := reconfigureFunc(key); cmp != nil {
			var fn func()
			if fn, err = cmp(old, value); err != nil {
				return
			}
			if fn != nil {
				reconfigure[key] = fn
			}
		}

		if _, err = setStmt.ExecContext(ctx, key, value); err != nil {
			return
		}
	}

	if err = tx.Commit(); err != nil {
		return
	}

	for _, fn := range reconfigure {
		fn()
	}

	jr = JSONResult{
		Code: http.StatusCreated,
		Result: struct {
			Result string `json:"result"`
		}{"success"},
	}
	return
}

// Map/Feature style end points

func getFeatureStyle(
	_ interface{},
	req *http.Request,
	db *sql.Conn,
) (jr JSONResult, err error) {

	feature := mux.Vars(req)["feature"]
	attr := mux.Vars(req)["attr"]

	c := models.Colour{}
	err = db.QueryRowContext(
		req.Context(),
		getFeatureColourSQL,
		feature, attr,
	).Scan(&c.R, &c.G, &c.B, &c.A)

	switch {
	case err == sql.ErrNoRows:
		err = JSONError{
			Code:    http.StatusNotFound,
			Message: "Requestes style not found.",
		}
		return
	case err != nil:
		return
	}

	jr.Result = &struct {
		Colour models.Colour `json:"colour"`
		Code   string        `json:"code"`
	}{c, fmt.Sprintf("rgba(%d, %d, %d, %g)", c.R, c.G, c.B, c.A)}
	return
}

func setFeatureStyle(
	input interface{},
	req *http.Request,
	db *sql.Conn,
) (jr JSONResult, err error) {

	feature := mux.Vars(req)["feature"]
	attr := mux.Vars(req)["attr"]

	c := input.(*models.Colour)
	if !c.IsValid() {
		err = JSONError{http.StatusBadRequest, "error: invalid colours"}
		return
	}

	var res sql.Result
	res, err = db.ExecContext(
		req.Context(),
		setFeatureColourSQL,
		feature, attr,
		c.R, c.G, c.B, c.A)

	if err != nil {
		return
	}

	if n, err2 := res.RowsAffected(); err2 == nil && n == 0 {
		err = JSONError{
			Code:    http.StatusNotFound,
			Message: "Requestes style not found.",
		}
		return
	}

	jr = JSONResult{
		Code: http.StatusCreated,
		Result: struct {
			Result string `json:"result"`
		}{"success"},
	}
	return
}