forked from weaveworks/footloose
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
api: Support creation/deletion of clusters and machines
- Loading branch information
Showing
11 changed files
with
481 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.