view pkg/geoserver/boot.go @ 904:e4b72a199258

New default bottleneck colors Mainly to make the stroke color one actually selectable in the ui. In addition the pink does better match the collors used on the ECDIS layer.
author Sascha Wilde <wilde@intevation.de>
date Tue, 02 Oct 2018 13:34:59 +0200
parents aa8f30c1ed27
children da526b58c9c4
line wrap: on
line source

package geoserver

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

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

const (
	workspaceName  = "gemma"
	datastoreName  = "gemma"
	databaseScheme = "waterway"
	databaseType   = "postgis"
)

const (
	startupSQL = `SELECT public.setrole('${user,'||encode('waterway_user', 'hex')||'}')`
	closeupSQL = `RESET ROLE`
)

func basicAuth(user, password string) func(req *http.Request) {
	return func(req *http.Request) {
		req.SetBasicAuth(user, password)
	}
}

func asJSON(req *http.Request) {
	req.Header.Set("Content-Type", "application/json")
}

func ensureWorkspace() error {
	var (
		url      = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	// Probe  workspace.
	req, err := http.NewRequest(
		http.MethodGet,
		url+"/rest/workspaces/"+workspaceName+".json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusNotFound {
		log.Println("info: workspace " + workspaceName + " already exists.")
		return nil
	}

	// Create workspace

	log.Println("info: creating workspace " + workspaceName)

	const createJSON = `{"workspace":{"name":"` + workspaceName + `"}}`

	req, err = http.NewRequest(
		http.MethodPost,
		url+"/rest/workspaces",
		strings.NewReader(createJSON))
	if err != nil {
		return err
	}
	auth(req)
	asJSON(req)
	if resp, err = http.DefaultClient.Do(req); err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated {
		err = fmt.Errorf("Status code '%s' (%d)",
			http.StatusText(resp.StatusCode),
			resp.StatusCode)
	}

	return err
}

func ensureDataStore() error {
	var (
		url      = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	// Probe datastore.
	req, err := http.NewRequest(
		http.MethodGet,
		url+"/rest/workspaces/"+workspaceName+"/datastores/"+datastoreName+".json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusNotFound {
		log.Println("info: datastore " + datastoreName + " already exists.")
		return nil
	}

	// Create datastore.
	log.Println("info: creating datastore " + datastoreName)

	type entry struct {
		Key   interface{} `json:"@key"`
		Value interface{} `json:"$"`
	}

	// Create datastore.
	ds := map[string]interface{}{
		"dataStore": map[string]interface{}{
			"name": datastoreName,
			"connectionParameters": map[string]interface{}{
				"entry": []entry{
					{"host", config.DBHost()},
					{"port", config.DBPort()},
					{"database", config.DBName()},
					{"schema", databaseScheme},
					{"user", config.DBUser()},
					{"passwd", config.DBPassword()},
					{"dbtype", databaseType},
					{"Session startup SQL", startupSQL},
					{"Session close-up SQL", closeupSQL},
				},
			},
		},
	}
	var out bytes.Buffer
	enc := json.NewEncoder(&out)
	if err := enc.Encode(&ds); err != nil {
		return err
	}

	req, err = http.NewRequest(
		http.MethodPost,
		url+"/rest/workspaces/"+workspaceName+"/datastores",
		bytes.NewReader(out.Bytes()))
	if err != nil {
		return err
	}
	auth(req)
	asJSON(req)
	resp, err = http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated {
		err = fmt.Errorf("Status code '%s' (%d)",
			http.StatusText(resp.StatusCode),
			resp.StatusCode)
	}

	return err
}

func ensureFeatures() error {
	var (
		url      = config.GeoServerURL()
		user     = config.GeoServerUser()
		password = config.GeoServerPassword()
		auth     = basicAuth(user, password)
	)

	tables := models.InternalServices.Filter(models.IntWFS)
	if len(tables) == 0 {
		log.Println("info: no tables to publish")
		return nil
	}

	log.Printf("info: number of tables to publish %d\n", len(tables))

	var features struct {
		FeatureTypes struct {
			FeatureType []struct {
				Name string `json:"name"`
			} `json:"featureType"`
		} `json:"featureTypes"`
	}

	hasFeature := func(name string) bool {
		for _, ft := range features.FeatureTypes.FeatureType {
			if ft.Name == name {
				return true
			}
		}
		return false
	}

	// Fetch all featuretypes.
	req, err := http.NewRequest(
		http.MethodGet,
		url+"/rest/workspaces/"+workspaceName+
			"/datastores/"+datastoreName+
			"/featuretypes.json",
		nil)
	if err != nil {
		return err
	}
	auth(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}

	err = json.NewDecoder(resp.Body).Decode(&features)
	resp.Body.Close()
	if err != nil {
		// XXX: Quirk in the JSON return by GeoServer:
		// If there are no features in the datastore
		// featureType deserializes to an empty string ""
		// instead of an empty array *palmface*.
		// So assume there no features.
		hasFeature = func(string) bool { return false }
	}

	for i := range tables {
		table := tables[i].Name

		if hasFeature(table) {
			log.Printf("info: featuretype %s already exists.\n", table)
			continue
		}

		// Create featuretype.
		log.Printf("info: creating featuretype %s.\n", table)

		// Create featuretype
		ft := map[string]interface{}{
			"featureType": map[string]interface{}{
				"name":       table,
				"nativeName": table,
				"title":      table,
			},
		}

		var out bytes.Buffer
		enc := json.NewEncoder(&out)
		if err := enc.Encode(&ft); err != nil {
			return err
		}

		req, err := http.NewRequest(
			http.MethodPost,
			url+"/rest/workspaces/"+workspaceName+
				"/datastores/"+datastoreName+
				"/featuretypes",
			bytes.NewReader(out.Bytes()))
		if err != nil {
			return err
		}
		auth(req)
		asJSON(req)
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return err
		}

		if resp.StatusCode != http.StatusCreated {
			return fmt.Errorf("Status code '%s' (%d)",
				http.StatusText(resp.StatusCode),
				resp.StatusCode)
		}
	}

	return nil
}

func prepareGeoServer() error {

	if config.DBUser() == "" {
		log.Println("info: Need metamorphic db user to configure GeoServer")
		return nil
	}

	if config.GeoServerURL() == "" {
		log.Println("info: No tables to publish on GeoServer")
		return nil
	}

	if err := ensureWorkspace(); err != nil {
		return err
	}

	if err := ensureDataStore(); err != nil {
		return err
	}

	// TODO: Styles

	return ensureFeatures()
}

func ConfigureBoot() {
	log.Println("Configure GeoServer...")
	const maxTries = 10
	const sleep = time.Second * 5

	for try := 1; try <= maxTries; try++ {
		err := prepareGeoServer()
		if err == nil {
			break
		}
		if try < maxTries {
			if uerr, ok := err.(*url.Error); ok {
				if oerr, ok := uerr.Err.(*net.OpError); ok && oerr.Op == "dial" {
					log.Printf("Failed attempt %d of %d to configure GeoServer. "+
						"Will try again in %s...\n", try, maxTries, sleep)
					time.Sleep(sleep)
					continue
				}
			}
		}
		log.Printf("warn: configure GeoServer failed: %v\n", err)
		break
	}
}