diff --git a/api/swagger.json b/api/swagger.json index 51b22c8..683b520 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -15,9 +15,8 @@ "version": "1.0.0" }, "host": "localhost:8080", - "basePath": "/api", "paths": { - "/clusters/": { + "/api/clusters/": { "get": { "security": [ { @@ -74,7 +73,7 @@ } } }, - "/jobs/": { + "/api/jobs/": { "get": { "security": [ { @@ -169,7 +168,7 @@ } } }, - "/jobs/delete_job/": { + "/api/jobs/delete_job/": { "delete": { "security": [ { @@ -244,7 +243,7 @@ } } }, - "/jobs/delete_job/{id}": { + "/api/jobs/delete_job/{id}": { "delete": { "security": [ { @@ -314,7 +313,7 @@ } } }, - "/jobs/delete_job_before/{ts}": { + "/api/jobs/delete_job_before/{ts}": { "delete": { "security": [ { @@ -384,7 +383,7 @@ } } }, - "/jobs/edit_meta/{id}": { + "/api/jobs/edit_meta/{id}": { "post": { "security": [ { @@ -454,7 +453,7 @@ } } }, - "/jobs/start_job/": { + "/api/jobs/start_job/": { "post": { "security": [ { @@ -523,7 +522,7 @@ } } }, - "/jobs/stop_job/": { + "/api/jobs/stop_job/": { "post": { "security": [ { @@ -595,7 +594,7 @@ } } }, - "/jobs/tag_job/{id}": { + "/api/jobs/tag_job/{id}": { "post": { "security": [ { @@ -668,7 +667,7 @@ } } }, - "/jobs/{id}": { + "/api/jobs/{id}": { "get": { "security": [ { @@ -827,7 +826,7 @@ } } }, - "/notice/": { + "/config/notice/": { "post": { "security": [ { @@ -893,7 +892,7 @@ } } }, - "/user/{id}": { + "/config/user/{id}": { "post": { "security": [ { @@ -998,7 +997,7 @@ } } }, - "/users/": { + "/config/users/": { "get": { "security": [ { diff --git a/api/swagger.yaml b/api/swagger.yaml index f5f0081..35ec6c4 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,4 +1,3 @@ -basePath: /api definitions: api.ApiReturnedUser: properties: @@ -671,7 +670,7 @@ info: title: ClusterCockpit REST API version: 1.0.0 paths: - /clusters/: + /api/clusters/: get: description: Get a list of all cluster configs. Specific cluster can be requested using query parameter. @@ -708,7 +707,7 @@ paths: summary: Lists all cluster configs tags: - Cluster query - /jobs/: + /api/jobs/: get: description: |- Get a list of all jobs. Filters can be applied using query parameters. @@ -773,7 +772,7 @@ paths: summary: Lists all jobs tags: - Job query - /jobs/{id}: + /api/jobs/{id}: get: description: |- Job to get is specified by database ID @@ -882,7 +881,7 @@ paths: summary: Get job meta and configurable metric data tags: - Job query - /jobs/delete_job/: + /api/jobs/delete_job/: delete: consumes: - application/json @@ -932,7 +931,7 @@ paths: summary: Remove a job from the sql database tags: - Job remove - /jobs/delete_job/{id}: + /api/jobs/delete_job/{id}: delete: description: Job to remove is specified by database ID. This will not remove the job from the job archive. @@ -979,7 +978,7 @@ paths: summary: Remove a job from the sql database tags: - Job remove - /jobs/delete_job_before/{ts}: + /api/jobs/delete_job_before/{ts}: delete: description: Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive. @@ -1026,7 +1025,7 @@ paths: summary: Remove a job from the sql database tags: - Job remove - /jobs/edit_meta/{id}: + /api/jobs/edit_meta/{id}: post: consumes: - application/json @@ -1073,7 +1072,7 @@ paths: summary: Edit meta-data json tags: - Job add and modify - /jobs/start_job/: + /api/jobs/start_job/: post: consumes: - application/json @@ -1120,7 +1119,7 @@ paths: summary: Adds a new job as "running" tags: - Job add and modify - /jobs/stop_job/: + /api/jobs/stop_job/: post: description: |- Job to stop is specified by request body. All fields are required in this case. @@ -1168,7 +1167,7 @@ paths: summary: Marks job as completed and triggers archiving tags: - Job add and modify - /jobs/tag_job/{id}: + /api/jobs/tag_job/{id}: post: consumes: - application/json @@ -1218,7 +1217,7 @@ paths: summary: Adds one or more tags to a job tags: - Job add and modify - /notice/: + /config/notice/: post: consumes: - multipart/form-data @@ -1263,7 +1262,7 @@ paths: summary: Updates or empties the notice box content tags: - User - /user/{id}: + /config/user/{id}: post: consumes: - multipart/form-data @@ -1337,7 +1336,7 @@ paths: summary: Updates an existing user tags: - User - /users/: + /config/users/: delete: consumes: - multipart/form-data diff --git a/cmd/cc-backend/server.go b/cmd/cc-backend/server.go index 1408162..3c19730 100644 --- a/cmd/cc-backend/server.go +++ b/cmd/cc-backend/server.go @@ -18,6 +18,7 @@ import ( "time" "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/ClusterCockpit/cc-backend/internal/api" "github.com/ClusterCockpit/cc-backend/internal/archiver" @@ -31,6 +32,7 @@ import ( "github.com/ClusterCockpit/cc-backend/web" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/gorilla/websocket" httpSwagger "github.com/swaggo/http-swagger" ) @@ -53,13 +55,24 @@ func serverInit() { // Setup the http.Handler/Router used by the server graph.Init() resolver := graph.GetResolverInstance() - graphQLEndpoint := handler.NewDefaultServer( + graphQLServer := handler.New( generated.NewExecutableSchema(generated.Config{Resolvers: resolver})) + graphQLServer.AddTransport(transport.SSE{}) + graphQLServer.AddTransport(transport.POST{}) + graphQLServer.AddTransport(transport.Websocket{ + KeepAlivePingInterval: 10 * time.Second, + Upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + }, + }) + if os.Getenv("DEBUG") != "1" { // Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed. // The problem with this is that then, no more stacktrace is printed to stderr. - graphQLEndpoint.SetRecoverFunc(func(ctx context.Context, err interface{}) error { + graphQLServer.SetRecoverFunc(func(ctx context.Context, err any) error { switch e := err.(type) { case string: return fmt.Errorf("MAIN > Panic: %s", e) @@ -78,7 +91,7 @@ func serverInit() { router = mux.NewRouter() buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date} - info := map[string]interface{}{} + info := map[string]any{} info["hasOpenIDConnect"] = false if config.Keys.OpenIDConfig != nil { @@ -208,7 +221,7 @@ func serverInit() { router.PathPrefix("/swagger/").Handler(httpSwagger.Handler( httpSwagger.URL("http://" + config.Keys.Addr + "/swagger/doc.json"))).Methods(http.MethodGet) } - secured.Handle("/query", graphQLEndpoint) + secured.Handle("/query", graphQLServer) // Send a searchId and then reply with a redirect to a user, or directly send query to job table for jobid and project. secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) { diff --git a/internal/api/docs.go b/internal/api/docs.go index 642003f..2408f85 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -23,7 +23,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/clusters/": { + "/api/clusters/": { "get": { "security": [ { @@ -80,7 +80,7 @@ const docTemplate = `{ } } }, - "/jobs/": { + "/api/jobs/": { "get": { "security": [ { @@ -175,7 +175,7 @@ const docTemplate = `{ } } }, - "/jobs/delete_job/": { + "/api/jobs/delete_job/": { "delete": { "security": [ { @@ -250,7 +250,7 @@ const docTemplate = `{ } } }, - "/jobs/delete_job/{id}": { + "/api/jobs/delete_job/{id}": { "delete": { "security": [ { @@ -320,7 +320,7 @@ const docTemplate = `{ } } }, - "/jobs/delete_job_before/{ts}": { + "/api/jobs/delete_job_before/{ts}": { "delete": { "security": [ { @@ -390,7 +390,7 @@ const docTemplate = `{ } } }, - "/jobs/edit_meta/{id}": { + "/api/jobs/edit_meta/{id}": { "post": { "security": [ { @@ -460,7 +460,7 @@ const docTemplate = `{ } } }, - "/jobs/start_job/": { + "/api/jobs/start_job/": { "post": { "security": [ { @@ -529,7 +529,7 @@ const docTemplate = `{ } } }, - "/jobs/stop_job/": { + "/api/jobs/stop_job/": { "post": { "security": [ { @@ -601,7 +601,7 @@ const docTemplate = `{ } } }, - "/jobs/tag_job/{id}": { + "/api/jobs/tag_job/{id}": { "post": { "security": [ { @@ -674,7 +674,7 @@ const docTemplate = `{ } } }, - "/jobs/{id}": { + "/api/jobs/{id}": { "get": { "security": [ { @@ -833,7 +833,7 @@ const docTemplate = `{ } } }, - "/notice/": { + "/config/notice/": { "post": { "security": [ { @@ -899,7 +899,7 @@ const docTemplate = `{ } } }, - "/user/{id}": { + "/config/user/{id}": { "post": { "security": [ { @@ -1004,7 +1004,7 @@ const docTemplate = `{ } } }, - "/users/": { + "/config/users/": { "get": { "security": [ { @@ -2191,7 +2191,7 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "1.0.0", Host: "localhost:8080", - BasePath: "/api", + BasePath: "", Schemes: []string{}, Title: "ClusterCockpit REST API", Description: "API for batch job control.", diff --git a/internal/api/rest.go b/internal/api/rest.go index 8a21e68..2b2a3bd 100644 --- a/internal/api/rest.go +++ b/internal/api/rest.go @@ -46,7 +46,6 @@ import ( // @license.url https://opensource.org/licenses/MIT // @host localhost:8080 -// @basePath /api // @securityDefinitions.apikey ApiKeyAuth // @in header @@ -220,7 +219,7 @@ func handleError(err error, statusCode int, rw http.ResponseWriter) { }) } -func decode(r io.Reader, val interface{}) error { +func decode(r io.Reader, val any) error { dec := json.NewDecoder(r) dec.DisallowUnknownFields() return dec.Decode(val) @@ -238,7 +237,7 @@ func decode(r io.Reader, val interface{}) error { // @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /clusters/ [get] +// @router /api/clusters/ [get] func (api *RestApi) getClusters(rw http.ResponseWriter, r *http.Request) { if user := repository.GetUserFromContext(r.Context()); user != nil && !user.HasRole(schema.RoleApi) { @@ -293,7 +292,7 @@ func (api *RestApi) getClusters(rw http.ResponseWriter, r *http.Request) { // @failure 403 {object} api.ErrorResponse "Forbidden" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/ [get] +// @router /api/jobs/ [get] func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { withMetadata := false filter := &model.JobFilter{} @@ -427,7 +426,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/{id} [get] +// @router /api/jobs/{id} [get] func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request) { // Fetch job from db id, ok := mux.Vars(r)["id"] @@ -520,7 +519,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request) // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/{id} [post] +// @router /api/jobs/{id} [post] func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) { // Fetch job from db id, ok := mux.Vars(r)["id"] @@ -624,7 +623,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) { // @failure 404 {object} api.ErrorResponse "Job does not exist" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/edit_meta/{id} [post] +// @router /api/jobs/edit_meta/{id} [post] func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if err != nil { @@ -670,7 +669,7 @@ func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) { // @failure 404 {object} api.ErrorResponse "Job or tag does not exist" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/tag_job/{id} [post] +// @router /api/jobs/tag_job/{id} [post] func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if err != nil { @@ -839,7 +838,7 @@ func (api *RestApi) removeTags(rw http.ResponseWriter, r *http.Request) { // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/start_job/ [post] +// @router /api/jobs/start_job/ [post] func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { req := schema.JobMeta{BaseJob: schema.JobDefaults} if err := decode(r.Body, &req); err != nil { @@ -912,7 +911,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: job has already been stopped" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/stop_job/ [post] +// @router /api/jobs/stop_job/ [post] func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { // Parse request body req := StopJobApiRequest{} @@ -953,7 +952,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) { // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/delete_job/{id} [delete] +// @router /api/jobs/delete_job/{id} [delete] func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { // Fetch job (that will be stopped) from db id, ok := mux.Vars(r)["id"] @@ -996,7 +995,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) { // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/delete_job/ [delete] +// @router /api/jobs/delete_job/ [delete] func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) { // Parse request body req := DeleteJobApiRequest{} @@ -1046,7 +1045,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) // @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set" // @failure 500 {object} api.ErrorResponse "Internal Server Error" // @security ApiKeyAuth -// @router /jobs/delete_job_before/{ts} [delete] +// @router /api/jobs/delete_job_before/{ts} [delete] func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) { var cnt int // Fetch job (that will be stopped) from db @@ -1183,7 +1182,7 @@ func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) { // @failure 422 {string} string "Unprocessable Entity: creating user failed" // @failure 500 {string} string "Internal Server Error" // @security ApiKeyAuth -// @router /users/ [post] +// @router /config/users/ [post] func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) { // SecuredCheck() only worked with TokenAuth: Removed @@ -1258,7 +1257,7 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) { // @failure 403 {string} string "Forbidden" // @failure 500 {string} string "Internal Server Error" // @security ApiKeyAuth -// @router /users/ [get] +// @router /config/users/ [get] func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) { // SecuredCheck() only worked with TokenAuth: Removed