Skip to content

Commit

Permalink
api: Support creation/deletion of clusters and machines
Browse files Browse the repository at this point in the history
  • Loading branch information
dlespiau committed Sep 3, 2019
1 parent 10bd106 commit 9cff4ac
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 10 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/docker/go-units v0.3.3 // indirect
github.com/ghodss/yaml v1.0.0
github.com/google/go-github/v24 v24.0.1
github.com/gorilla/mux v1.7.3
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.8.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju5
github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
Expand Down
16 changes: 16 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package api

// API represents the footloose REST API.
type API struct {
BaseURI string
db db
}

// New creates a new object able to answer footloose REST API.
func New(baseURI string) *API {
api := &API{
BaseURI: baseURI,
}
api.db.init()
return api
}
85 changes: 85 additions & 0 deletions pkg/api/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package api

import (
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/weaveworks/footloose/pkg/cluster"
"github.com/weaveworks/footloose/pkg/config"
)

// ClusterURI returns the URI identifying a cluster in the REST API.
func (a *API) ClusterURI(c *cluster.Cluster) string {
return fmt.Sprintf("%s/api/clusters/%s", a.BaseURI, c.Name())
}

// CreateCluster creates a cluster.
func (a *API) CreateCluster(w http.ResponseWriter, r *http.Request) {
var def config.Cluster
if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
sendError(w, http.StatusBadRequest, errors.Wrap(err, "could not decode body"))
return
}
if def.Name == "" {
sendError(w, http.StatusBadRequest, errors.New("no cluster name provided"))
return
}

cluster, err := cluster.New(config.Config{Cluster: def})
if err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

if err := a.db.addCluster(def.Name, cluster); err != nil {
sendError(w, http.StatusBadRequest, err)
return
}

if err := cluster.Create(); err != nil {
a.db.removeCluster(def.Name)
sendError(w, http.StatusInternalServerError, err)
return
}
sendCreated(w, a.ClusterURI((cluster)))
}

// DeleteCluster deletes a cluster.
func (a *API) DeleteCluster(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
c, err := a.db.cluster(vars["cluster"])
if err != nil {
sendError(w, http.StatusBadRequest, err)
return
}

// Starts by deleting the machines associated with the cluster.
machines, err := a.db.machines(vars["cluster"])
for _, m := range machines {
if err := c.DeleteMachine(m, 0); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}
if _, err := a.db.removeMachine(vars["cluster"], m.Hostname()); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}
}

// Delete cluster.
if err := c.Delete(); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

_, err = a.db.removeCluster(vars["cluster"])
if err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

sendOK(w)
}
144 changes: 144 additions & 0 deletions pkg/api/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package api

import (
"sync"

"github.com/pkg/errors"
"github.com/weaveworks/footloose/pkg/cluster"
)

type entry struct {
cluster *cluster.Cluster
machines map[string]*cluster.Machine
}

type db struct {
sync.Mutex

clusters map[string]entry
}

func (db *db) init() {
db.clusters = make(map[string]entry)
}

func (db *db) entry(name string) *entry {
db.Lock()
defer db.Unlock()

entry, ok := db.clusters[name]
if !ok {
return nil
}
return &entry
}

func (db *db) cluster(name string) (*cluster.Cluster, error) {
entry := db.entry(name)
if entry == nil {
return nil, errors.Errorf("unknown cluster '%s'", name)
}
return entry.cluster, nil
}

func (db *db) addCluster(name string, c *cluster.Cluster) error {
db.Lock()
defer db.Unlock()

if _, ok := db.clusters[name]; ok {
return errors.Errorf("cluster '%s' has already been added", name)
}
db.clusters[name] = entry{
cluster: c,
machines: make(map[string]*cluster.Machine),
}
return nil
}

func (db *db) removeCluster(name string) (*cluster.Cluster, error) {
db.Lock()
defer db.Unlock()

var entry entry
var ok bool
if entry, ok = db.clusters[name]; !ok {
return nil, errors.Errorf("unknown cluster '%s'", name)
}
// It is an error to remove the cluster from the db before removing all of its
// machines.
if len(entry.machines) != 0 {
return nil, errors.Errorf("cluster has machines associated with it")
}
delete(db.clusters, name)
return entry.cluster, nil
}

func (db *db) machine(clusterName, machineName string) (*cluster.Machine, error) {
entry := db.entry(clusterName)
if entry == nil {
return nil, errors.Errorf("unknown cluster '%s'", clusterName)
}

db.Lock()
defer db.Unlock()

var m *cluster.Machine
var ok bool
if m, ok = entry.machines[machineName]; !ok {
return nil, errors.Errorf("unknown machine '%s' for cluster '%s'", machineName, clusterName)
}
return m, nil
}

func (db *db) machines(clusterName string) ([]*cluster.Machine, error) {
entry := db.entry(clusterName)
if entry == nil {
return nil, errors.Errorf("unknown cluster '%s'", clusterName)
}

db.Lock()
defer db.Unlock()

var machines []*cluster.Machine
for _, m := range entry.machines {
machines = append(machines, m)
}
return machines, nil
}

func (db *db) addMachine(cluster string, m *cluster.Machine) error {
entry := db.entry(cluster)
if entry == nil {
return errors.Errorf("unknown cluster '%s'", cluster)
}

db.Lock()
defer db.Unlock()

// Hostname is really the machine unique name as we don't allow setting a
// different hostname.
if _, ok := entry.machines[m.Hostname()]; ok {
return errors.Errorf("machine '%s' has already been added", m.Hostname())

}
entry.machines[m.Hostname()] = m
return nil
}

func (db *db) removeMachine(clusterName, machineName string) (*cluster.Machine, error) {
entry := db.entry(clusterName)
if entry == nil {
return nil, errors.Errorf("unknown cluster '%s'", clusterName)
}

db.Lock()
defer db.Unlock()

var m *cluster.Machine
var ok bool
if m, ok = entry.machines[machineName]; !ok {
return nil, errors.Errorf("unknown machine '%s' for cluster '%s'", machineName, clusterName)
}
delete(entry.machines, machineName)
return m, nil
}
79 changes: 79 additions & 0 deletions pkg/api/machine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package api

import (
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/weaveworks/footloose/pkg/cluster"
"github.com/weaveworks/footloose/pkg/config"
)

// MachineURI returns the URI identifying a machine in the REST API.
func (a *API) MachineURI(c *cluster.Cluster, m *cluster.Machine) string {
return fmt.Sprintf("%s/api/clusters/%s/machines/%s", a.BaseURI, c.Name(), m.Hostname())
}

// CreateMachine creates a machine.
func (a *API) CreateMachine(w http.ResponseWriter, r *http.Request) {
var def config.Machine
if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
sendError(w, http.StatusBadRequest, errors.Wrap(err, "could not decode body"))
return
}
if def.Name == "" {
sendError(w, http.StatusBadRequest, errors.New("no machine name provided"))
return
}

vars := mux.Vars(r)
c, err := a.db.cluster(vars["cluster"])
if err != nil {
sendError(w, http.StatusBadRequest, err)
return
}

m := c.NewMachine(&def)

if err := c.CreateMachine(m, 0); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

if err := a.db.addMachine(vars["cluster"], m); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

sendCreated(w, a.MachineURI(c, m))
}

// DeleteMachine deletes a machine.
func (a *API) DeleteMachine(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
c, err := a.db.cluster(vars["cluster"])
if err != nil {
sendError(w, http.StatusBadRequest, err)
return
}
m, err := a.db.machine(vars["cluster"], vars["machine"])
if err != nil {
sendError(w, http.StatusBadRequest, err)
return
}

if err := c.DeleteMachine(m, 0); err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

_, err = a.db.removeMachine(vars["cluster"], vars["machine"])
if err != nil {
sendError(w, http.StatusInternalServerError, err)
return
}

sendOK(w)
}
39 changes: 39 additions & 0 deletions pkg/api/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package api

import (
"encoding/json"
"net/http"
)

func sendOK(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
}

// ErrorResponse is the response API entry points return when they encountered an error.
type ErrorResponse struct {
Error string `json:"error"`
}

func sendError(w http.ResponseWriter, status int, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := ErrorResponse{
Error: err.Error(),
}
json.NewEncoder(w).Encode(&resp)
}

// CreatedResponse is the response POST entry points return when a resource has been
// successfully created.
type CreatedResponse struct {
URI string `json:"uri"`
}

func sendCreated(w http.ResponseWriter, URI string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
resp := CreatedResponse{
URI: URI,
}
json.NewEncoder(w).Encode(&resp)
}
Loading

0 comments on commit 9cff4ac

Please sign in to comment.