-
Notifications
You must be signed in to change notification settings - Fork 710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dashboard V1 #125
Dashboard V1 #125
Changes from 20 commits
dc9116c
f3fa5f7
8d9b88e
8b2190d
37c7655
f9e5ef5
be85cb3
95a36ea
37cf983
f5a1ab9
1933e2f
7056b3f
3a0b35d
a3d3efb
6f3f2e9
f89bbde
8d09f08
2c547fd
7ac5407
7661d2f
79bf0ef
fbc2bf3
18aee39
48eaa0e
eeaa478
bd1cb24
5fd7d9a
e8aca7e
7ef434b
5fb6564
fc63c29
38c6de1
c2f8b39
62965c7
89aec3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,8 @@ | |
# only so we exclude them. | ||
bin/ | ||
vendor/ | ||
|
||
node_modules/ | ||
build/ | ||
.vscode/ | ||
|
||
# Compiled python files. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package client | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a package comment somewhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved |
||
|
||
import ( | ||
"github.com/tensorflow/k8s/pkg/util/k8sutil" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/rest" | ||
) | ||
|
||
const ( | ||
CRDGroup = "mlkube.io" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't right. We renamed it to tensorflow.org and v1alpha1. Is there a reason you are redefining these constants here rather than using the existing constants defined in pkg/spec? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, these constants aren't use anymore, forgot to delete them earlier. |
||
CRDVersion = "v1beta1" | ||
) | ||
|
||
type ClientManager struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Public variables, functions should have a doc string. |
||
restCfg *rest.Config | ||
ClientSet *kubernetes.Clientset | ||
TfJobClient *k8sutil.TfJobRestClient | ||
} | ||
|
||
func (c *ClientManager) init() { | ||
restCfg, err := k8sutil.GetClusterConfig() | ||
if err != nil { | ||
panic(err) | ||
} | ||
c.restCfg = restCfg | ||
|
||
clientset, err := kubernetes.NewForConfig(c.restCfg) | ||
if err != nil { | ||
panic(err.Error()) | ||
} | ||
c.ClientSet = clientset | ||
|
||
tfJobClient, err := k8sutil.NewTfJobClient() | ||
if err != nil { | ||
panic(err) | ||
} | ||
c.TfJobClient = tfJobClient | ||
} | ||
|
||
func NewClientManager() (ClientManager, error) { | ||
cm := ClientManager{} | ||
cm.init() | ||
|
||
return cm, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
package handler | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Package comment? |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit I think we've been using underscores in file names; so api_handler.go. |
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
restful "github.com/emicklei/go-restful" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason you picked this package? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I started the UI, my initial thought was that in the end it might end up integrated with the kubernetes dashboard once they have a better plan to support third parties integration, so the backend architecture is pretty close to https://github.com/kubernetes/dashboard/tree/master/src/app/backend. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM thanks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved. |
||
"github.com/tensorflow/k8s/dashboard/backend/client" | ||
"github.com/tensorflow/k8s/pkg/spec" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/pkg/api/v1" | ||
) | ||
|
||
type APIHandler struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs a doc string. |
||
cManager client.ClientManager | ||
} | ||
|
||
//add tensorboard ips, add azure file / gs links | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. space after "//" Needs a doc string; see https://golang.org/doc/effective_go.html#commentary The first sentence should begin with the name of the thing being described; e.g. "TfJobDetail is ...." |
||
type TfJobDetail struct { | ||
TfJob *spec.TfJob `json:"tfJob"` | ||
TbService *v1.Service `json:"tbService"` | ||
Pods []v1.Pod `json:"pods"` | ||
} | ||
|
||
type TfJobList struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment please. |
||
tfJobs []spec.TfJob `json:"TfJobs"` | ||
} | ||
|
||
func CreateHTTPAPIHandler(client client.ClientManager) (http.Handler, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment please. |
||
apiHandler := APIHandler{ | ||
cManager: client, | ||
} | ||
|
||
wsContainer := restful.NewContainer() | ||
wsContainer.EnableContentEncoding(true) | ||
|
||
cors := restful.CrossOriginResourceSharing{ | ||
ExposeHeaders: []string{"X-My-Header"}, | ||
AllowedHeaders: []string{"Content-Type", "Accept"}, | ||
AllowedMethods: []string{"GET", "POST", "DELETE"}, | ||
CookiesAllowed: false, | ||
Container: wsContainer, | ||
} | ||
wsContainer.Filter(cors.Filter) | ||
wsContainer.Filter(wsContainer.OPTIONSFilter) | ||
|
||
apiV1Ws := new(restful.WebService) | ||
|
||
apiV1Ws.Path("/api"). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do these API methods differ from the API methods that the K8s APIServer provides for our TfJob CRD? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of them (such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any particular reason why you need to define methods that just forward to the APIServer rather than having callers of those methods just call the APIServer directly? I think this has the potential of creating confusion down the line. e.g if someone is writing a program to create TfJobs programmatically, should they be using the APIServer or the backend server? Should the extra data (e.g. service details for TensorBoard) be included in the TfJob status? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API is called by the dashboard's frontend only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The APIServer speaks oauth. So couldn't the JS app running on the client just obtain an OAuth token for the user and then include that in the https request to the APIServer? That's probably beyond the scope of this PR. However, if that seems like a promising approach then it might be worth opening an issue or TODO to track. |
||
Consumes(restful.MIME_JSON). | ||
Produces(restful.MIME_JSON) | ||
|
||
apiV1Ws.Route( | ||
apiV1Ws.GET("/tfjob"). | ||
To(apiHandler.handleGetTfJobs). | ||
Writes(TfJobList{})) | ||
|
||
apiV1Ws.Route( | ||
apiV1Ws.GET("/tfjob/{namespace}/{tfjob}"). | ||
To(apiHandler.handleGetTfJobDetail). | ||
Writes(TfJobDetail{})) | ||
|
||
apiV1Ws.Route( | ||
apiV1Ws.POST("/tfjob"). | ||
To(apiHandler.handleDeploy). | ||
Reads(spec.TfJob{}). | ||
Writes(spec.TfJob{})) | ||
|
||
apiV1Ws.Route( | ||
apiV1Ws.DELETE("/tfjob/{namespace}/{tfjob}"). | ||
To(apiHandler.handleDeleteTfJob)) | ||
|
||
apiV1Ws.Route( | ||
apiV1Ws.GET("/logs/{namespace}/{podname}"). | ||
To(apiHandler.handleGetPodLogs). | ||
Writes([]byte{})) | ||
|
||
wsContainer.Add(apiV1Ws) | ||
return wsContainer, nil | ||
} | ||
|
||
func (apiHandler *APIHandler) handleGetTfJobs(request *restful.Request, response *restful.Response) { | ||
|
||
//TODO: namespace handling | ||
jobs, err := apiHandler.cManager.TfJobClient.List("default") | ||
|
||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
response.WriteHeaderAndEntity(http.StatusOK, jobs) | ||
} | ||
|
||
func (apiHandler *APIHandler) handleGetTfJobDetail(request *restful.Request, response *restful.Response) { | ||
namespace := request.PathParameter("namespace") | ||
name := request.PathParameter("tfjob") | ||
|
||
job, err := apiHandler.cManager.TfJobClient.Get(namespace, name) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
tfJobDetail := TfJobDetail{ | ||
TfJob: job, | ||
} | ||
|
||
if job.Spec.TensorBoard != nil { | ||
tbSpec, err := apiHandler.cManager.ClientSet.CoreV1().Services(namespace).List(metav1.ListOptions{ | ||
LabelSelector: fmt.Sprintf("tensorflow.org=,app=tensorboard,runtime_id=%s", job.Spec.RuntimeId), | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
if len(tbSpec.Items) > 0 { | ||
// Should never be more than 1 service that matched, handle error | ||
// Handle case where no tensorboard is found | ||
tfJobDetail.TbService = &tbSpec.Items[0] | ||
} else { | ||
fmt.Println(fmt.Sprintf("Couldn't find a TensorBoard service for TfJob %s", job.Metadata.Name)) | ||
} | ||
} | ||
|
||
// Get associated pods | ||
pods, err := apiHandler.cManager.ClientSet.CoreV1().Pods(namespace).List(metav1.ListOptions{ | ||
LabelSelector: fmt.Sprintf("tensorflow.org=,runtime_id=%s", job.Spec.RuntimeId), | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
tfJobDetail.Pods = pods.Items | ||
|
||
response.WriteHeaderAndEntity(http.StatusOK, tfJobDetail) | ||
} | ||
|
||
func (apiHandler *APIHandler) handleDeploy(request *restful.Request, response *restful.Response) { | ||
client := apiHandler.cManager.TfJobClient | ||
spec := new(spec.TfJob) | ||
if err := request.ReadEntity(spec); err != nil { | ||
panic(err) | ||
} | ||
j, err := client.Create(spec.Metadata.Namespace, spec) | ||
if err != nil { | ||
panic(err) | ||
} | ||
response.WriteHeaderAndEntity(http.StatusCreated, j) | ||
} | ||
|
||
func (apiHandler *APIHandler) handleDeleteTfJob(request *restful.Request, response *restful.Response) { | ||
namespace := request.PathParameter("namespace") | ||
name := request.PathParameter("tfjob") | ||
client := apiHandler.cManager.TfJobClient | ||
err := client.Delete(namespace, name) | ||
if err != nil { | ||
panic(err) | ||
} | ||
response.WriteHeader(http.StatusOK) | ||
} | ||
|
||
func (apiHandler *APIHandler) handleGetPodLogs(request *restful.Request, response *restful.Response) { | ||
namespace := request.PathParameter("namespace") | ||
name := request.PathParameter("podname") | ||
|
||
logs, err := apiHandler.cManager.ClientSet.CoreV1().Pods(namespace).GetLogs(name, &v1.PodLogOptions{}).Do().Raw() | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
response.WriteHeaderAndEntity(http.StatusOK, string(logs)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
|
||
"github.com/tensorflow/k8s/dashboard/backend/client" | ||
"github.com/tensorflow/k8s/dashboard/backend/handler" | ||
) | ||
|
||
func main() { | ||
log.SetOutput(os.Stdout) | ||
cm, err := client.NewClientManager() | ||
if err != nil { | ||
log.Fatalf("Error while initializing connection to Kubernetes apiserver: %v", err) | ||
} | ||
apiHandler, err := handler.CreateHTTPAPIHandler(cm) | ||
if err != nil { | ||
log.Fatalf("Error while creating the API Handler: %v", err) | ||
} | ||
|
||
http.Handle("/api/", apiHandler) | ||
http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("/opt/mlkube/dashboard/frontend/build/")))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use /opt/mlkube since we are trying to remove references to mlkube. |
||
|
||
p := ":8080" | ||
fmt.Println("Listening on", p) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why fmt and not log? |
||
|
||
http.ListenAndServe(p, nil) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "tensorflowk8s-dashboard", | ||
"version": "0.1.0", | ||
"description": "Dashboard for tensorflow/k8s.", | ||
"private": true, | ||
"dependencies": { | ||
"lodash": "^4.17.4", | ||
"material-ui": "^0.19.4", | ||
"react": "^16.0.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add prop-types if you are feeling good :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and license? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. personal suggestion: |
||
"react-dom": "^16.0.0", | ||
"react-router-dom": "^4.2.2", | ||
"react-scripts": "^1.0.17" | ||
}, | ||
"scripts": { | ||
"start": "react-scripts start", | ||
"build": "react-scripts build", | ||
"test": "react-scripts test --env=jsdom", | ||
"eject": "react-scripts eject", | ||
"toolbox": "react-toolbox-themr" | ||
}, | ||
"license": "Apache-2.0" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
<meta name="theme-color" content="#000000"> | ||
<!-- | ||
manifest.json provides metadata used when your web app is added to the | ||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ | ||
--> | ||
<link rel="shortcut icon" href="favicon.ico"> | ||
<!-- | ||
Notice the use of %PUBLIC_URL% in the tags above. | ||
It will be replaced with the URL of the `public` folder during the build. | ||
Only files inside the `public` folder can be referenced from the HTML. | ||
|
||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | ||
work correctly both with client-side routing and a non-root public URL. | ||
Learn how to configure a non-root public URL by running `npm run build`. | ||
--> | ||
<title>TensorFlow/k8s</title> | ||
</head> | ||
<body> | ||
<noscript> | ||
You need to enable JavaScript to run this app. | ||
</noscript> | ||
<div id="root"></div> | ||
<!-- | ||
This HTML file is a template. | ||
If you open it directly in the browser, you will see an empty page. | ||
|
||
You can add webfonts, meta tags, or analytics to this file. | ||
The build step will place the bundled scripts into the <body> tag. | ||
|
||
To begin the development, run `npm start` or `yarn start`. | ||
To create a production bundle, use `npm run build` or `yarn build`. | ||
--> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
html { | ||
font-family: roboto, sans-serif; | ||
} | ||
|
||
body { | ||
font-family: 'Roboto', sans-serif; | ||
background-color: #eee !important; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remove this? Now that we check in vendor we don't want in gitignoe.