Skip to content

Commit

Permalink
feat(dashboard): Namespace handling (#338)
Browse files Browse the repository at this point in the history
Fix #334 & #324.

* Adds a selector to choose a namespace in which we should list the TFJobs.
* By default we look into every namespace.
screen shot 2018-01-19 at 4 10 56 pm

* Also handles namespaces correctly at creation.
  If a job is created in a new (non-existant) namespace, the namespace will be created before-hand.
  • Loading branch information
wbuchwalter authored and jlewi committed Jan 25, 2018
1 parent 2b88614 commit 8caa0c9
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 180 deletions.
53 changes: 49 additions & 4 deletions dashboard/backend/handler/api_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type TFJobList struct {
tfJobs []v1alpha1.TFJob `json:"TFJobs"`
}

// NamepsaceList is a list of namespaces
type NamespaceList struct {
namespaces []v1.Namespace `json:"namespaces"`
}

// CreateHTTPAPIHandler creates the restful Container and defines the routes the API will serve
func CreateHTTPAPIHandler(client client.ClientManager) (http.Handler, error) {
apiHandler := APIHandler{
Expand Down Expand Up @@ -65,6 +70,11 @@ func CreateHTTPAPIHandler(client client.ClientManager) (http.Handler, error) {
apiV1Ws.GET("/tfjob").
To(apiHandler.handleGetTFJobs).
Writes(TFJobList{}))

apiV1Ws.Route(
apiV1Ws.GET("/tfjob/{namespace}").
To(apiHandler.handleGetTFJobs).
Writes(TFJobList{}))

apiV1Ws.Route(
apiV1Ws.GET("/tfjob/{namespace}/{tfjob}").
Expand All @@ -86,19 +96,28 @@ func CreateHTTPAPIHandler(client client.ClientManager) (http.Handler, error) {
To(apiHandler.handleGetPodLogs).
Writes([]byte{}))

apiV1Ws.Route(
apiV1Ws.GET("/namespace").
To(apiHandler.handleGetNamespaces).
Writes(NamespaceList{}))

wsContainer.Add(apiV1Ws)
return wsContainer, nil
}

func (apiHandler *APIHandler) handleGetTFJobs(request *restful.Request, response *restful.Response) {
//TODO: namespace handling
namespace := "default"
namespace := request.PathParameter("namespace")
jobs, err := apiHandler.cManager.TFJobClient.TensorflowV1alpha1().TFJobs(namespace).List(metav1.ListOptions{})

ns := "all"
if namespace != "" {
ns = namespace
}
if err != nil {
log.Warningf("failed to list TFJobs under namespace %v: %v", namespace, err)
log.Warningf("failed to list TFJobs under %v namespace(s): %v", ns, err)
response.WriteError(http.StatusInternalServerError, err)
} else {
log.Infof("successfully listed TFJobs under namespace %v", namespace)
log.Infof("successfully listed TFJobs under %v namespace(s)", ns)
response.WriteHeaderAndEntity(http.StatusOK, jobs)
}
}
Expand Down Expand Up @@ -162,6 +181,21 @@ func (apiHandler *APIHandler) handleDeploy(request *restful.Request, response *r
response.WriteError(http.StatusBadRequest, err)
return
}

_, err := apiHandler.cManager.ClientSet.CoreV1().Namespaces().Get(tfJob.Namespace, metav1.GetOptions{})

if errors.IsNotFound(err) {
// If namespace doesn't exist we create it
_, nsErr := apiHandler.cManager.ClientSet.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: tfJob.Namespace}})
if nsErr != nil {
log.Warningf("failed to create namespace %v for TFJob %v: %v", tfJob.Namespace, tfJob.Name, nsErr)
response.WriteError(http.StatusInternalServerError, nsErr)
}
} else if err != nil {
log.Warningf("failed to deploy TFJob %v under namespace %v: %v", tfJob.Name, tfJob.Namespace, err)
response.WriteError(http.StatusInternalServerError, err)
}

j, err := clt.TensorflowV1alpha1().TFJobs(tfJob.Namespace).Create(tfJob)
if err != nil {
log.Warningf("failed to deploy TFJob %v under namespace %v: %v", tfJob.Name, tfJob.Namespace, err)
Expand Down Expand Up @@ -198,3 +232,14 @@ func (apiHandler *APIHandler) handleGetPodLogs(request *restful.Request, respons
response.WriteHeaderAndEntity(http.StatusOK, string(logs))
}
}

func (apiHandler *APIHandler) handleGetNamespaces(request *restful.Request, response *restful.Response) {
l, err := apiHandler.cManager.ClientSet.CoreV1().Namespaces().List(metav1.ListOptions{})
if err != nil {
log.Warningf("failed to list namespaces.")
response.WriteError(http.StatusInternalServerError, err)
} else {
log.Infof("sucessfully listed namespaces")
response.WriteHeaderAndEntity(http.StatusOK, l)
}
}
61 changes: 54 additions & 7 deletions dashboard/frontend/src/components/Home.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,87 @@
import React, { Component } from "react";
import { Route, Switch } from "react-router-dom";
import "./App.css";
import SelectField from "material-ui/SelectField";
import MenuItem from "material-ui/MenuItem";

import "./App.css";
import JobList from "./JobList";
import Job from "./Job";
import CreateJob from "./CreateJob";
import AppBar from "./AppBar";
import { getTFJobListService } from "../services";
import { getTFJobListService, getNamespaces } from "../services";

const allNamespacesKey = "All namespaces";

class Home extends Component {
constructor(props) {
super(props);
this.state = {
tfJobs: []
tfJobs: [],
selectedNamespace: allNamespacesKey,
namespaces: []
};

this.handleNamespaceChange = this.handleNamespaceChange.bind(this);
this.lastNamespaceQueried = allNamespacesKey;
}

componentDidMount() {
this.fetch();
setInterval(() => this.fetch(), 10000);
this.fetchJobs();
this.fetchNamespaces();
setInterval(() => this.fetchJobs(), 10000);
}

fetch() {
getTFJobListService()
fetchJobs() {
let ns =
this.state.selectedNamespace === allNamespacesKey
? ""
: this.state.selectedNamespace;
getTFJobListService(ns)
.then(b => {
this.lastNamespaceQueried = this.state.selectedNamespace;
this.setState({ tfJobs: b.items });
})
.catch(console.error);
}

fetchNamespaces() {
getNamespaces()
.then(b =>
this.setState({
namespaces: b.items
.map(ns => ns.metadata.name)
.concat(allNamespacesKey)
})
)
.catch(console.error);
}

handleNamespaceChange(event, index, value) {
this.setState({ selectedNamespace: value });
}

render() {
if (this.lastNamespaceQueried !== this.state.selectedNamespace) {
// if the user changed the selected namespace we want to refresh immediatly, not once the timer ticks.
this.fetchJobs();
}

const nsl = this.state.namespaces.map(ns => {
return <MenuItem value={ns} primaryText={ns} key={ns} />;
});

return (
<div>
<AppBar />
<div id="main" style={this.styles.mainStyle}>
<div style={this.styles.list}>
<SelectField
floatingLabelText="Namespace"
value={this.state.selectedNamespace}
onChange={this.handleNamespaceChange}
>
{nsl}
</SelectField>
<JobList jobs={this.state.tfJobs} />
</div>
<div style={this.styles.content}>
Expand Down
10 changes: 7 additions & 3 deletions dashboard/frontend/src/services.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// let host = "http://localhost:8080";
//let host = "http://localhost:8080";
let host = "";

export function getTFJobListService() {
return fetch(`${host}/api/tfjob`).then(r => r.json());
export function getTFJobListService(namespace) {
return fetch(`${host}/api/tfjob/${namespace}`).then(r => r.json());
}

export function createTFJobService(spec) {
Expand Down Expand Up @@ -37,3 +37,7 @@ export function deleteTFJob(namespace, name) {
export function getPodLogs(namespace, name) {
return fetch(`${host}/api/logs/${namespace}/${name}`).then(r => r.json());
}

export function getNamespaces() {
return fetch(`${host}/api/namespace`).then(r => r.json());
}
Loading

0 comments on commit 8caa0c9

Please sign in to comment.