blob: 03dba1fbba82e925aef6637f6cb3d4d8759504e7 [file]
/*****************************************************************************\
* 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 "slurm/slurm_errno.h"
#include "src/common/data.h"
#include "src/common/http.h"
#include "src/common/http_router.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/http.h"
#include "src/slurmrestd/openapi.h"
#include "src/slurmrestd/operations.h"
#define OPENAPI_MAJOR_TYPE "openapi"
#define MAX_URL_PATH_DEPTH 1024
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 openapi_entry_s {
char *entry;
entry_type_t type;
openapi_type_t parameter;
} entry_t;
#define RESOLVE_PARAMS_ARGS_MAGIC 0x0b1a2e3f
typedef struct {
int magic; /* RESOLVE_PARAMS_ARGS_MAGIC */
const entry_t *entry;
data_t *params;
} resolve_params_args_t;
typedef struct {
const openapi_path_binding_method_t *bound;
entry_t *entries;
bool has_params;
http_request_method_t method;
} entry_method_t;
#define MAGIC_PATH 0x0a0b09fd
typedef struct openapi_path_s {
int magic; /* MAGIC_PATH */
char *path; /* path as string */
const openapi_path_binding_t *bound;
data_parser_t *parser;
entry_method_t *methods;
} path_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;
#define URL_PARSE_ARGS_MAGIC 0x0abfccaa
typedef struct {
int magic; /* URL_PARSE_ARGS_MAGIC */
int count;
entry_t *entry;
} url_parse_args_t;
#define URL_PARSE_OPERATION_IDARGS_MAGIC 0xaaffec0e
typedef struct {
int magic; /* URL_PARSE_OPERATION_IDARGS_MAGIC */
data_t *dpath;
data_t *last;
} url_parse_operation_id_args_t;
static list_t *paths = NULL;
static plugins_t *plugins = NULL;
static data_parser_t **parsers = NULL; /* symlink to parser array */
#define MAGIC_BIND_METHOD_PATH 0x10aa0bf0
typedef struct {
int magic; /* MAGIC_BIND_METHOD_PATH */
entry_t *entry;
const openapi_resp_meta_t *meta;
const openapi_path_binding_method_t *op_method;
const openapi_path_binding_t *op_path;
data_parser_t *parser;
} bind_method_path_t;
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[4];
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 = "util",
.desc = "utilities available directly through slurmrestd",
},
{
.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 (OPENAPI_BIND_HIDDEN_OAS | OPENAPI_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_CODE_INVALID,
};
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, entry_method_t *method)
{
entry_t *itr = entry;
if (!entry)
return;
while (itr->type) {
debug5("%s: remove path method:%s entry:%s",
__func__, (method ?
get_http_method_string(method->method) :
"N/A"), itr->entry);
xfree(itr->entry);
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);
em = path->methods;
while (em->entries) {
debug5("%s: remove path method:%s",
__func__, get_http_method_string(em->method));
_free_entry_list(em->entries, em);
em->entries = NULL;
em++;
}
xfree(path->methods);
xfree(path->path);
path->magic = ~MAGIC_PATH;
xfree(path);
}
static int _on_parse_url_entry(const char *token, bool template, void *arg)
{
url_parse_args_t *args = arg;
entry_t *entry = args->entry;
xassert(args->magic == URL_PARSE_ARGS_MAGIC);
if (!args->entry) {
args->count++;
return SLURM_SUCCESS;
}
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);
return ESLURM_URL_INVALID_PATH;
}
if (template) {
entry->type = OPENAPI_PATH_ENTRY_MATCH_PARAMETER;
debug5("%s: parameter %s at entry", __func__, token);
} else { /* not a variable */
entry->type = OPENAPI_PATH_ENTRY_MATCH_STRING;
debug5("%s: string match entry %s", __func__, token);
}
args->entry++;
return SLURM_SUCCESS;
}
static entry_t *_parse_openapi_path(const char *str_path, int *count_ptr)
{
url_parse_args_t args = {
.magic = URL_PARSE_ARGS_MAGIC,
};
entry_t *entries = NULL;
if (url_path_walk(str_path, true, _on_parse_url_entry, &args))
fatal_abort("Unable to parse path format: %s", str_path);
xassert(args.count > 0);
xassert(args.count < MAX_URL_PATH_DEPTH);
args.entry = entries = xcalloc((args.count + 1), sizeof(*entries));
if (url_path_walk(str_path, true, _on_parse_url_entry, &args))
fatal_abort("Unable to parse path: %s", str_path);
xassert(args.entry <= (entries + args.count));
if (count_ptr)
*count_ptr = args.count;
return entries;
}
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->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 == OPENAPI_BIND_NONE) ||
((op_path->flags > OPENAPI_BIND_NONE) &&
(op_path->flags < OPENAPI_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_INVALID_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;
}
static path_t *_register_path(const char *path,
const openapi_path_binding_t *op_path,
const openapi_resp_meta_t *meta,
data_parser_t *parser)
{
entry_t *entries = NULL;
int methods_count = 0, entries_count = 0;
path_t *p = NULL;
debug4("%s: attempting to bind %s with %s",
__func__, (parser ? data_parser_get_plugin_version(parser) :
"data_parser/none"), path);
_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, NULL);
return NULL;
}
p = xmalloc(sizeof(*p));
p->magic = MAGIC_PATH;
p->methods = xcalloc((methods_count + 1), sizeof(*p->methods));
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->entry);
t->has_params = true;
}
debug5("%s: add binded path %s entry: method=%s entry=%s parameter=%s entry_type=%s",
__func__, path,
get_http_method_string(m->method), e->entry,
openapi_type_to_string(e->parameter),
_get_entry_type_string(e->type));
}
/* only move to next method if populated */
mi++;
}
list_append(paths, p);
return p;
}
static int _on_request(http_con_t *hcon, const char *name,
const http_con_request_t *request, void *arg,
void *path_arg)
{
bind_method_path_t *bmp = path_arg;
http_context_t *ctxt = arg;
xassert(bmp->magic == MAGIC_BIND_METHOD_PATH);
return on_request(hcon, name, ctxt, request, bmp->op_path,
bmp->op_method, bmp->meta, bmp->parser, bmp->entry);
}
static void _on_fini(http_router_on_request_event_t on_request, void *path_arg)
{
bind_method_path_t *bmp = path_arg;
xassert(bmp->magic == MAGIC_BIND_METHOD_PATH);
xassert(on_request == _on_request);
bmp->magic = ~MAGIC_BIND_METHOD_PATH;
xfree(bmp);
}
static void _bind_path(const char *url_path,
const openapi_path_binding_t *op_path,
const openapi_path_binding_method_t *op_method,
const openapi_resp_meta_t *meta, data_parser_t *parser,
entry_t *entry)
{
bind_method_path_t *bmp = xmalloc(sizeof(*bmp));
*bmp = (bind_method_path_t) {
.magic = MAGIC_BIND_METHOD_PATH,
.entry = entry,
.meta = meta,
.op_method = op_method,
.op_path = op_path,
.parser = parser,
};
xassert(bmp->parser);
xassert(bmp->op_path);
xassert(bmp->op_method);
http_router_bind(op_method->method, url_path, _on_request, _on_fini,
bmp);
}
static void _bind_op_path_parser(const openapi_path_binding_t *op_path,
const openapi_resp_meta_t *meta,
data_parser_t *parser)
{
path_t *path = NULL;
char *url_path = NULL;
const char *url = NULL;
if (op_path->flags & OPENAPI_BIND_DATA_PARSER) {
url_path = xstrdup(op_path->path);
xassert(xstrstr(url_path, OPENAPI_DATA_PARSER_PARAM));
xstrsubstitute(url_path, OPENAPI_DATA_PARSER_PARAM,
data_parser_get_plugin_version(parser));
url = url_path;
} else {
url = op_path->path;
}
if ((path = _register_path(url, op_path, meta, parser))) {
for (int i = 0; path->methods[i].bound; i++) {
_bind_path(url, op_path, path->methods[i].bound, meta,
parser,
(path->methods[i].has_params ?
path->methods[i].entries :
NULL));
}
}
xfree(url_path);
}
static void _bind_op_path(const openapi_path_binding_t *op_path,
const openapi_resp_meta_t *meta,
data_parser_t *default_parser)
{
if (op_path->flags & OPENAPI_BIND_DATA_PARSER)
for (int i = 0; parsers[i]; i++)
_bind_op_path_parser(op_path, meta, parsers[i]);
else
_bind_op_path_parser(op_path, meta, default_parser);
}
static void _bind_paths(const openapi_path_binding_t *paths,
const openapi_resp_meta_t *meta)
{
data_parser_t *default_parser = NULL;
if (!parsers || !parsers[0])
fatal("No data_parsers plugins loaded. Refusing to load.");
for (int i = 0; parsers[i]; i++) {
if (!xstrcmp(data_parser_get_plugin(parsers[i]),
SLURM_DATA_PARSER_VERSION)) {
default_parser = parsers[i];
break;
}
}
/*
* The current version is not always loaded, such as with
* slurmrestd -d v0.0.43, so fall back to the first parser given.
*/
if (!default_parser)
default_parser = parsers[0];
for (const openapi_path_binding_t *op_path = &paths[0]; op_path->path;
op_path++)
_bind_op_path(op_path, meta, default_parser);
}
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);
parsers = parsers_ptr;
/*
* Only the version is substituted into a {data_parser} URL, so two
* plugins of the same version resolve to one path and the second bind
* would collide inside the router. Reject it here where the offending
* plugins can be named.
*/
for (int i = 0; parsers && parsers[i]; i++) {
const char *version =
data_parser_get_plugin_version(parsers[i]);
for (int j = (i + 1); parsers[j]; j++) {
if (xstrcmp(version,
data_parser_get_plugin_version(parsers[j])))
continue;
fatal("data_parser plugins %s and %s both resolve to URL version %s. Only one data_parser per version may be loaded.",
data_parser_get_plugin(parsers[i]),
data_parser_get_plugin(parsers[j]), version);
}
}
/* must have JSON plugin to parse the openapi.json */
serializer_required(MIME_TYPE_JSON);
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.");
_bind_paths(openapi_paths, NULL);
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));
_bind_paths(paths, meta);
}
/* 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 int _add_operation_path(const char *entry, bool template, void *arg)
{
url_parse_operation_id_args_t *args = arg;
data_t *dpath = args->dpath;
data_t *last = args->last;
data_t *c = NULL;
xassert(args->magic == URL_PARSE_OPERATION_IDARGS_MAGIC);
if (!xstrcasecmp(entry, ".") || !xstrcasecmp(entry, ".."))
fatal_abort("OpenAPI path must not include . or ..");
if (template && last && !xstrcmp(data_get_string(last), entry)) {
/*
* Last entry is the same as the parameter name which means that
* the entry is uncountable and we need to set last entry as
* "single" to avoid ambiguous operationIds
*/
data_set_string(data_list_append(dpath), entry);
c = data_set_string(last, "single");
} else if (template) {
/* skip including template names in operationId */
} else {
char *e = xstrdup(entry);
/* Strip out all '.' in entry */
xstrsubstituteall(e, ".", "");
c = data_set_string_own(data_list_append(dpath), e);
}
args->last = c;
return SLURM_SUCCESS;
}
/* 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 = NULL, *merged = 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,
};
url_parse_operation_id_args_t url_args = {
.magic = URL_PARSE_OPERATION_IDARGS_MAGIC,
};
url_args.dpath = dpath = data_set_list(data_new());
(void) url_path_walk(path->path, true, _add_operation_path, &url_args);
xassert((data_get_list_length(dpath) + 1) < (ARRAY_SIZE(merge) - 1));
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 & OPENAPI_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 *db_conn,
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 & OPENAPI_BIND_NO_SLURMDBD) {
; /* Do not attempt to open a connection to slurmdbd */
} else if (!(ctxt.db_conn = db_conn)) {
if (op_path->flags & OPENAPI_BIND_REQUIRE_SLURMDBD)
openapi_resp_error(
&ctxt, (rc = ESLURM_DB_CONNECTION),
XSTRINGIFY(openapi_get_db_conn),
"Failed to open slurmdbd connection");
else
openapi_resp_warn(
&ctxt,
XSTRINGIFY(openapi_get_db_conn),
"Failed to open connection to slurmdbd. Response fields may not be fully populated or empty.");
} 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 & OPENAPI_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;
}
/*
* Populate one OAS parameter from the matching request URL entry
* IN entry - entry from the request URL path
* IN template - always false: request URLs never carry {templates}
* IN arg - resolve_params_args_t
* RET SLURM_SUCCESS or ESLURM_URL_INVALID_PATH
*
* The HTTP router already matched this path to pick the binding, so there is
* nothing here to match. Literal entries are walked past and only a parameter
* entry creates a data_t, which is why the request URL is never converted as a
* whole.
*/
static int _on_path_entry(const char *entry, bool template, void *arg)
{
resolve_params_args_t *args = arg;
const entry_t *e = args->entry;
data_t *param = NULL;
data_type_t type = DATA_TYPE_NONE;
xassert(args->magic == RESOLVE_PARAMS_ARGS_MAGIC);
xassert(!template);
if (!e->type) {
debug5("%s: request has more entries than %s was registered with",
__func__, entry);
return ESLURM_URL_INVALID_PATH;
}
args->entry++;
if (e->type == OPENAPI_PATH_ENTRY_MATCH_STRING) {
/* router matched every literal to get here */
xassert(!xstrcmp(entry, e->entry));
return SLURM_SUCCESS;
}
xassert(e->type == OPENAPI_PATH_ENTRY_MATCH_PARAMETER);
param = data_key_set(args->params, e->entry);
data_set_string(param, entry);
type = openapi_type_to_data_type(e->parameter);
/*
* A URL path segment is always a scalar string. Force every type that
* can not be converted from one to a string, including types that
* never resolved, instead of rejecting the request.
*/
if ((type == DATA_TYPE_NONE) || (type == DATA_TYPE_DICT) ||
(type == DATA_TYPE_LIST)) {
debug5("%s: parameter %s[%s] forced to string",
__func__, e->entry,
openapi_type_to_string(e->parameter));
type = DATA_TYPE_STRING;
}
if (data_convert_type(param, type) != type) {
debug5("%s: parameter %s[%s] rejected value %s",
__func__, e->entry,
openapi_type_to_string(e->parameter), entry);
return ESLURM_URL_INVALID_PATH;
}
debug5("%s: parameter %s[%s] = %s",
__func__, e->entry, openapi_type_to_string(e->parameter), entry);
return SLURM_SUCCESS;
}
extern int resolve_params(const openapi_entry_t *entry, const char *url_path,
data_t *params)
{
int rc = EINVAL;
resolve_params_args_t args = {
.magic = RESOLVE_PARAMS_ARGS_MAGIC,
.entry = entry,
.params = params,
};
xassert(entry);
xassert(data_get_type(params) == DATA_TYPE_DICT);
if ((rc = url_path_walk(url_path, false, _on_path_entry, &args)))
return rc;
if (args.entry->type) {
debug5("%s: %s is shorter than the registered path",
__func__, url_path);
return ESLURM_URL_INVALID_PATH;
}
return SLURM_SUCCESS;
}