refactor to go-restful
This commit is contained in:
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
@ -61,56 +60,6 @@ func hostByIP(w http.ResponseWriter, r *http.Request) (*localconfig.Host, *local
|
||||
return host, cfg
|
||||
}
|
||||
|
||||
func serveHosts(w http.ResponseWriter, r *http.Request) {
|
||||
if !authorizeHosts(r) {
|
||||
forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
renderJSON(w, cfg.Hosts)
|
||||
}
|
||||
|
||||
func serveHost(w http.ResponseWriter, r *http.Request) {
|
||||
if !authorizeHosts(r) {
|
||||
forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
match := reHost.FindStringSubmatch(r.URL.Path)
|
||||
if match == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
hostName, what := match[1], match[2]
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
host := cfg.Host(hostName)
|
||||
|
||||
if host == nil {
|
||||
host = cfg.HostByMAC(hostName)
|
||||
}
|
||||
|
||||
if host == nil {
|
||||
log.Printf("no host with name or MAC %q", hostName)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
renderHost(w, r, what, host, cfg)
|
||||
}
|
||||
|
||||
func renderHost(w http.ResponseWriter, r *http.Request, what string, host *localconfig.Host, cfg *localconfig.Config) {
|
||||
ctx, err := newRenderContext(host, cfg)
|
||||
if err != nil {
|
||||
@ -171,66 +120,6 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
|
||||
}
|
||||
}
|
||||
|
||||
func renderJSON(w http.ResponseWriter, v interface{}) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func serveClusters(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
clusterNames := make([]string, len(cfg.Clusters))
|
||||
for i, cluster := range cfg.Clusters {
|
||||
clusterNames[i] = cluster.Name
|
||||
}
|
||||
|
||||
renderJSON(w, clusterNames)
|
||||
}
|
||||
|
||||
func serveCluster(w http.ResponseWriter, r *http.Request) {
|
||||
// "/clusters/<name>/<what>" split => "", "clusters", "<name>", "<what>"
|
||||
p := strings.Split(r.URL.Path, "/")
|
||||
|
||||
if len(p) != 4 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
clusterName := p[2]
|
||||
what := p[3]
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
log.Print("failed to read config: ", err)
|
||||
http.Error(w, "", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
cluster := cfg.Cluster(clusterName)
|
||||
if cluster == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch what {
|
||||
case "addons":
|
||||
if len(cluster.Addons) == 0 {
|
||||
log.Printf("cluster %q has no addons defined", clusterName)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(cluster.Addons))
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, err error) {
|
||||
log.Print("request failed: ", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"github.com/mcluseau/go-swagger-ui"
|
||||
"novit.nc/direktil/pkg/cas"
|
||||
|
||||
"novit.nc/direktil/local-server/pkg/apiutils"
|
||||
@ -39,16 +40,7 @@ func main() {
|
||||
restful.Add(buildWS())
|
||||
})
|
||||
|
||||
// by default, serve a host resource by its IP
|
||||
//http.HandleFunc("/", serveHostByIP)
|
||||
|
||||
//http.HandleFunc("/configs", uploadConfig)
|
||||
|
||||
http.HandleFunc("/hosts", serveHosts)
|
||||
//http.HandleFunc("/hosts/", serveHost)
|
||||
|
||||
http.HandleFunc("/clusters", serveClusters)
|
||||
http.HandleFunc("/clusters/", serveCluster)
|
||||
swaggerui.HandleAt("/swagger-ui/")
|
||||
|
||||
if *address != "" {
|
||||
log.Print("HTTP listening on ", *address)
|
||||
|
41
cmd/dkl-local-server/ws-auth.go
Normal file
41
cmd/dkl-local-server/ws-auth.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
func adminAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
tokenAuth(req, resp, chain, *adminToken)
|
||||
}
|
||||
|
||||
func hostsAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
tokenAuth(req, resp, chain, *hostsToken, *adminToken)
|
||||
}
|
||||
|
||||
func tokenAuth(req *restful.Request, resp *restful.Response, chain *restful.FilterChain, allowedTokens ...string) {
|
||||
token := getToken(req)
|
||||
|
||||
for _, allowedToken := range allowedTokens {
|
||||
if allowedToken == "" || token == allowedToken {
|
||||
chain.ProcessFilter(req, resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.WriteErrorString(401, "401: Not Authorized")
|
||||
return
|
||||
}
|
||||
|
||||
func getToken(req *restful.Request) string {
|
||||
const bearerPrefix = "Bearer "
|
||||
|
||||
token := req.HeaderParameter("Authorization")
|
||||
|
||||
if !strings.HasPrefix(token, bearerPrefix) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return token[len(bearerPrefix):]
|
||||
}
|
63
cmd/dkl-local-server/ws-clusters.go
Normal file
63
cmd/dkl-local-server/ws-clusters.go
Normal file
@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
func wsListClusters(req *restful.Request, resp *restful.Response) {
|
||||
cfg := wsReadConfig(resp)
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
clusterNames := make([]string, len(cfg.Clusters))
|
||||
for i, cluster := range cfg.Clusters {
|
||||
clusterNames[i] = cluster.Name
|
||||
}
|
||||
|
||||
resp.WriteEntity(clusterNames)
|
||||
}
|
||||
|
||||
func wsReadCluster(req *restful.Request, resp *restful.Response) (cluster *localconfig.Cluster) {
|
||||
clusterName := req.PathParameter("cluster-name")
|
||||
|
||||
cfg := wsReadConfig(resp)
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cluster = cfg.Cluster(clusterName)
|
||||
if cluster == nil {
|
||||
wsNotFound(req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func wsCluster(req *restful.Request, resp *restful.Response) {
|
||||
cluster := wsReadCluster(req, resp)
|
||||
if cluster == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(cluster)
|
||||
}
|
||||
|
||||
func wsClusterAddons(req *restful.Request, resp *restful.Response) {
|
||||
cluster := wsReadCluster(req, resp)
|
||||
if cluster == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(cluster.Addons) == 0 {
|
||||
log.Printf("cluster %q has no addons defined", cluster.Name)
|
||||
wsNotFound(req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write([]byte(cluster.Addons))
|
||||
}
|
88
cmd/dkl-local-server/ws-configs.go
Normal file
88
cmd/dkl-local-server/ws-configs.go
Normal file
@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
r := req.Request
|
||||
w := resp.ResponseWriter
|
||||
|
||||
if !authorizeAdmin(r) {
|
||||
forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Remove(out.Name())
|
||||
|
||||
_, err = io.Copy(out, r.Body)
|
||||
out.Close()
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
archivesPath := filepath.Join(*dataDir, "archives")
|
||||
cfgPath := configFilePath()
|
||||
|
||||
err = os.MkdirAll(archivesPath, 0700)
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
backupPath := filepath.Join(archivesPath, "config."+ulid()+".yaml.gz")
|
||||
|
||||
bck, err := os.Create(backupPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer bck.Close()
|
||||
|
||||
in, err := os.Open(cfgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gz, err := gzip.NewWriterLevel(bck, 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(gz, in)
|
||||
gz.Close()
|
||||
in.Close()
|
||||
return
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(out.Name(), cfgPath)
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
@ -13,39 +12,53 @@ type wsHost struct {
|
||||
getHost func(req *restful.Request) string
|
||||
}
|
||||
|
||||
func (ws *wsHost) register(rws *restful.WebService) {
|
||||
for _, what := range []string{
|
||||
"boot.img",
|
||||
"boot.img.gz",
|
||||
"boot.img.lz4",
|
||||
"boot.iso",
|
||||
"boot.tar",
|
||||
"config",
|
||||
"initrd",
|
||||
"ipxe",
|
||||
"kernel",
|
||||
func (ws *wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBuilder)) {
|
||||
b := func(what string) *restful.RouteBuilder {
|
||||
return rws.GET(ws.prefix + "/" + what).To(ws.render)
|
||||
}
|
||||
|
||||
for _, rb := range []*restful.RouteBuilder{
|
||||
// raw configuration
|
||||
b("config").Doc("Get the host's configuration"),
|
||||
|
||||
// metal/local HDD install
|
||||
b("boot.img").Doc("Get the host's boot disk image"),
|
||||
b("boot.img.gz").Doc("Get the host's boot disk image (gzip compressed)"),
|
||||
b("boot.img.lz4").Doc("Get the host's boot disk image (lz4 compressed)"),
|
||||
|
||||
// metal/local HDD upgrades
|
||||
b("boot.tar").Doc("Get the host's /boot archive (ie: for metal upgrades)"),
|
||||
|
||||
// read-only ISO support
|
||||
b("boot.iso").Doc("Get the host's boot CD-ROM image"),
|
||||
|
||||
// netboot support
|
||||
b("ipxe").Doc("Get the host's IPXE code (for netboot)"),
|
||||
b("kernel").Doc("Get the host's kernel (ie: for netboot)"),
|
||||
b("initrd").Doc("Get the host's initial RAM disk (ie: for netboot)"),
|
||||
} {
|
||||
rws.Route(rws.GET(ws.prefix + "/" + what).To(ws.render))
|
||||
alterRB(rb)
|
||||
rws.Route(rb)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *wsHost) render(req *restful.Request, resp *restful.Response) {
|
||||
hostname := ws.getHost(req)
|
||||
if hostname == "" {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
wsNotFound(req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
writeError(resp.ResponseWriter, err)
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
host := cfg.Host(hostname)
|
||||
if host == nil {
|
||||
log.Print("no host named ", hostname)
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
wsNotFound(req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
|
22
cmd/dkl-local-server/ws-hosts.go
Normal file
22
cmd/dkl-local-server/ws-hosts.go
Normal file
@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
func wsListHosts(req *restful.Request, resp *restful.Response) {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
resp.WriteErrorString(http.StatusServiceUnavailable, "failed to read configuration")
|
||||
return
|
||||
}
|
||||
|
||||
names := make([]string, len(cfg.Hosts))
|
||||
for i, host := range cfg.Hosts {
|
||||
names[i] = host.Name
|
||||
}
|
||||
|
||||
resp.WriteEntity(names)
|
||||
}
|
@ -1,35 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
"novit.nc/direktil/pkg/localconfig"
|
||||
)
|
||||
|
||||
func buildWS() *restful.WebService {
|
||||
ws := &restful.WebService{}
|
||||
|
||||
ws.Route(ws.POST("/configs").To(wsUploadConfig))
|
||||
// configs API
|
||||
ws.Route(ws.POST("/configs").Filter(adminAuth).To(wsUploadConfig).
|
||||
Doc("Upload a new current configuration, archiving the previous one"))
|
||||
|
||||
// clusters API
|
||||
ws.Route(ws.GET("/clusters").Filter(adminAuth).To(wsListClusters).
|
||||
Doc("List clusters"))
|
||||
ws.Route(ws.GET("/clusters/{cluster-name}").Filter(adminAuth).To(wsCluster).
|
||||
Doc("Get cluster details"))
|
||||
ws.Route(ws.GET("/clusters/{cluster-name}/addons").Filter(adminAuth).To(wsClusterAddons).
|
||||
Doc("Get cluster addons").
|
||||
Returns(http.StatusOK, "OK", nil).
|
||||
Returns(http.StatusNotFound, "The cluster does not exists or does not have addons defined", nil))
|
||||
|
||||
// hosts API
|
||||
ws.Route(ws.GET("/hosts").Filter(hostsAuth).To(wsListHosts).
|
||||
Doc("List hosts"))
|
||||
|
||||
(&wsHost{
|
||||
prefix: "",
|
||||
getHost: detectHost,
|
||||
}).register(ws)
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Notes("In this case, the host is detected from the remote IP")
|
||||
})
|
||||
|
||||
(&wsHost{
|
||||
prefix: "/hosts/{hostname}",
|
||||
prefix: "/hosts/{host-name}",
|
||||
getHost: func(req *restful.Request) string {
|
||||
return req.PathParameter("hostname")
|
||||
return req.PathParameter("host-name")
|
||||
},
|
||||
}).register(ws)
|
||||
}).register(ws, func(rb *restful.RouteBuilder) {
|
||||
rb.Filter(adminAuth)
|
||||
})
|
||||
|
||||
return ws
|
||||
}
|
||||
@ -65,78 +81,24 @@ func detectHost(req *restful.Request) string {
|
||||
return host.Name
|
||||
}
|
||||
|
||||
func wsUploadConfig(req *restful.Request, resp *restful.Response) {
|
||||
r := req.Request
|
||||
w := resp.ResponseWriter
|
||||
|
||||
if !authorizeAdmin(r) {
|
||||
forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := ioutil.TempFile(*dataDir, ".config-upload")
|
||||
func wsReadConfig(resp *restful.Response) *localconfig.Config {
|
||||
cfg, err := readConfig()
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
log.Print("failed to read config: ", err)
|
||||
resp.WriteErrorString(http.StatusServiceUnavailable, "failed to read config")
|
||||
return nil
|
||||
}
|
||||
|
||||
defer os.Remove(out.Name())
|
||||
|
||||
_, err = io.Copy(out, r.Body)
|
||||
out.Close()
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
archivesPath := filepath.Join(*dataDir, "archives")
|
||||
cfgPath := configFilePath()
|
||||
|
||||
err = os.MkdirAll(archivesPath, 0700)
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
backupPath := filepath.Join(archivesPath, "config."+ulid()+".yaml.gz")
|
||||
|
||||
bck, err := os.Create(backupPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer bck.Close()
|
||||
|
||||
in, err := os.Open(cfgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gz, err := gzip.NewWriterLevel(bck, 2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(gz, in)
|
||||
gz.Close()
|
||||
in.Close()
|
||||
return
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Rename(out.Name(), cfgPath)
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func wsNotFound(req *restful.Request, resp *restful.Response) {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func wsError(resp *restful.Response, err error) {
|
||||
log.Print("request failed: ", err)
|
||||
resp.WriteErrorString(
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
Reference in New Issue
Block a user