| /*****************************************************************************\ |
| * openapi.c - OpenAPI plugin handler |
| ***************************************************************************** |
| * Copyright (C) SchedMD LLC. |
| * |
| * This file is part of Slurm, a resource management program. |
| * For details, see <https://slurm.schedmd.com/>. |
| * Please also read the included file: DISCLAIMER. |
| * |
| * Slurm is free software; you can redistribute it and/or modify it under |
| * the terms of the GNU General Public License as published by the Free |
| * Software Foundation; either version 2 of the License, or (at your option) |
| * any later version. |
| * |
| * In addition, as a special exception, the copyright holders give permission |
| * to link the code of portions of this program with the OpenSSL library under |
| * certain conditions as described in each individual source file, and |
| * distribute linked combinations including the two. You must obey the GNU |
| * General Public License in all respects for all of the code used other than |
| * OpenSSL. If you modify file(s) with this exception, you may extend this |
| * exception to your version of the file(s), but you are not obligated to do |
| * so. If you do not wish to do so, delete this exception statement from your |
| * version. If you delete this exception statement from all source files in |
| * the program, then also delete it here. |
| * |
| * Slurm is distributed in the hope that it will be useful, but WITHOUT ANY |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
| * details. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with Slurm; if not, write to the Free Software Foundation, Inc., |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| \*****************************************************************************/ |
| |
| #include "src/common/data.h" |
| #include "src/common/plugrack.h" |
| #include "src/common/read_config.h" |
| #include "src/common/xassert.h" |
| #include "src/common/xmalloc.h" |
| #include "src/common/xstring.h" |
| |
| #include "src/interfaces/serializer.h" |
| #include "src/slurmrestd/openapi.h" |
| #include "src/slurmrestd/operations.h" |
| |
| #define OPENAPI_MAJOR_TYPE "openapi" |
| |
| typedef struct { |
| int (*init)(void); |
| int (*fini)(void); |
| int (*get_paths)(const openapi_path_binding_t **paths_ptr, |
| const openapi_resp_meta_t **meta_ptr); |
| } funcs_t; |
| |
| /* |
| * Must be synchronized with funcs_t above. |
| */ |
| static const char *syms[] = { |
| "slurm_openapi_p_init", |
| "slurm_openapi_p_fini", |
| "slurm_openapi_p_get_paths", |
| }; |
| |
| typedef enum { |
| OPENAPI_PATH_ENTRY_UNKNOWN = 0, |
| OPENAPI_PATH_ENTRY_MATCH_STRING, |
| OPENAPI_PATH_ENTRY_MATCH_PARAMETER, |
| OPENAPI_PATH_ENTRY_MAX |
| } entry_type_t; |
| |
| /* |
| * This is a simplified entry since OAS allows combos of |
| * parameters but we will only honor having a single parameter |
| * as an dir entry for now |
| */ |
| typedef struct { |
| char *entry; |
| char *name; |
| entry_type_t type; |
| openapi_type_t parameter; |
| } entry_t; |
| |
| typedef struct { |
| const openapi_path_binding_method_t *bound; |
| entry_t *entries; |
| http_request_method_t method; |
| } entry_method_t; |
| |
| #define MAGIC_PATH 0x0a0b09fd |
| typedef struct { |
| int magic; /* MAGIC_PATH */ |
| char *path; /* path as string */ |
| const openapi_path_binding_t *bound; |
| data_parser_t *parser; |
| entry_method_t *methods; |
| int tag; |
| } path_t; |
| |
| typedef struct { |
| const data_t *dpath; |
| path_t *path; |
| data_t *params; |
| http_request_method_t method; |
| entry_t *entry; |
| int tag; |
| } match_path_from_data_t; |
| |
| #define MAGIC_MERGE_PATH 0x22b2ae44 |
| typedef struct { |
| int magic; /* MAGIC_MERGE_PATH */ |
| data_t *paths; |
| data_t *server_path; |
| } merge_path_t; |
| |
| #define MAGIC_MERGE_ID_PATH 0x22b2aeae |
| typedef struct { |
| int magic; /* MAGIC_MERGE_ID_PATH */ |
| data_t *server_path; |
| char *operation; |
| char *at; |
| char *path; |
| merge_path_t *merge_args; |
| } id_merge_path_t; |
| |
| #define MAGIC_OAS 0x1218eeee |
| typedef struct { |
| int magic; /* MAGIC_OAS */ |
| const char **mime_types; |
| data_t *spec; |
| data_t *tags; |
| data_t *paths; |
| data_t *components; |
| data_t *components_schemas; |
| data_t *security_schemas; |
| data_t *info; |
| data_t *contact; |
| data_t *license; |
| data_t *servers; |
| data_t *security; |
| data_t *x_slurm; |
| /* tracked references per data_parser */ |
| void **references; |
| } openapi_spec_t; |
| |
| static list_t *paths = NULL; |
| static int path_tag_counter = 0; |
| static plugins_t *plugins = NULL; |
| static data_parser_t **parsers = NULL; /* symlink to parser array */ |
| |
| static const struct { |
| char *openapi_version; |
| struct { |
| char *title; |
| char *desc; |
| char *tos; |
| struct { |
| char *name; |
| char *url; |
| char *email; |
| } contact; |
| struct { |
| char *name; |
| char *url; |
| } license; |
| } info; |
| struct { |
| char *name; |
| char *desc; |
| } tags[3]; |
| struct { |
| char *url; |
| } servers[1]; |
| /* security is too complex for here */ |
| struct { |
| struct security_scheme_s { |
| char *key; |
| char *type; |
| char *desc; |
| char *name; |
| char *in; |
| char *scheme; |
| char *bearer_format; |
| } security_schemes[3]; |
| } components; |
| } openapi_spec = { |
| .openapi_version = "3.0.3", |
| .info = { |
| .title = "Slurm REST API", |
| .desc = "API to access and control Slurm", |
| .tos = "https://github.com/SchedMD/slurm/blob/master/DISCLAIMER", |
| .contact = { |
| .name = "SchedMD LLC", |
| .url = "https://www.schedmd.com/", |
| .email = "sales@schedmd.com", |
| }, |
| .license = { |
| .name = "Apache 2.0", |
| .url = "https://www.apache.org/licenses/LICENSE-2.0.html", |
| }, |
| }, |
| .tags = { |
| { |
| .name = "slurm", |
| .desc = "methods that query slurmctld", |
| }, |
| { |
| .name = "slurmdb", |
| .desc = "methods that query slurmdbd", |
| }, |
| { |
| .name = "openapi", |
| .desc = "methods that query for generated OpenAPI specifications", |
| }, |
| }, |
| .servers = { |
| { |
| .url = "/", |
| }, |
| }, |
| .components = { |
| .security_schemes = { |
| { |
| #define SEC_SCHEME_USER_INDEX 0 |
| .key = "user", |
| .type = "apiKey", |
| .desc = "User name", |
| .name = "X-SLURM-USER-NAME", |
| .in = "header", |
| }, |
| { |
| #define SEC_SCHEME_TOKEN_INDEX 1 |
| .key = "token", |
| .type = "apiKey", |
| .desc = "User access token", |
| .name = "X-SLURM-USER-TOKEN", |
| .in = "header", |
| }, |
| { |
| #define SEC_SCHEME_BEARER_INDEX 2 |
| .key = "bearerAuth", |
| .type = "http", |
| .desc = "Bearer Authentication", |
| .scheme = "bearer", |
| .bearer_format = "JWT", |
| }, |
| }, |
| }, |
| }; |
| |
| static const openapi_path_binding_method_t openapi_methods[] = { |
| { |
| .method = HTTP_REQUEST_GET, |
| .tags = (const char*[]) { "openapi", NULL }, |
| .summary = "Retrieve OpenAPI Specification", |
| .response = { |
| .type = DATA_PARSER_OPENAPI_SHARES_RESP, |
| .description = "OpenAPI Specification", |
| }, |
| .parameters = DATA_PARSER_SHARES_REQ_MSG, |
| }, |
| {0} |
| }; |
| |
| static int _op_handler_openapi(openapi_ctxt_t *ctxt); |
| |
| #define OP_FLAGS (OP_BIND_HIDDEN_OAS | OP_BIND_NO_SLURMDBD) |
| |
| /* |
| * Paths to generate OpenAPI specification |
| */ |
| static const openapi_path_binding_t openapi_paths[] = { |
| { |
| .path = "/openapi.json", |
| .callback = _op_handler_openapi, |
| .methods = openapi_methods, |
| .flags = OP_FLAGS, |
| }, |
| { |
| .path = "/openapi.yaml", |
| .callback = _op_handler_openapi, |
| .methods = openapi_methods, |
| .flags = OP_FLAGS, |
| }, |
| { |
| .path = "/openapi", |
| .callback = _op_handler_openapi, |
| .methods = openapi_methods, |
| .flags = OP_FLAGS, |
| }, |
| { |
| .path = "/openapi/v3", |
| .callback = _op_handler_openapi, |
| .methods = openapi_methods, |
| .flags = OP_FLAGS, |
| }, |
| {0} |
| }; |
| |
| static const http_status_code_t *response_status_codes = NULL; |
| /* |
| * Default to "default" and 200 as openapi generator breaks with only "default" |
| * response code. |
| */ |
| static const http_status_code_t default_response_status_codes[] = { |
| HTTP_STATUS_CODE_SUCCESS_OK, |
| HTTP_STATUS_CODE_DEFAULT, |
| HTTP_STATUS_NONE |
| }; |
| |
| static char *_entry_to_string(entry_t *entry); |
| |
| static const char *_get_entry_type_string(entry_type_t type) |
| { |
| switch (type) { |
| case OPENAPI_PATH_ENTRY_MATCH_STRING: |
| return "string"; |
| case OPENAPI_PATH_ENTRY_MATCH_PARAMETER: |
| return "parameter"; |
| default: |
| return "invalid"; |
| } |
| } |
| |
| static int _resolve_parser_index(data_parser_t *parser) |
| { |
| for (int i = 0; parsers[i]; i++) |
| if (parsers[i] == parser) |
| return i; |
| |
| fatal_abort("Unable to find parser. This should never happen!"); |
| } |
| |
| static void _free_entry_list(entry_t *entry, int tag, |
| entry_method_t *method) |
| { |
| entry_t *itr = entry; |
| |
| if (!entry) |
| return; |
| |
| while (itr->type) { |
| debug5("%s: remove path tag:%d method:%s entry:%s name:%s", |
| __func__, tag, |
| (method ? get_http_method_string(method->method) : |
| "N/A"), |
| itr->entry, itr->name); |
| xfree(itr->entry); |
| xfree(itr->name); |
| itr++; |
| } |
| |
| xfree(entry); |
| } |
| |
| static void _list_delete_path_t(void *x) |
| { |
| entry_method_t *em; |
| |
| if (!x) |
| return; |
| |
| path_t *path = x; |
| xassert(path->magic == MAGIC_PATH); |
| xassert(path->tag != -1); |
| em = path->methods; |
| |
| while (em->entries) { |
| debug5("%s: remove path tag:%d method:%s", __func__, path->tag, |
| get_http_method_string(em->method)); |
| |
| _free_entry_list(em->entries, path->tag, em); |
| em->entries = NULL; |
| em++; |
| } |
| |
| xfree(path->methods); |
| xfree(path->path); |
| path->magic = ~MAGIC_PATH; |
| xfree(path); |
| } |
| |
| static entry_t *_parse_openapi_path(const char *str_path, int *count_ptr) |
| { |
| char *save_ptr = NULL; |
| char *buffer = xstrdup(str_path); |
| char *token = strtok_r(buffer, "/", &save_ptr); |
| entry_t *entries = NULL; |
| entry_t *entry = NULL; |
| int count = 0; |
| |
| /* find max bound on number of entries */ |
| for (const char *i = str_path; *i; i++) |
| if (*i == '/') |
| count++; |
| |
| if (count > 1024) |
| fatal_abort("%s: url %s is way too long", str_path, __func__); |
| |
| entry = entries = xcalloc((count + 1), sizeof(entry_t)); |
| |
| while (token) { |
| const size_t slen = strlen(token); |
| |
| /* ignore // entries */ |
| if (slen <= 0) |
| goto again; |
| |
| entry->entry = xstrdup(token); |
| |
| if (!xstrcmp(token, ".") || !xstrcmp(token, "..")) { |
| /* |
| * there should not be a .. or . in a path |
| * definition, it just doesn't make any sense |
| */ |
| error("%s: invalid %s at entry", |
| __func__, token); |
| goto fail; |
| } else if (slen > 3 && token[0] == '{' && |
| token[slen - 1] == '}') { |
| entry->type = OPENAPI_PATH_ENTRY_MATCH_PARAMETER; |
| entry->name = xstrndup(token + 1, slen - 2); |
| |
| debug5("%s: parameter %s at entry %s", |
| __func__, entry->name, token); |
| } else { /* not a variable */ |
| entry->type = OPENAPI_PATH_ENTRY_MATCH_STRING; |
| entry->name = NULL; |
| |
| debug5("%s: string match entry %s", |
| __func__, token); |
| } |
| |
| entry++; |
| xassert(entry <= entries + count); |
| again: |
| token = strtok_r(NULL, "/", &save_ptr); |
| } |
| |
| /* last is always NULL */ |
| xassert(!entry->type); |
| xfree(buffer); |
| if (count_ptr) |
| *count_ptr = count; |
| return entries; |
| |
| fail: |
| _free_entry_list(entries, -1, NULL); |
| xfree(buffer); |
| if (count_ptr) |
| *count_ptr = -1; |
| return NULL; |
| } |
| |
| static int _print_path_tag_methods(void *x, void *arg) |
| { |
| path_t *path = (path_t *) x; |
| int *tag = (int *) arg; |
| |
| xassert(path->magic == MAGIC_PATH); |
| |
| if (path->tag != *tag) |
| return 0; |
| |
| if (!path->methods->entries) |
| debug4("%s: no methods found in path tag %d", |
| __func__, path->tag); |
| |
| for (entry_method_t *em = path->methods; em->entries; em++) { |
| char *path_str = _entry_to_string(em->entries); |
| |
| debug4("%s: path tag %d entry: %s %s", |
| __func__, path->tag, get_http_method_string(em->method), |
| path_str); |
| |
| xfree(path_str); |
| } |
| |
| /* |
| * We found the (unique) tag, so return -1 to exit early. The item's |
| * index returned by list_for_each_ro() will be negative. |
| */ |
| return -1; |
| } |
| |
| extern void print_path_tag_methods(int tag) |
| { |
| if (get_log_level() < LOG_LEVEL_DEBUG4) |
| return; |
| |
| if (list_for_each_ro(paths, _print_path_tag_methods, &tag) >= 0) |
| error("%s: Tag %d not found in paths", __func__, tag); |
| } |
| |
| static void _clone_entries(entry_t **dst_ptr, entry_t *src, int count) |
| { |
| entry_t *dst = xcalloc((count + 1), sizeof(*dst)); |
| |
| xassert(!*dst_ptr); |
| xassert(count > 0); |
| |
| *dst_ptr = dst; |
| |
| for (; src->type; src++, dst++) { |
| dst->entry = xstrdup(src->entry); |
| dst->name = xstrdup(src->name); |
| dst->type = src->type; |
| dst->parameter = src->parameter; |
| } |
| } |
| |
| static void _check_openapi_path_binding(const openapi_path_binding_t *op_path) |
| { |
| #ifndef NDEBUG |
| xassert(op_path->path); |
| xassert(op_path->callback); |
| xassert((op_path->flags == OP_BIND_NONE) || |
| ((op_path->flags > OP_BIND_NONE) && |
| (op_path->flags < OP_BIND_INVALID_MAX))); |
| |
| for (int i = 0;; i++) { |
| const openapi_path_binding_method_t *method = |
| &op_path->methods[i]; |
| |
| if (method->method == HTTP_REQUEST_INVALID) |
| break; |
| |
| xassert(method->summary && method->summary[0]); |
| xassert(method->response.description && |
| method->response.description[0]); |
| xassert(method->method > HTTP_REQUEST_INVALID); |
| xassert(method->method < HTTP_REQUEST_MAX); |
| xassert(method->tags && method->tags[0]); |
| xassert(method->response.type > DATA_PARSER_TYPE_INVALID); |
| xassert(method->response.type < DATA_PARSER_TYPE_MAX); |
| xassert(method->parameters >= DATA_PARSER_TYPE_INVALID); |
| xassert(method->parameters < DATA_PARSER_TYPE_MAX); |
| xassert(method->query >= DATA_PARSER_TYPE_INVALID); |
| xassert(method->query < DATA_PARSER_TYPE_MAX); |
| xassert(method->body.type >= DATA_PARSER_TYPE_INVALID); |
| xassert(method->body.type < DATA_PARSER_TYPE_MAX); |
| } |
| #endif /* !NDEBUG */ |
| } |
| |
| static bool _data_parser_supports_type(data_parser_t *parser, |
| data_parser_type_t type) |
| { |
| openapi_type_t oapi_type; |
| |
| oapi_type = data_parser_g_resolve_openapi_type(parser, type, NULL); |
| xassert(oapi_type >= OPENAPI_TYPE_INVALID); |
| xassert(oapi_type < OPENAPI_TYPE_MAX); |
| |
| return (oapi_type != OPENAPI_TYPE_INVALID); |
| } |
| |
| static bool _data_parser_supports_method(data_parser_t *parser, |
| const openapi_path_binding_method_t *m) |
| { |
| /* check that parser supports each possible type if set */ |
| |
| if ((m->response.type != DATA_PARSER_TYPE_INVALID) && |
| !_data_parser_supports_type(parser, m->response.type)) |
| return false; |
| |
| if ((m->parameters != DATA_PARSER_TYPE_INVALID) && |
| !_data_parser_supports_type(parser, m->parameters)) |
| return false; |
| |
| if ((m->query != DATA_PARSER_TYPE_INVALID) && |
| !_data_parser_supports_type(parser, m->query)) |
| return false; |
| |
| if ((m->body.type != DATA_PARSER_TYPE_INVALID) && |
| !_data_parser_supports_type(parser, m->body.type)) |
| return false; |
| |
| return true; |
| } |
| |
| extern int register_path_binding(const char *in_path, |
| const openapi_path_binding_t *op_path, |
| const openapi_resp_meta_t *meta, |
| data_parser_t *parser, int *tag_ptr) |
| { |
| entry_t *entries = NULL; |
| int tag = -1, methods_count = 0, entries_count = 0; |
| path_t *p = NULL; |
| const char *path = (in_path ? in_path : op_path->path); |
| |
| debug4("%s: attempting to bind %s with %s", |
| __func__, (parser ? data_parser_get_plugin_version(parser) : |
| "data_parser/none"), path); |
| |
| xassert(!!in_path == !!(op_path->flags & OP_BIND_DATA_PARSER)); |
| _check_openapi_path_binding(op_path); |
| |
| if (!(entries = _parse_openapi_path(path, &entries_count))) |
| fatal("%s: parse_openapi_path(%s) failed", __func__, path); |
| |
| for (int i = 0; op_path->methods[i].method != HTTP_REQUEST_INVALID; |
| i++) { |
| if (!_data_parser_supports_method(parser, &op_path->methods[i])) |
| continue; |
| |
| methods_count++; |
| } |
| |
| if (!methods_count) { |
| debug5("%s: skip binding %s with %s", |
| __func__, path, data_parser_get_plugin(parser)); |
| _free_entry_list(entries, -1, NULL); |
| return ESLURM_NOT_SUPPORTED; |
| } |
| |
| tag = path_tag_counter++; |
| |
| p = xmalloc(sizeof(*p)); |
| p->magic = MAGIC_PATH; |
| p->methods = xcalloc((methods_count + 1), sizeof(*p->methods)); |
| p->tag = tag; |
| p->bound = op_path; |
| p->parser = parser; |
| p->path = xstrdup(path); |
| |
| for (int i = 0, mi = 0;; i++) { |
| const openapi_path_binding_method_t *m = &op_path->methods[i]; |
| entry_method_t *t = &p->methods[mi]; |
| entry_t *e; |
| |
| if (m->method == HTTP_REQUEST_INVALID) |
| break; |
| |
| /* |
| * Skip method if data_parser does not support any of the in/out |
| * types which would just cause slurmrestd to abort() later. |
| */ |
| if (!_data_parser_supports_method(parser, m)) { |
| debug5("%s: skip binding \"%s %s\" with %s", |
| __func__, get_http_method_string(m->method), |
| path, data_parser_get_plugin(parser)); |
| continue; |
| } |
| |
| t->method = m->method; |
| t->bound = m; |
| |
| if (i != 0) { |
| _clone_entries(&t->entries, entries, entries_count); |
| e = t->entries; |
| } else { |
| p->methods[0].entries = e = entries; |
| } |
| |
| for (; e->type; e++) { |
| if (e->type == OPENAPI_PATH_ENTRY_MATCH_PARAMETER) |
| e->parameter = |
| data_parser_g_resolve_openapi_type( |
| parser, m->parameters, e->name); |
| |
| debug5("%s: add binded path %s entry: method=%s tag=%d entry=%s name=%s parameter=%s entry_type=%s", |
| __func__, path, |
| get_http_method_string(m->method), tag, e->entry, |
| e->name, openapi_type_to_string(e->parameter), |
| _get_entry_type_string(e->type)); |
| } |
| |
| /* only move to next method if populated */ |
| mi++; |
| } |
| |
| list_append(paths, p); |
| *tag_ptr = tag; |
| return SLURM_SUCCESS; |
| } |
| |
| /* |
| * Check if the entry matches based on the OAS type |
| * and if it does, then add that matched parameter |
| */ |
| static bool _match_param(const data_t *data, match_path_from_data_t *args) |
| { |
| bool matched = false; |
| entry_t *entry = args->entry; |
| data_t *params = args->params; |
| data_t *match = data_new(); |
| |
| data_copy(match, data); |
| |
| switch (entry->parameter) { |
| case OPENAPI_TYPE_NUMBER: |
| { |
| if (data_convert_type(match, DATA_TYPE_FLOAT) == |
| DATA_TYPE_FLOAT) { |
| data_set_float(data_key_set(params, entry->name), |
| data_get_float(match)); |
| matched = true; |
| } |
| break; |
| } |
| case OPENAPI_TYPE_INTEGER: |
| { |
| if (data_convert_type(match, DATA_TYPE_INT_64) == |
| DATA_TYPE_INT_64) { |
| data_set_int(data_key_set(params, entry->name), |
| data_get_int(match)); |
| matched = true; |
| } |
| break; |
| } |
| default: /* assume string */ |
| debug("%s: unknown parameter type %s", |
| __func__, openapi_type_to_string(entry->parameter)); |
| /* fall through */ |
| case OPENAPI_TYPE_STRING: |
| { |
| if (data_convert_type(match, DATA_TYPE_STRING) == |
| DATA_TYPE_STRING) { |
| data_set_string(data_key_set(params, entry->name), |
| data_get_string(match)); |
| matched = true; |
| } |
| break; |
| } |
| } |
| |
| if (get_log_level() >= LOG_LEVEL_DEBUG5) { |
| char *str = NULL; |
| data_get_string_converted(data, &str); |
| |
| debug5("%s: parameter %s[%s]->%s[%s] result=%s", |
| __func__, entry->name, |
| openapi_type_to_string(entry->parameter), |
| str, data_get_type_string(data), |
| (matched ? "matched" : "failed")); |
| |
| xfree(str); |
| } |
| |
| FREE_NULL_DATA(match); |
| return matched; |
| } |
| |
| static data_for_each_cmd_t _match_path(const data_t *data, void *y) |
| { |
| match_path_from_data_t *args = y; |
| entry_t *entry = args->entry; |
| |
| if (!entry->type) |
| return DATA_FOR_EACH_FAIL; |
| |
| if (entry->type == OPENAPI_PATH_ENTRY_MATCH_STRING) { |
| bool match; |
| |
| if (data_get_type(data) != DATA_TYPE_STRING) |
| return DATA_FOR_EACH_FAIL; |
| |
| match = !xstrcmp(data_get_string(data), entry->entry); |
| |
| debug5("%s: string attempt match %s to %s: %s", |
| __func__, entry->entry, data_get_string(data), |
| (match ? "SUCCESS" : "FAILURE")); |
| |
| if (!match) |
| return DATA_FOR_EACH_FAIL; |
| } else if (entry->type == OPENAPI_PATH_ENTRY_MATCH_PARAMETER) { |
| if (!_match_param(data, args)) |
| return DATA_FOR_EACH_FAIL; |
| } else |
| fatal_abort("%s: unknown OAS path entry match type", |
| __func__); |
| |
| args->entry++; |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| static char *_entry_to_string(entry_t *entry) |
| { |
| char *path = NULL; |
| data_t *d = data_set_list(data_new()); |
| |
| for (; entry->type; entry++) { |
| switch (entry->type) { |
| case OPENAPI_PATH_ENTRY_MATCH_STRING: |
| data_set_string(data_list_append(d), entry->entry); |
| break; |
| case OPENAPI_PATH_ENTRY_MATCH_PARAMETER: |
| data_set_string_fmt(data_list_append(d), "{%s}", |
| entry->name); |
| break; |
| case OPENAPI_PATH_ENTRY_UNKNOWN: |
| case OPENAPI_PATH_ENTRY_MAX: |
| fatal_abort("invalid entry type"); |
| } |
| } |
| |
| serialize_g_data_to_string(&path, NULL, d, MIME_TYPE_JSON, |
| SER_FLAGS_COMPACT); |
| |
| FREE_NULL_DATA(d); |
| return path; |
| } |
| |
| static int _match_path_from_data(void *x, void *key) |
| { |
| char *dst_path = NULL, *src_path = NULL; |
| match_path_from_data_t *args = key; |
| path_t *path = x; |
| entry_method_t *method; |
| bool matched = false; |
| |
| xassert(path->magic == MAGIC_PATH); |
| |
| if (get_log_level() >= LOG_LEVEL_DEBUG5) { |
| serialize_g_data_to_string(&dst_path, NULL, args->dpath, |
| MIME_TYPE_JSON, SER_FLAGS_COMPACT); |
| } |
| |
| args->path = path; |
| for (method = path->methods; method->entries; method++) { |
| int entries = 0; |
| |
| if (get_log_level() >= LOG_LEVEL_DEBUG5) { |
| xfree(src_path); |
| src_path = _entry_to_string(method->entries); |
| } |
| |
| if (args->method != method->method) { |
| debug5("%s: method skip for %s(%d, %s != %s) to %s(0x%"PRIXPTR")", |
| __func__, src_path, args->path->tag, |
| get_http_method_string(args->method), |
| get_http_method_string(method->method), |
| dst_path, (uintptr_t) args->dpath); |
| continue; |
| } |
| |
| for (args->entry = method->entries; args->entry->type; |
| entries++, args->entry++) |
| /* do nothing */; |
| |
| if (data_get_list_length(args->dpath) != entries) { |
| debug5("%s: skip non-matching subdirectories: registered=%u requested=%zu ", |
| __func__, entries, |
| data_get_list_length(args->dpath)); |
| continue; |
| } |
| |
| args->entry = method->entries; |
| if (data_list_for_each_const(args->dpath, _match_path, |
| args) < 0) { |
| debug5("%s: match failed %s", |
| __func__, args->entry->entry); |
| continue; |
| } |
| |
| /* |
| * The list is NULL terminated, so if entry->type is not NULL |
| * we didn't match the whole list, but we already don't have |
| * anything to compare in request. |
| */ |
| if (!args->entry->type) { |
| args->tag = path->tag; |
| matched = true; |
| break; |
| } |
| } |
| |
| debug5("%s: match %s for %s(%d, %s) to %s(0x%"PRIXPTR")", |
| __func__, |
| matched ? "successful" : "failed", |
| src_path, |
| args->path->tag, |
| get_http_method_string(args->method), |
| dst_path, |
| (uintptr_t) args->dpath); |
| |
| xfree(src_path); |
| xfree(dst_path); |
| |
| return matched; |
| } |
| |
| extern int find_path_tag(const data_t *dpath, data_t *params, |
| http_request_method_t method) |
| { |
| match_path_from_data_t args = { |
| .params = params, |
| .dpath = dpath, |
| .method = method, |
| .tag = -1, |
| }; |
| |
| xassert(data_get_type(params) == DATA_TYPE_DICT); |
| |
| (void) list_find_first(paths, _match_path_from_data, &args); |
| |
| return args.tag; |
| } |
| |
| static int _bind_paths(const openapi_path_binding_t *paths, |
| const openapi_resp_meta_t *meta) |
| { |
| int rc = SLURM_SUCCESS; |
| |
| for (int i = 0; paths[i].path; i++) { |
| const openapi_path_binding_t *op_path = &paths[i]; |
| |
| if ((rc = bind_operation_path(op_path, meta))) |
| break; |
| } |
| |
| return rc; |
| } |
| |
| extern int init_openapi(const char *plugin_list, plugrack_foreach_t listf, |
| data_parser_t **parsers_ptr, |
| const http_status_code_t *resp_status_codes) |
| { |
| int rc; |
| |
| if (paths) |
| fatal("%s called twice", __func__); |
| |
| if (resp_status_codes) |
| response_status_codes = resp_status_codes; |
| else |
| response_status_codes = default_response_status_codes; |
| |
| paths = list_create(_list_delete_path_t); |
| |
| /* must have JSON plugin to parse the openapi.json */ |
| serializer_required(MIME_TYPE_JSON); |
| |
| if ((rc = _bind_paths(openapi_paths, NULL))) |
| fatal("Unable to bind openapi specification paths: %s", |
| slurm_strerror(rc)); |
| |
| rc = load_plugins(&plugins, OPENAPI_MAJOR_TYPE, plugin_list, listf, |
| syms, ARRAY_SIZE(syms)); |
| |
| if (!xstrcasecmp("list", plugin_list)) |
| return SLURM_SUCCESS; |
| |
| if (rc) |
| fatal("Loading OpenAPI plugins failed: %s", slurm_strerror(rc)); |
| |
| if ((rc = load_plugins(&plugins, OPENAPI_MAJOR_TYPE, plugin_list, listf, |
| syms, ARRAY_SIZE(syms)))) |
| fatal("Loading OpenAPI plugins failed: %s", slurm_strerror(rc)); |
| |
| if (!plugins->count) |
| fatal("No OpenAPI plugins loaded."); |
| |
| parsers = parsers_ptr; |
| |
| for (size_t i = 0; i < plugins->count; i++) { |
| const funcs_t *funcs = plugins->functions[i]; |
| const openapi_path_binding_t *paths; |
| const openapi_resp_meta_t *meta; |
| |
| if ((rc = funcs->get_paths(&paths, &meta))) |
| fatal("Failure loading plugin path bindings: %s", |
| slurm_strerror(rc)); |
| |
| if ((rc = _bind_paths(paths, meta))) |
| fatal("Unable to bind openapi specification paths: %s", |
| slurm_strerror(rc)); |
| } |
| |
| /* Call init() after all plugins are fully loaded */ |
| for (size_t i = 0; i < plugins->count; i++) { |
| const funcs_t *funcs = plugins->functions[i]; |
| funcs->init(); |
| } |
| |
| return rc; |
| } |
| |
| extern void destroy_openapi(void) |
| { |
| if (!paths) |
| return; |
| |
| for (size_t i = 0; i < plugins->count; i++) { |
| const funcs_t *funcs = plugins->functions[i]; |
| funcs->fini(); |
| } |
| |
| FREE_NULL_PLUGINS(plugins); |
| FREE_NULL_LIST(paths); |
| } |
| |
| static data_for_each_cmd_t _merge_operationId_strings(data_t *data, void *arg) |
| { |
| id_merge_path_t *args = arg; |
| char *p; |
| |
| xassert(args->magic == MAGIC_MERGE_ID_PATH); |
| xassert(args->merge_args->magic == MAGIC_MERGE_PATH); |
| |
| if (data_convert_type(data, DATA_TYPE_STRING) != DATA_TYPE_STRING) |
| return DATA_FOR_EACH_FAIL; |
| |
| p = xstrdup(data_get_string(data)); |
| |
| /* sub out '.' for '_' to avoid breaking compilers */ |
| for (int s = strlen(p), i = 0; i < s; i++) |
| if ((p[i] == '.') || (p[i] == '{') || (p[i] == '}')) |
| p[i] = '_'; |
| |
| xstrfmtcatat(args->operation, &args->at, "%s%s", |
| (args->operation ? "_" : ""), data_get_string(data)); |
| |
| xfree(p); |
| |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| static data_for_each_cmd_t _foreach_strip_params(data_t *data, void *arg) |
| { |
| const char *item = data_get_string(data); |
| data_t **last_ptr = arg; |
| int len = strlen(item); |
| |
| xassert(item); |
| if (!item || (item[0] != '{')) { |
| char *dst = xstrdup(item); |
| char *last = dst; |
| |
| /* strip out '.' */ |
| for (int i = 0; i < len; i++) { |
| if (item[i] == '.') |
| continue; |
| |
| *last = item[i]; |
| last++; |
| } |
| |
| *last = '\0'; |
| |
| data_set_string_own(data, dst); |
| |
| *last_ptr = data; |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| xassert(len > 2); |
| xassert(item[len - 1] == '}'); |
| if (*last_ptr && |
| !xstrncmp(data_get_string(*last_ptr), (item + 1), (len - 2))) { |
| /* |
| * Last item is the same as the parameter name which means that |
| * the item is uncountable and we need to set last as single. |
| */ |
| data_set_string(data, data_get_string(*last_ptr)); |
| data_set_string(*last_ptr, "single"); |
| return DATA_FOR_EACH_CONT; |
| } |
| |
| return DATA_FOR_EACH_DELETE; |
| } |
| |
| /* Caller must xfree() returned string */ |
| static char *_get_method_operationId(openapi_spec_t *spec, path_t *path, |
| const openapi_path_binding_method_t |
| *method) |
| { |
| data_t *merge[10] = {0}, *dpath, *merged = NULL, *last = NULL; |
| const char *method_str = get_http_method_string_lc(method->method); |
| int i = 0; |
| merge_path_t merge_args = { |
| .magic = MAGIC_MERGE_PATH, |
| .paths = spec->paths, |
| }; |
| id_merge_path_t merge_id_args = { |
| .magic = MAGIC_MERGE_ID_PATH, |
| .merge_args = &merge_args, |
| }; |
| |
| dpath = parse_url_path(path->path, false, true); |
| |
| xassert((data_get_list_length(dpath) + 1) < (ARRAY_SIZE(merge) - 1)); |
| |
| (void) data_list_for_each(dpath, _foreach_strip_params, &last); |
| |
| if (data_get_list_length(dpath) < 3) { |
| /* unversioned paths */ |
| merge[i++] = data_set_string(data_new(), method_str); |
| } else { |
| merge[i++] = data_list_dequeue(dpath); /* slurm vs slurmdb */ |
| merge[i++] = data_list_dequeue(dpath); /* v0.0.XX */ |
| merge[i++] = data_set_string(data_new(), method_str); |
| } |
| |
| while (data_get_list_length(dpath) && (i < (ARRAY_SIZE(merge) - 1))) |
| merge[i++] = data_list_dequeue(dpath); |
| |
| merged = data_list_join((const data_t **) merge, true); |
| if (data_list_for_each(merged, _merge_operationId_strings, |
| &merge_id_args) < 0) |
| fatal_abort("_merge_operationId_strings() failed which should never happen"); |
| |
| for (i = 0; i < ARRAY_SIZE(merge); i++) |
| FREE_NULL_DATA(merge[i]); |
| FREE_NULL_DATA(merged); |
| FREE_NULL_DATA(dpath); |
| |
| debug5("%s: [%s %s] setting OperationId: %s", |
| __func__, method_str, path->path, merge_id_args.operation); |
| |
| return merge_id_args.operation; |
| } |
| |
| static int _populate_method(path_t *path, openapi_spec_t *spec, data_t *dpath, |
| const openapi_path_binding_method_t *method) |
| { |
| const char **mime_types = spec->mime_types; |
| void *refs = &spec->references[_resolve_parser_index(path->parser)]; |
| data_t *dmethod = data_set_dict(data_key_set(dpath, |
| get_http_method_string_lc(method->method))); |
| data_t *dtags = data_set_list(data_key_set(dmethod, "tags")); |
| |
| for (int i = 0; method->tags[i]; i++) |
| data_set_string(data_list_append(dtags), method->tags[i]); |
| |
| if (method->summary) |
| data_set_string(data_key_set(dmethod, "summary"), |
| method->summary); |
| if (method->description) |
| data_set_string(data_key_set(dmethod, "description"), |
| method->description); |
| |
| if (data_parser_g_is_deprecated(path->parser)) |
| data_set_bool(data_key_set(dmethod, "deprecated"), true); |
| |
| { |
| char *opid = _get_method_operationId(spec, path, method); |
| data_set_string_own(data_key_set(dmethod, "operationId"), opid); |
| } |
| |
| if (method->parameters || method->query) { |
| /* |
| * Use existing replacements |
| */ |
| data_t *dst = data_key_set(dmethod, "parameters"); |
| |
| if (data_parser_g_populate_parameters(path->parser, |
| method->parameters, |
| method->query, refs, dst, |
| spec->components_schemas)) |
| fatal_abort("data_parser_g_populate_parameters() failed"); |
| } |
| |
| if (method->response.type) { |
| data_t *dresp = data_set_dict(data_key_set(dmethod, "responses")); |
| data_t *resp_code = data_set_dict(data_new()); |
| data_t *cnt = data_set_dict(data_key_set(resp_code, "content")); |
| |
| if (method->response.description) |
| data_set_string(data_set_dict(data_key_set(resp_code, |
| "description")), method->response.description); |
| |
| for (int i = 0; mime_types[i]; i++) { |
| data_t *dtype, *dschema; |
| |
| /* |
| * Never return URL encoded mimetype as it is only for |
| * HTTP query |
| */ |
| if (!xstrcmp(mime_types[i], MIME_TYPE_URL_ENCODED)) |
| continue; |
| |
| dtype = data_set_dict(data_key_set(cnt, mime_types[i])); |
| dschema = data_set_dict(data_key_set(dtype, "schema")); |
| |
| if (data_parser_g_populate_schema(path->parser, |
| method->response.type, refs, dschema, |
| spec->components_schemas)) |
| fatal_abort("data_parser_g_populate_schema() failed"); |
| } |
| |
| for (int i = 0; response_status_codes[i]; i++) { |
| const http_status_code_t code = |
| response_status_codes[i]; |
| char str[64]; |
| |
| if (code == HTTP_STATUS_CODE_DEFAULT) |
| snprintf(str, sizeof(str), "%s", |
| get_http_status_code_string(code)); |
| else |
| snprintf(str, sizeof(str), "%u", code); |
| |
| data_copy(data_key_set(dresp, str), resp_code); |
| } |
| |
| FREE_NULL_DATA(resp_code); |
| } |
| |
| if (method->body.type) { |
| data_t *dbody = data_set_dict(data_key_set(dmethod, |
| "requestBody")); |
| data_t *cnt = data_set_dict(data_key_set(dbody, "content")); |
| |
| if (method->body.description) |
| data_set_string(data_set_dict(data_key_set(dbody, |
| "description")), method->body.description); |
| |
| for (int i = 0; mime_types[i]; i++) { |
| data_t *dtype, *dschema; |
| |
| /* |
| * Never return URL encoded mimetype as it is only for |
| * HTTP query |
| */ |
| if (!xstrcmp(mime_types[i], MIME_TYPE_URL_ENCODED)) |
| continue; |
| |
| dtype = data_set_dict(data_key_set(cnt, mime_types[i])); |
| dschema = data_set_dict(data_key_set(dtype, "schema")); |
| |
| if (data_parser_g_populate_schema(path->parser, |
| method->body.type, refs, dschema, |
| spec->components_schemas)) |
| fatal_abort("data_parser_g_populate_schema() failed"); |
| } |
| |
| } |
| |
| return SLURM_SUCCESS; |
| } |
| |
| static int _foreach_add_path(void *x, void *arg) |
| { |
| int rc = SLURM_SUCCESS; |
| path_t *path = x; |
| openapi_spec_t *spec = arg; |
| const openapi_path_binding_t *bound = path->bound; |
| data_t *dpath; |
| |
| xassert(spec->magic == MAGIC_OAS); |
| xassert(path->magic == MAGIC_PATH); |
| |
| if (!bound) |
| return SLURM_SUCCESS; |
| |
| if (bound->flags & OP_BIND_HIDDEN_OAS) |
| return SLURM_SUCCESS; |
| |
| xassert(!data_key_get(spec->paths, path->path)); |
| dpath = data_set_dict(data_key_set(spec->paths, path->path)); |
| |
| for (int i = 0; !rc && path->methods[i].method; i++) |
| rc = _populate_method(path, spec, dpath, |
| path->methods[i].bound); |
| |
| return rc; |
| } |
| |
| static int _foreach_count_path(void *x, void *arg) |
| { |
| path_t *path = x; |
| openapi_spec_t *spec = arg; |
| const char **mime_types = spec->mime_types; |
| const openapi_path_binding_t *bound = path->bound; |
| void *refs; |
| |
| xassert(spec->magic == MAGIC_OAS); |
| xassert(path->magic == MAGIC_PATH); |
| |
| if (!bound) |
| return SLURM_SUCCESS; |
| |
| refs = &spec->references[_resolve_parser_index(path->parser)]; |
| |
| for (int i = 0; path->methods[i].method; i++) { |
| const openapi_path_binding_method_t *method = |
| path->methods[i].bound; |
| |
| if (method->parameters && |
| data_parser_g_increment_reference(path->parser, |
| method->parameters, refs)) |
| fatal_abort("data_parser_g_increment_reference() failed"); |
| |
| if (method->query && |
| data_parser_g_increment_reference(path->parser, |
| method->query, refs)) |
| fatal_abort("data_parser_g_increment_reference() failed"); |
| |
| if (method->body.type) { |
| /* |
| * Need to add 1 reference per mime type that will get |
| * dumped |
| */ |
| for (int i = 0; mime_types[i]; i++) |
| if (data_parser_g_increment_reference( |
| path->parser, method->body.type, refs)) |
| fatal_abort("data_parser_g_increment_reference() failed"); |
| } |
| |
| if (method->response.type) { |
| /* |
| * Need to add 1 reference per mime type that will get |
| * dumped |
| */ |
| for (int i = 0; mime_types[i]; i++) |
| if (data_parser_g_increment_reference( |
| path->parser, method->response.type, |
| refs)) |
| fatal_abort("data_parser_g_increment_reference() failed"); |
| } |
| } |
| |
| return SLURM_SUCCESS; |
| } |
| |
| extern int generate_spec(data_t *dst, const char **mime_types) |
| { |
| openapi_spec_t spec = { |
| .magic = MAGIC_OAS, |
| .spec = dst, |
| .mime_types = mime_types, |
| }; |
| data_t *security1, *security2, *security3; |
| data_t *openapi_plugins, *data_parsers, *slurm_version; |
| char *version = xstrdup_printf("Slurm-%s", SLURM_VERSION_STRING); |
| int parsers_count; |
| |
| /* count the parsers present to allocate refs counts */ |
| for (parsers_count = 0; parsers[parsers_count]; parsers_count++); |
| spec.references = xcalloc(parsers_count, sizeof(*spec.references)); |
| |
| data_set_dict(spec.spec); |
| spec.tags = data_set_list(data_key_set(spec.spec, "tags")); |
| spec.paths = data_set_dict(data_key_set(spec.spec, "paths")); |
| spec.components = data_set_dict(data_key_set(spec.spec, "components")); |
| spec.components_schemas = data_set_dict(data_key_set(spec.components, |
| "schemas")); |
| spec.security_schemas = data_set_dict(data_key_set(spec.components, |
| "securitySchemes")); |
| spec.info = data_set_dict(data_key_set(spec.spec, "info")); |
| spec.contact = data_set_dict(data_key_set(spec.info, "contact")); |
| spec.license = data_set_dict(data_key_set(spec.info, "license")); |
| spec.x_slurm = data_set_dict(data_key_set(spec.info, "x-slurm")); |
| spec.servers = data_set_list(data_key_set(spec.spec, "servers")); |
| spec.security = data_set_list(data_key_set(spec.spec, "security")); |
| security1 = data_set_dict(data_list_append(spec.security)); |
| security2 = data_set_dict(data_list_append(spec.security)); |
| security3 = data_set_dict(data_list_append(spec.security)); |
| data_parsers = |
| data_set_list(data_key_set(spec.x_slurm, "data_parsers")); |
| openapi_plugins = data_set_list(data_key_set(spec.x_slurm, "openapi")); |
| slurm_version = data_set_dict(data_key_set(spec.x_slurm, "version")); |
| |
| data_set_string(data_key_set(spec.spec, "openapi"), |
| openapi_spec.openapi_version); |
| data_set_string(data_key_set(spec.info, "title"), openapi_spec.info.title); |
| data_set_string(data_key_set(spec.info, "description"), |
| openapi_spec.info.desc); |
| data_set_string(data_key_set(spec.info, "termsOfService"), |
| openapi_spec.info.tos); |
| data_set_string_own(data_key_set(spec.info, "version"), version); |
| |
| /* Populate info spec extension x-slurm with data_parsers and flags */ |
| for (int i = 0; i < parsers_count; i++) { |
| data_t *tmp_data = data_list_append(data_parsers); |
| |
| data_set_dict(tmp_data); |
| data_set_string(data_key_set(tmp_data, "plugin"), |
| data_parser_get_plugin_version(parsers[i])); |
| if (data_parser_g_dump_flags(parsers[i], |
| data_key_set(tmp_data, "flags"))) |
| fatal_abort("data_parser_g_dump_flags() failed"); |
| } |
| |
| /* Populate info spec extension x-slurm with openapi plugins */ |
| for (int i = 0; i < plugins->count; i++) |
| data_set_string(data_list_append(openapi_plugins), |
| plugins->types[i]); |
| |
| data_set_string(data_key_set(slurm_version, "major"), SLURM_MAJOR); |
| data_set_string(data_key_set(slurm_version, "micro"), SLURM_MICRO); |
| data_set_string(data_key_set(slurm_version, "minor"), SLURM_MINOR); |
| data_set_string(data_key_set(spec.x_slurm, "release"), |
| SLURM_VERSION_STRING); |
| data_set_string(data_key_set(spec.contact, "name"), |
| openapi_spec.info.contact.name); |
| data_set_string(data_key_set(spec.contact, "url"), |
| openapi_spec.info.contact.url); |
| data_set_string(data_key_set(spec.contact, "email"), |
| openapi_spec.info.contact.email); |
| data_set_string(data_key_set(spec.license, "name"), |
| openapi_spec.info.license.name); |
| data_set_string(data_key_set(spec.license, "url"), |
| openapi_spec.info.license.url); |
| |
| for (int i = 0; i < ARRAY_SIZE(openapi_spec.tags); i++) { |
| data_t *tag = data_set_dict(data_list_append(spec.tags)); |
| data_set_string(data_key_set(tag, "name"), |
| openapi_spec.tags[i].name); |
| data_set_string(data_key_set(tag, "description"), |
| openapi_spec.tags[i].desc); |
| } |
| for (int i = 0; i < ARRAY_SIZE(openapi_spec.servers); i++) { |
| data_t *server = data_set_dict(data_list_append(spec.servers)); |
| data_set_string(data_key_set(server, "url"), |
| openapi_spec.servers[i].url); |
| } |
| |
| /* Add default of no auth required */ |
| data_set_dict(data_list_append(spec.security)); |
| /* Add user and token auth */ |
| data_set_list(data_key_set( |
| security1, |
| openapi_spec.components.security_schemes[SEC_SCHEME_USER_INDEX] |
| .key)); |
| data_set_list(data_key_set( |
| security1, |
| openapi_spec.components.security_schemes[SEC_SCHEME_TOKEN_INDEX] |
| .key)); |
| /* Add only token auth */ |
| data_set_list(data_key_set( |
| security2, |
| openapi_spec.components.security_schemes[SEC_SCHEME_TOKEN_INDEX] |
| .key)); |
| /* Add only bearer */ |
| data_set_list(data_key_set( |
| security3, openapi_spec.components |
| .security_schemes[SEC_SCHEME_BEARER_INDEX] |
| .key)); |
| |
| for (int i = 0; |
| i < ARRAY_SIZE(openapi_spec.components.security_schemes); i++) { |
| const struct security_scheme_s *s = |
| &openapi_spec.components.security_schemes[i]; |
| data_t *schema = |
| data_set_dict(data_key_set(spec.security_schemas, |
| s->key)); |
| data_set_string(data_key_set(schema, "type"), s->type); |
| data_set_string(data_key_set(schema, "description"), s->desc); |
| |
| if (s->name) |
| data_set_string(data_key_set(schema, "name"), s->name); |
| if (s->in) |
| data_set_string(data_key_set(schema, "in"), s->in); |
| if (s->scheme) |
| data_set_string(data_key_set(schema, "scheme"), |
| s->scheme); |
| if (s->bearer_format) |
| data_set_string(data_key_set(schema, "bearerFormat"), |
| s->bearer_format); |
| } |
| |
| (void) list_for_each(paths, _foreach_count_path, &spec); |
| |
| /* Add generated paths */ |
| (void) list_for_each(paths, _foreach_add_path, &spec); |
| |
| for (int i = 0; parsers[i]; i++) |
| if (spec.references[i]) |
| data_parser_g_release_references(parsers[i], |
| &spec.references[i]); |
| xfree(spec.references); |
| |
| /* |
| * We currently fatal instead of returning failure since openapi are |
| * compile time static and we should not be failing to serve the specs |
| * out |
| */ |
| return SLURM_SUCCESS; |
| } |
| |
| static int _op_handler_openapi(openapi_ctxt_t *ctxt) |
| { |
| return generate_spec(ctxt->resp, get_mime_type_array()); |
| } |
| |
| static bool _on_error(void *arg, data_parser_type_t type, int error_code, |
| const char *source, const char *why, ...) |
| { |
| va_list ap; |
| char *str; |
| openapi_ctxt_t *ctxt = arg; |
| |
| va_start(ap, why); |
| str = vxstrfmt(why, ap); |
| va_end(ap); |
| |
| openapi_resp_error(ctxt, error_code, source, "%s", str); |
| |
| xfree(str); |
| |
| return false; |
| } |
| |
| static void _on_warn(void *arg, data_parser_type_t type, const char *source, |
| const char *why, ...) |
| { |
| va_list ap; |
| char *str; |
| openapi_ctxt_t *ctxt = arg; |
| |
| va_start(ap, why); |
| str = vxstrfmt(why, ap); |
| va_end(ap); |
| |
| openapi_resp_warn(ctxt, source, "%s", str); |
| |
| xfree(str); |
| } |
| |
| extern int openapi_resp_error(openapi_ctxt_t *ctxt, int error_code, |
| const char *source, const char *why, ...) |
| { |
| openapi_resp_error_t *e; |
| |
| xassert(ctxt->errors); |
| |
| if (!ctxt->errors) |
| return error_code; |
| |
| e = xmalloc(sizeof(*e)); |
| |
| if (why) { |
| va_list ap; |
| char *str; |
| |
| va_start(ap, why); |
| str = vxstrfmt(why, ap); |
| va_end(ap); |
| |
| error("%s: [%s] parser=%s rc[%d]=%s -> %s", |
| (source ? source : __func__), ctxt->id, |
| data_parser_get_plugin(ctxt->parser), error_code, |
| slurm_strerror(error_code), str); |
| |
| e->description = str; |
| } |
| |
| if (error_code) { |
| e->num = error_code; |
| |
| if (!ctxt->rc) |
| ctxt->rc = error_code; |
| } |
| |
| if (source) |
| e->source = xstrdup(source); |
| |
| list_append(ctxt->errors, e); |
| |
| return error_code; |
| } |
| |
| extern void openapi_resp_warn(openapi_ctxt_t *ctxt, const char *source, |
| const char *why, ...) |
| { |
| openapi_resp_warning_t *w; |
| |
| xassert(ctxt->warnings); |
| |
| if (!ctxt->warnings) |
| return; |
| |
| w = xmalloc(sizeof(*w)); |
| |
| if (why) { |
| va_list ap; |
| char *str; |
| |
| va_start(ap, why); |
| str = vxstrfmt(why, ap); |
| va_end(ap); |
| |
| debug("%s: [%s] parser=%s WARNING: %s", |
| (source ? source : __func__), ctxt->id, |
| data_parser_get_plugin(ctxt->parser), str); |
| |
| w->description = str; |
| } |
| |
| if (source) |
| w->source = xstrdup(source); |
| |
| list_append(ctxt->warnings, w); |
| } |
| |
| static void _populate_openapi_results(openapi_ctxt_t *ctxt, |
| openapi_resp_meta_t *query_meta) |
| { |
| data_t *errors, *warnings, *meta; |
| int rc; |
| |
| /* need to populate meta, errors and warnings */ |
| errors = data_key_set(ctxt->resp, |
| XSTRINGIFY(OPENAPI_RESP_STRUCT_ERRORS_FIELD_NAME)); |
| warnings = data_key_set(ctxt->resp, |
| XSTRINGIFY(OPENAPI_RESP_STRUCT_WARNINGS_FIELD_NAME)); |
| meta = data_key_set(ctxt->resp, |
| XSTRINGIFY(OPENAPI_RESP_STRUCT_META_FIELD_NAME)); |
| |
| if (data_get_type(meta) == DATA_TYPE_NULL) |
| DATA_DUMP(ctxt->parser, OPENAPI_META_PTR, query_meta, meta); |
| |
| if (data_get_type(errors) == DATA_TYPE_LIST) { |
| if (!data_get_list_length(errors)) |
| data_set_null(errors); |
| else |
| xassert(list_is_empty(ctxt->errors)); |
| } |
| |
| if ((data_get_type(errors) == DATA_TYPE_NULL) && |
| ((rc = DATA_DUMP(ctxt->parser, OPENAPI_ERRORS, ctxt->errors, |
| errors)))) { |
| /* data_parser doesn't support OPENAPI_ERRORS parser */ |
| data_t *e = |
| data_set_dict(data_list_append(data_set_list(errors))); |
| data_set_string(data_key_set(e, "description"), |
| "Requested data_parser plugin does not support OpenAPI plugin"); |
| data_set_int(data_key_set(e, "error_number"), |
| ESLURM_NOT_SUPPORTED); |
| data_set_string(data_key_set(e, "error"), |
| slurm_strerror(ESLURM_NOT_SUPPORTED)); |
| } |
| |
| if (data_get_type(warnings) == DATA_TYPE_LIST) { |
| if (!data_get_list_length(warnings)) |
| data_set_null(warnings); |
| else |
| xassert(list_is_empty(ctxt->warnings)); |
| } |
| |
| if (data_get_type(warnings) == DATA_TYPE_NULL) |
| DATA_DUMP(ctxt->parser, OPENAPI_WARNINGS, ctxt->warnings, |
| warnings); |
| } |
| |
| extern int wrap_openapi_ctxt_callback(const char *context_id, |
| http_request_method_t method, |
| data_t *parameters, data_t *query, |
| int tag, data_t *resp, void *auth, |
| data_parser_t *parser, |
| const openapi_path_binding_t *op_path, |
| const openapi_resp_meta_t *plugin_meta) |
| { |
| int rc = SLURM_SUCCESS; |
| openapi_ctxt_t ctxt = { |
| .id = context_id, |
| .method = method, |
| .parameters = parameters, |
| .query = query, |
| .resp = resp, |
| .tag = tag, |
| }; |
| openapi_resp_meta_t query_meta = {{0}}; |
| openapi_ctxt_handler_t callback = op_path->callback; |
| |
| if (plugin_meta) |
| query_meta = *plugin_meta; |
| |
| query_meta.plugin.data_parser = (char *) data_parser_get_plugin(parser); |
| query_meta.plugin.accounting_storage = |
| (char *) slurm_conf.accounting_storage_type; |
| query_meta.client.source = (char *) context_id; |
| query_meta.slurm.cluster = slurm_conf.cluster_name; |
| |
| ctxt.parent_path = data_set_list(data_new()); |
| ctxt.errors = list_create(free_openapi_resp_error); |
| ctxt.warnings = list_create(free_openapi_resp_warning); |
| ctxt.parser = data_parser_g_new(_on_error, _on_error, _on_error, &ctxt, |
| _on_warn, _on_warn, _on_warn, &ctxt, |
| data_parser_get_plugin(parser), NULL, |
| true); |
| |
| debug("%s: [%s] %s using %s", |
| __func__, context_id, get_http_method_string(method), |
| data_parser_get_plugin(ctxt.parser)); |
| |
| if (op_path->flags & OP_BIND_NO_SLURMDBD) { |
| ; /* Do not attempt to open a connection to slurmdbd */ |
| } else if (slurm_conf.accounting_storage_type && |
| !(ctxt.db_conn = openapi_get_db_conn(auth))) { |
| char *auth_error_msg = ""; |
| if (errno == SLURM_PROTOCOL_AUTHENTICATION_ERROR) |
| auth_error_msg = ", authentication error"; |
| if (op_path->flags & OP_BIND_REQUIRE_SLURMDBD) |
| openapi_resp_error(&ctxt, (rc = ESLURM_DB_CONNECTION), |
| XSTRINGIFY(openapi_get_db_conn), |
| "Failed to open slurmdbd connection%s", |
| auth_error_msg); |
| else |
| openapi_resp_warn(&ctxt, |
| XSTRINGIFY(openapi_get_db_conn), |
| "Failed to open connection to slurmdbd%s. Response fields may not be fully populated or empty.", |
| auth_error_msg); |
| } else { |
| rc = data_parser_g_assign(ctxt.parser, |
| DATA_PARSER_ATTR_DBCONN_PTR, |
| ctxt.db_conn); |
| } |
| |
| if (!rc) |
| rc = callback(&ctxt); |
| |
| if (data_get_type(ctxt.resp) == DATA_TYPE_NULL) |
| data_set_dict(ctxt.resp); |
| |
| if (op_path->flags & OP_BIND_OPENAPI_RESP_FMT) |
| _populate_openapi_results(&ctxt, &query_meta); |
| |
| if (!rc) |
| rc = ctxt.rc; |
| |
| FREE_NULL_LIST(ctxt.errors); |
| FREE_NULL_LIST(ctxt.warnings); |
| FREE_NULL_DATA_PARSER(ctxt.parser); |
| FREE_NULL_DATA(ctxt.parent_path); |
| |
| return rc; |
| } |
| |
| extern bool is_spec_generation_only(bool set) |
| { |
| static bool is_spec_only = false; |
| |
| if (set) |
| is_spec_only = true; |
| |
| return is_spec_only; |
| } |