Add initial web server implementation 48/162048/6
authorAlexander Mazuruk <a.mazuruk@samsung.com>
Mon, 30 Oct 2017 09:22:46 +0000 (10:22 +0100)
committerAlexander Mazuruk <a.mazuruk@samsung.com>
Fri, 8 Dec 2017 14:57:33 +0000 (15:57 +0100)
Weles must provide RESTful API over HTTP that allows Weles to:
- Receive yaml file with instructions to create a job
- Receive job cancellation request
- Send a list of current Jobs to client
- Send a list of artifacts to client

Change-Id: I1a8e606e763f6c712224f234d98d302450f0c6d0

server/api/v1/api.go [new file with mode: 0644]
server/api/v1/handlers.go [new file with mode: 0644]
server/mockup/mockup.go [new file with mode: 0644]
server/server.go [new file with mode: 0644]

diff --git a/server/api/v1/api.go b/server/api/v1/api.go
new file mode 100644 (file)
index 0000000..e1710bc
--- /dev/null
@@ -0,0 +1,72 @@
+/*  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// Package v1 provides HTTP API of Weles.
+//
+// Through this API, clients may:
+//
+// * Create a Weles Job
+//
+// * Cancel a Weles Job
+//
+// * List and filter Weles Jobs
+//
+// * List and filter Weles Artifacts
+package v1
+
+import (
+       "git.tizen.org/tools/weles"
+       "github.com/dimfeld/httptreemux"
+)
+
+// API provides HTTP API handlers.
+type API struct {
+       router *httptreemux.TreeMux
+       jm     weles.JobManager
+       am     weles.ArtifactManager
+       cfg    apiConfig
+}
+
+// apiConfig contains configuration for API Handlers.
+type apiConfig struct {
+       maxYamlSize int64
+}
+
+// initRouter initializes the routes of the Weles API.
+func (api *API) initRouter() {
+       //TODO(aalexnaderr) check groups and contexts in the httptreemux package
+
+       api.router.POST("/api/v1/jobs/", api.JobCreator)
+       api.router.POST("/api/v1/jobs/:jobID/cancel", api.JobCanceler)
+       api.router.GET("/api/v1/jobs/", api.JobFetcher)
+       api.router.POST("/api/v1/jobs/filter", api.JobFetcher)
+
+       api.router.GET("/api/v1/artifacts/", api.ArtifactsLister)
+       api.router.POST("/api/v1/artifacts/filter", api.ArtifactsLister)
+}
+
+// NewAPI defines possible routes.
+func NewAPI(router *httptreemux.TreeMux, jm weles.JobManager, am weles.ArtifactManager) (api *API) {
+       api = new(API)
+       api.router = router
+       api.jm = jm
+       api.am = am
+       // 10mb should be more than enough for yaml file
+       // this should be moved to the initApiConfig function when there will be more configs.
+       api.cfg.maxYamlSize = 10 << 20
+       api.initRouter()
+
+       return api
+}
diff --git a/server/api/v1/handlers.go b/server/api/v1/handlers.go
new file mode 100644 (file)
index 0000000..f3a3b7f
--- /dev/null
@@ -0,0 +1,172 @@
+/*  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package v1
+
+import (
+       "encoding/json"
+       "errors"
+       "io/ioutil"
+       "net/http"
+       "strconv"
+
+       "git.tizen.org/tools/weles"
+)
+
+type jobIDpack struct {
+       weles.JobID `json:"jobid"`
+}
+
+type jobIDfilter struct {
+       Jobids []weles.JobID `json:"jobid"`
+}
+
+// errHandler is error handler function that returns error in the HTTP Bad Request response when
+// error is not nil. After the errHandler is executed no further writes to the request should
+// be done.
+func errHandler(w http.ResponseWriter, err error) bool {
+       // TODO(aalexanderr) errors should be analysed to set proper http status e.g. when there is
+       // no file supplied by user, StatusNotFound should be returned.
+       if err != nil {
+               http.Error(w, err.Error(), http.StatusBadRequest)
+               return true
+       }
+       return false
+}
+
+// JobCreator takes yaml file from http POST request:
+// - bodycontenttype: multipart/form-data
+// - field name: "uploadfile"
+// and passes it as byte slice to CreateJob function.
+// CreateJob function returns JobId which is passed to the requestor in JSON response
+func (api *API) JobCreator(w http.ResponseWriter, r *http.Request, _ map[string]string) {
+       if errHandler(w, r.ParseMultipartForm(api.cfg.maxYamlSize)) {
+               return
+       }
+
+       file, _, err := r.FormFile("uploadfile")
+       if errHandler(w, err) {
+               return
+       }
+       //TODO file.Close error should be passed to logger which currently does not exist thus
+       // an assumption is made that correctly opened file returned by standard library will
+       // always close.
+       defer file.Close()
+
+       byteContainer, err := ioutil.ReadAll(file)
+       if errHandler(w, err) {
+               return
+       }
+
+       jobid, err := api.jm.CreateJob(byteContainer)
+       if errHandler(w, err) {
+               return
+       }
+
+       response, err := json.Marshal(&jobIDpack{jobid})
+       if errHandler(w, err) {
+               return
+       }
+       w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+       w.WriteHeader(http.StatusCreated)
+       w.Write(response)
+
+}
+
+// JobCanceler takes jobID and passes it to CancelJob function.
+func (api *API) JobCanceler(w http.ResponseWriter, r *http.Request, p map[string]string) {
+       jobidStr, ok := p["jobID"]
+       if !ok {
+               errHandler(w, errors.New("jobID parameter is not filled"))
+               return
+       }
+       jobidUint, err := strconv.ParseUint(jobidStr, 10, 64)
+       if errHandler(w, err) {
+               return
+       }
+       if errHandler(w, api.jm.CancelJob(weles.JobID(jobidUint))) {
+               return
+       }
+       w.WriteHeader(http.StatusOK)
+}
+
+// JobFetcher will return info on
+// - all jobs available when receiving GET request
+// - jobs with specified JobIDs when they are JSON encoded in the POST request body
+func (api *API) JobFetcher(w http.ResponseWriter, r *http.Request, _ map[string]string) {
+       var jobInfos []weles.JobInfo
+       switch r.Method {
+       case http.MethodGet:
+               var err error
+               jobInfos, err = api.jm.ListJobs(nil)
+               if errHandler(w, err) {
+                       return
+               }
+       case http.MethodPost:
+               body, err := ioutil.ReadAll(r.Body)
+               if errHandler(w, err) {
+                       return
+               }
+               var jobids jobIDfilter
+               err = json.Unmarshal(body, &jobids)
+               if errHandler(w, err) {
+                       return
+               }
+               jobInfos, err = api.jm.ListJobs(jobids.Jobids)
+               if errHandler(w, err) {
+                       return
+               }
+
+       }
+       data, err := json.Marshal(&jobInfos)
+       if errHandler(w, err) {
+               return
+       }
+       w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+       w.WriteHeader(http.StatusOK)
+       w.Write(data)
+}
+
+// ArtifactsLister will return info on:
+//  - all artifacts when receiving GET request
+//  - filtered artifacts according to specified filters in POST request
+func (api *API) ArtifactsLister(w http.ResponseWriter, r *http.Request, _ map[string]string) {
+       var filter weles.ArtifactFilter
+       var artifactInfos []weles.ArtifactInfo
+       switch r.Method {
+       case http.MethodGet:
+
+       case http.MethodPost:
+               body, err := ioutil.ReadAll(r.Body)
+               if errHandler(w, err) {
+                       return
+               }
+               err = json.Unmarshal(body, &filter)
+               if errHandler(w, err) {
+                       return
+               }
+       }
+       artifactInfos, err := api.am.ListArtifact(filter)
+       if errHandler(w, err) {
+               return
+       }
+       data, err := json.Marshal(artifactInfos)
+       if errHandler(w, err) {
+               return
+       }
+       w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+       w.WriteHeader(http.StatusOK)
+       w.Write(data)
+}
diff --git a/server/mockup/mockup.go b/server/mockup/mockup.go
new file mode 100644 (file)
index 0000000..4c260a7
--- /dev/null
@@ -0,0 +1,83 @@
+// Package mockup is used for the purpose of development of api
+// before the interfaces are implemented in the appropriate packages.
+// After the interfaces will be implemented, rebase will be performed
+// and this mockup package will be deleted.
+package mockup
+
+import (
+       "fmt"
+
+       "git.tizen.org/tools/weles"
+)
+
+// JobManagerMock is a mockup  struct that will implement JobManager interface.
+type JobManagerMock struct {
+}
+
+// NewJobManagerMock is function that creates JobManagerMock object.
+func NewJobManagerMock() weles.JobManager {
+       return &JobManagerMock{}
+}
+
+// CreateJob is a mockup function implementing JobManager interface.
+func (mock *JobManagerMock) CreateJob(yaml []byte) (weles.JobID, error) {
+       return 123, nil
+}
+
+// CancelJob is a mockup function implementing JobManager interface.
+func (mock *JobManagerMock) CancelJob(j weles.JobID) (err error) {
+       if j == 1234 {
+               err = nil
+       } else {
+               err = fmt.Errorf("Wrong ID")
+       }
+
+       return err
+}
+
+// ListJobs is a mockup function implementing JobManager interface.
+func (mock *JobManagerMock) ListJobs(jid []weles.JobID) ([]weles.JobInfo, error) {
+       if jid == nil {
+               return []weles.JobInfo{
+                       {JobID: 123, Name: "JobNamee", Info: "asddd"},
+                       {JobID: 1234, Name: "JobNamee", Info: "asddd"},
+               }, nil
+       }
+
+       return []weles.JobInfo{
+               {JobID: 1, Name: "JobNamee", Info: "asddd"},
+               {JobID: 123, Name: "JobNam3ee", Info: "ssasddd"},
+               {JobID: 1245, Name: "JobName5e", Info: "asddd"},
+       }, nil
+}
+
+// ArtifactManagerMock is a mockup struct that will implement ArtifactManagerMock interface.
+type ArtifactManagerMock struct {
+}
+
+// NewArtifactManagerMock is a function that creates ArtifactManagerMock struct.
+func NewArtifactManagerMock() weles.ArtifactManager {
+       return &ArtifactManagerMock{}
+}
+
+// ListArtifact is a mockup function implementing ArtifactManager interface.
+func (mock *ArtifactManagerMock) ListArtifact(filter weles.ArtifactFilter) ([]weles.ArtifactInfo, error) {
+       return nil, nil
+}
+
+// PushArtifact is a mockup function implementing ArtifactManager interface.
+func (mock *ArtifactManagerMock) PushArtifact(artifact weles.ArtifactDescription, ch chan weles.ArtifactStatusChange) (weles.ArtifactPath, error) {
+       return "", nil
+}
+
+// CreateArtifact is a mockup function implementing ArtifactManager interface.
+func (mock *ArtifactManagerMock) CreateArtifact(artifact weles.ArtifactDescription) (weles.ArtifactPath, error) {
+       return "", nil
+}
+
+// GetArtifactInfo is a mockup function implementing ArtifactManager interface.
+func (mock *ArtifactManagerMock) GetArtifactInfo(path weles.ArtifactPath) (weles.ArtifactInfo, error) {
+       ai := new(weles.ArtifactInfo)
+
+       return *ai, nil
+}
diff --git a/server/server.go b/server/server.go
new file mode 100644 (file)
index 0000000..66ac647
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ *  Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package main
+
+import (
+       "log"
+       "net/http"
+
+       "git.tizen.org/tools/weles/server/api/v1"
+       "git.tizen.org/tools/weles/server/mockup"
+       "github.com/dimfeld/httptreemux"
+)
+
+func main() {
+       router := httptreemux.New()
+       jm := mockup.NewJobManagerMock()
+       am := mockup.NewArtifactManagerMock()
+       //TODO pass pointer not copy
+       _ = v1.NewAPI(router, jm, am)
+
+       log.Fatal(http.ListenAndServe(":8080", router))
+}