diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/conda.c | 2 | ||||
-rw-r--r-- | src/delivery.c | 22 | ||||
-rw-r--r-- | src/github.c | 133 | ||||
-rw-r--r-- | src/globals.c | 16 | ||||
-rw-r--r-- | src/ini.c | 2 | ||||
-rw-r--r-- | src/junitxml.c | 6 | ||||
-rw-r--r-- | src/stasis_main.c | 49 | ||||
-rw-r--r-- | src/template.c | 31 | ||||
-rw-r--r-- | src/template_func_proto.c | 65 |
10 files changed, 307 insertions, 21 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c42bb0f..a7b06f7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(stasis_core STATIC rules.c docker.c junitxml.c + github.c + template_func_proto.c ) add_executable(stasis diff --git a/src/conda.c b/src/conda.c index 1e8b03f..976bbbc 100644 --- a/src/conda.c +++ b/src/conda.c @@ -32,7 +32,7 @@ int micromamba(struct MicromambaInfo *info, char *command, ...) { sprintf(mmbin, "%s/micromamba", info->micromamba_prefix); if (access(mmbin, F_OK)) { - char untarcmd[PATH_MAX]; + char untarcmd[PATH_MAX * 2]; mkdirs(info->micromamba_prefix, 0755); sprintf(untarcmd, "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", installer_path, info->micromamba_prefix); system(untarcmd); diff --git a/src/delivery.c b/src/delivery.c index 278647c..7bedef3 100644 --- a/src/delivery.c +++ b/src/delivery.c @@ -1419,7 +1419,7 @@ void delivery_install_conda(char *install_script, char *conda_install_dir) { // Proceed with the installation // -b = batch mode (non-interactive) - char cmd[255] = {0}; + char cmd[PATH_MAX] = {0}; snprintf(cmd, sizeof(cmd) - 1, "%s %s -b -p %s", find_program("bash"), install_script, @@ -1476,7 +1476,7 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { struct StrList *filtered = NULL; filtered = strlist_init(); - for (size_t i = 0, z = 0; i < strlist_count(dataptr); i++) { + for (size_t i = 0; i < strlist_count(dataptr); i++) { int ignore_pkg = 0; name = strlist_item(dataptr, i); @@ -1494,7 +1494,6 @@ void delivery_defer_packages(struct Delivery *ctx, int type) { if (strstr(name, ctx->tests[x].name)) { version = ctx->tests[x].version; ignore_pkg = 1; - z++; break; } } @@ -2164,4 +2163,21 @@ int delivery_fixup_test_results(struct Delivery *ctx) { closedir(dp); return 0; +} + +int delivery_exists(struct Delivery *ctx) { + // TODO: scan artifactory repo for the same information + char release_pattern[PATH_MAX] = {0}; + sprintf(release_pattern, "*%s*", ctx->info.release_name); + struct StrList *files = listdir(ctx->storage.delivery_dir); + for (size_t i = 0; i < strlist_count(files); i++) { + char *filename = strlist_item(files, i); + int release_exists = fnmatch(release_pattern, filename, FNM_PATHNAME); + if (!globals.enable_overwrite && !release_exists) { + guard_strlist_free(&files); + return 1; + } + } + guard_strlist_free(&files); + return 0; }
\ No newline at end of file diff --git a/src/github.c b/src/github.c new file mode 100644 index 0000000..36e2e7c --- /dev/null +++ b/src/github.c @@ -0,0 +1,133 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "core.h" + +struct GHContent { + char *data; + size_t len; +}; + +static size_t writer(void *contents, size_t size, size_t nmemb, void *result) { + const size_t newlen = size * nmemb; + struct GHContent *content = (struct GHContent *) result; + + char *ptr = realloc(content->data, content->len + newlen + 1); + if (!ptr) { + perror("realloc failed"); + return 0; + } + + content->data = ptr; + memcpy(&(content->data[content->len]), contents, newlen); + content->len += newlen; + content->data[content->len] = 0; + + return newlen; +} + +static char *unescape_lf(char *value) { + char *seq = strstr(value, "\\n"); + while (seq != NULL) { + size_t cur_len = strlen(seq); + memmove(seq, seq + 1, strlen(seq) - 1); + *seq = '\n'; + if (strlen(seq) && cur_len) { + seq[cur_len - 1] = 0; + } + seq = strstr(value, "\\n"); + } + return value; +} + +int get_github_release_notes(const char *api_token, const char *repo, const char *tag, const char *target_commitish, char **output) { + const char *field_body = "\"body\":\""; + const char *field_message = "\"message\":\""; + const char *endpoint_header_auth_fmt = "Authorization: Bearer %s"; + const char *endpoint_header_api_version = "X-GitHub-Api-Version: " STASIS_GITHUB_API_VERSION; + const char *endpoint_post_fields_fmt = "{\"tag_name\":\"%s\", \"target_commitish\":\"%s\"}"; + const char *endpoint_url_fmt = "https://api.github.com/repos/%s/releases/generate-notes"; + char endpoint_header_auth[PATH_MAX] = {0}; + char endpoint_post_fields[PATH_MAX] = {0}; + char endpoint_url[PATH_MAX] = {0}; + struct curl_slist *list = NULL; + struct GHContent content; + + CURL *curl = curl_easy_init(); + if (!curl) { + return -1; + } + + // Render the header data + sprintf(endpoint_header_auth, endpoint_header_auth_fmt, api_token); + sprintf(endpoint_post_fields, endpoint_post_fields_fmt, tag, target_commitish); + sprintf(endpoint_url, endpoint_url_fmt, repo); + + // Begin curl configuration + curl_easy_setopt(curl, CURLOPT_URL, endpoint_url); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, endpoint_post_fields); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &content); + + // Append headers to the request + list = curl_slist_append(list, "Accept: application/vnd.github+json"); + list = curl_slist_append(list, endpoint_header_auth); + list = curl_slist_append(list, endpoint_header_api_version); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + + // Set the user-agent (github requires one) + char user_agent[20] = {0}; + sprintf(user_agent, "stasis/%s", VERSION); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent); + + // Execute curl request + memset(&content, 0, sizeof(content)); + CURLcode res; + res = curl_easy_perform(curl); + + // Clean up + curl_slist_free_all(list); + curl_easy_cleanup(curl); + + if(res != CURLE_OK) { + fprintf(stderr, "curl_easy_perform() failed: %s\n", + curl_easy_strerror(res)); + return -1; + } + + // Replace all "\\n" literals with new line characters + char *line = unescape_lf(content.data); + if (line) { + char *data_offset = NULL; + if ((data_offset = strstr(line, field_body))) { + // Skip past the body field + data_offset += strlen(field_body); + // Remove quotation mark (and trailing comma if it exists) + int trim = 2; + char last_char = data_offset[strlen(data_offset) - trim]; + if (last_char == ',') { + trim++; + } + data_offset[strlen(data_offset) - trim] = 0; + // Extract release notes + *output = strdup(data_offset); + } else if ((data_offset = strstr(line, field_message))) { + // Skip past the message field + data_offset += strlen(field_message); + *(strchr(data_offset, '"')) = 0; + fprintf(stderr, "GitHub API Error: '%s'\n", data_offset); + fprintf(stderr, "URL: %s\n", endpoint_url); + fprintf(stderr, "POST: %s\n", endpoint_post_fields); + guard_free(content.data); + return -1; + } + } else { + fprintf(stderr, "Unknown error\n"); + guard_free(content.data); + return -1; + } + + guard_free(content.data); + return 0; +}
\ No newline at end of file diff --git a/src/globals.c b/src/globals.c index 297598f..18a32b5 100644 --- a/src/globals.c +++ b/src/globals.c @@ -36,6 +36,22 @@ struct STASIS_GLOBAL globals = { .enable_docker = true, .enable_artifactory = true, .enable_testing = true, + .envctl = { + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"TMPDIR", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_ROOT", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_SYSCONFDIR", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_CPU_COUNT", "CPU_COUNT", NULL}}, + {.flags = STASIS_ENVCTL_REQUIRED | STASIS_ENVCTL_REDACT, .name={"STASIS_GH_TOKEN", "GITHUB_TOKEN", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_ACCESS_TOKEN", NULL}}, + {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_JF_USER", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_PASSWORD", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_KEY_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_PASSPHRASE", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_CERT_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_KEY_PATH", NULL}}, + {.flags = STASIS_ENVCTL_REQUIRED, .name = {"STASIS_JF_REPO", NULL}}, + {.flags = 0, .name = {NULL}}, + } }; void globals_free() { @@ -153,7 +153,7 @@ int ini_getval(struct INIFILE *ini, char *section_name, char *key, int type, uni case INIVAL_TYPE_STR_ARRAY: strcpy(tbufp, data->value); *data->value = '\0'; - for (size_t i = 0; (token = strsep(&tbufp, "\n")) != NULL; i++) { + while ((token = strsep(&tbufp, "\n")) != NULL) { lstrip(token); strcat(data->value, token); strcat(data->value, "\n"); diff --git a/src/junitxml.c b/src/junitxml.c index c1bf1be..9c7e5b4 100644 --- a/src/junitxml.c +++ b/src/junitxml.c @@ -146,16 +146,16 @@ static struct StrList *attributes_to_strlist(xmlTextReaderPtr reader) { static int read_xml_data(xmlTextReaderPtr reader, struct JUNIT_Testsuite **testsuite) { const xmlChar *name; - const xmlChar *value; + //const xmlChar *value; name = xmlTextReaderConstName(reader); if (!name) { // name could not be converted to string name = BAD_CAST "--"; } - value = xmlTextReaderConstValue(reader); + //value = xmlTextReaderConstValue(reader); const char *node_name = (char *) name; - const char *node_value = (char *) value; + //const char *node_value = (char *) value; struct StrList *attrs = attributes_to_strlist(reader); if (attrs && strlist_count(attrs)) { diff --git a/src/stasis_main.c b/src/stasis_main.c index c550982..ce49829 100644 --- a/src/stasis_main.c +++ b/src/stasis_main.c @@ -9,6 +9,7 @@ #define OPT_NO_DOCKER 1001 #define OPT_NO_ARTIFACTORY 1002 #define OPT_NO_TESTING 1003 +#define OPT_OVERWRITE 1004 static struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'V'}, @@ -18,6 +19,7 @@ static struct option long_options[] = { {"verbose", no_argument, 0, 'v'}, {"unbuffered", no_argument, 0, 'U'}, {"update-base", no_argument, 0, OPT_ALWAYS_UPDATE_BASE}, + {"overwrite", no_argument, 0, OPT_OVERWRITE}, {"no-docker", no_argument, 0, OPT_NO_DOCKER}, {"no-artifactory", no_argument, 0, OPT_NO_ARTIFACTORY}, {"no-testing", no_argument, 0, OPT_NO_TESTING}, @@ -33,6 +35,7 @@ const char *long_options_help[] = { "Increase output verbosity", "Disable line buffering", "Update conda installation prior to STASIS environment creation", + "Overwrite an existing release", "Do not build docker images", "Do not upload artifacts to Artifactory", "Do not execute test scripts", @@ -95,7 +98,32 @@ static void usage(char *progname) { } } +static const char *has_envctl_key_(size_t i) { + for (size_t x = 0; globals.envctl[i].name[x] != NULL; x++) { + const char *name = globals.envctl[i].name[x]; + const char *data = getenv(name); + if (data) { + return name; + } + } + return NULL; +} + +static void check_system_env_requirements() { + msg(STASIS_MSG_L1, "Checking environment\n"); + for (size_t i = 0; globals.envctl[i].name[0] != NULL; i++) { + unsigned int flags = globals.envctl[i].flags; + const char *key = has_envctl_key_(i); + if ((flags & STASIS_ENVCTL_REQUIRED) && !(key && strlen(getenv(key)))) { + if (!strcmp(key, "STASIS_JF_REPO") && !globals.enable_artifactory) { + continue; + } + msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "Environment variable '%s' must be configured.\n", globals.envctl[i].name[0]); + exit(1); + } + } +} static void check_system_requirements(struct Delivery *ctx) { const char *tools_required[] = { @@ -139,6 +167,11 @@ static void check_system_requirements(struct Delivery *ctx) { } } +static void check_requirements(struct Delivery *ctx) { + check_system_requirements(ctx); + check_system_env_requirements(); +} + int main(int argc, char *argv[]) { struct Delivery ctx; struct Process proc = { @@ -193,6 +226,9 @@ int main(int argc, char *argv[]) { case 'v': globals.verbose = true; break; + case OPT_OVERWRITE: + globals.enable_overwrite = true; + break; case OPT_NO_DOCKER: globals.enable_docker = false; user_disabled_docker = true; @@ -260,6 +296,11 @@ int main(int argc, char *argv[]) { tpl_register("workaround.tox_posargs", &globals.workaround.tox_posargs); tpl_register("workaround.conda_reactivate", &globals.workaround.conda_reactivate); + // Expose function(s) to the template engine + // Prototypes can be found in template_func_proto.h + tpl_register_func("get_github_release_notes", &get_github_release_notes_tplfunc_entrypoint, 3, NULL); + tpl_register_func("get_github_release_notes_auto", &get_github_release_notes_auto_tplfunc_entrypoint, 1, &ctx); + // Set up PREFIX/etc directory information // The user may manipulate the base directory path with STASIS_SYSCONFDIR // environment variable @@ -325,7 +366,7 @@ int main(int argc, char *argv[]) { msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Failed to initialize delivery context\n"); exit(1); } - check_system_requirements(&ctx); + check_requirements(&ctx); msg(STASIS_MSG_L2, "Configuring JFrog CLI\n"); if (delivery_init_artifactory(&ctx)) { @@ -346,6 +387,12 @@ int main(int argc, char *argv[]) { //delivery_runtime_show(&ctx); } + // Safety gate: Avoid clobbering a delivery unless the user wants that behavior + if (delivery_exists(&ctx)) { + msg(STASIS_MSG_ERROR | STASIS_MSG_L1, "Refusing to overwrite delivery: %s\nUse --overwrite to enable release clobbering.\n", ctx.info.release_name); + exit(1); + } + msg(STASIS_MSG_L1, "Conda setup\n"); delivery_get_installer_url(&ctx, installer_url); msg(STASIS_MSG_L2, "Downloading: %s\n", installer_url); diff --git a/src/template.c b/src/template.c index 819fe92..8e1357c 100644 --- a/src/template.c +++ b/src/template.c @@ -25,10 +25,14 @@ extern void tpl_reset() { tpl_pool_func_used = 0; } -void tpl_register_func(char *key, struct tplfunc_frame *frame) { - (void) key; // TODO: placeholder - tpl_pool_func[tpl_pool_func_used] = calloc(1, sizeof(*tpl_pool_func[tpl_pool_func_used])); - memcpy(tpl_pool_func[tpl_pool_func_used], frame, sizeof(*frame)); +void tpl_register_func(char *key, void *tplfunc_ptr, int argc, void *data_in) { + struct tplfunc_frame *frame = calloc(1, sizeof(*frame)); + frame->key = strdup(key); + frame->argc = argc; + frame->func = tplfunc_ptr; + frame->data_in = data_in; + + tpl_pool_func[tpl_pool_func_used] = frame; tpl_pool_func_used++; } @@ -220,7 +224,7 @@ char *tpl_render(char *str) { strcpy(func_name_temp, type_stop + 1); char *param_begin = strchr(func_name_temp, '('); if (!param_begin) { - fprintf(stderr, "offset %zu: function name must be followed by a '('\n", off); + fprintf(stderr, "At position %zu in %s\nfunction name must be followed by a '('\n", off, key); guard_free(output); return NULL; } @@ -228,7 +232,7 @@ char *tpl_render(char *str) { param_begin++; char *param_end = strrchr(param_begin, ')'); if (!param_end) { - fprintf(stderr, "offset %zu: function arguments must be closed with a ')'\n", off); + fprintf(stderr, "At position %zu in %s\nfunction arguments must be closed with a ')'\n", off, key); guard_free(output); return NULL; } @@ -239,8 +243,8 @@ char *tpl_render(char *str) { for (params_count = 0; params[params_count] != NULL; params_count++); struct tplfunc_frame *frame = tpl_getfunc(k); - if (params_count > frame->argc) { - fprintf(stderr, "offset %zu: Too many arguments for function: %s()\n", off, frame->key); + if (params_count > frame->argc || params_count < frame->argc) { + fprintf(stderr, "At position %zu in %s\nIncorrect number of arguments for function: %s (expected %d, got %d)\n", off, key, frame->key, frame->argc, params_count); value = strdup(""); } else { for (size_t p = 0; p < sizeof(frame->argv) / sizeof(*frame->argv) && params[p] != NULL; p++) { @@ -248,10 +252,13 @@ char *tpl_render(char *str) { strip(params[p]); frame->argv[p].t_char_ptr = params[p]; } - char func_result[100]; - char *fres = func_result; - frame->func(frame, fres); - value = strdup(fres); + char *func_result = NULL; + int func_status = 0; + if ((func_status = frame->func(frame, &func_result))) { + fprintf(stderr, "%s returned non-zero status: %d\n", frame->key, func_status); + } + value = strdup(func_result ? func_result : ""); + guard_free(func_result); } GENERIC_ARRAY_FREE(params); } else { diff --git a/src/template_func_proto.c b/src/template_func_proto.c new file mode 100644 index 0000000..140a5e0 --- /dev/null +++ b/src/template_func_proto.c @@ -0,0 +1,65 @@ +#include "template_func_proto.h" + +int get_github_release_notes_tplfunc_entrypoint(void *frame, void *data_out) { + int result; + char **output = (char **) data_out; + struct tplfunc_frame *f = (struct tplfunc_frame *) frame; + char *api_token = getenv("STASIS_GH_TOKEN"); + if (!api_token) { + api_token = getenv("GITHUB_TOKEN"); + } + result = get_github_release_notes(api_token ? api_token : "anonymous", + (const char *) f->argv[0].t_char_ptr, + (const char *) f->argv[1].t_char_ptr, + (const char *) f->argv[2].t_char_ptr, + output); + return result; +} + +int get_github_release_notes_auto_tplfunc_entrypoint(void *frame, void *data_out) { + int result = 0; + char **output = (char **) data_out; + struct tplfunc_frame *f = (struct tplfunc_frame *) frame; + char *api_token = getenv("STASIS_GITHUB_TOKEN"); + if (!api_token) { + api_token = getenv("GITHUB_TOKEN"); + } + + const struct Delivery *ctx = (struct Delivery *) f->data_in; + struct StrList *notes_list = strlist_init(); + for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(*ctx->tests); i++) { + // Get test context + const struct Test *test = &ctx->tests[i]; + if (test->name && test->version && test->repository) { + char *repository = strdup(test->repository); + char *match = strstr(repository, "spacetelescope/"); + // Cull repository URL + if (match) { + replace_text(repository, "https://github.com/", "", 0); + if (endswith(repository, ".git")) { + replace_text(repository, ".git", "", 0); + } + // Record release notes for version relative to HEAD + // Using HEAD, GitHub returns the previous tag + char *note = NULL; + char h1_title[NAME_MAX] = {0}; + sprintf(h1_title, "# %s", test->name); + strlist_append(¬es_list, h1_title); + result += get_github_release_notes(api_token ? api_token : "anonymous", + repository, + test->version, + "HEAD", + ¬e); + if (note) { + strlist_append(¬es_list, note); + guard_free(note); + } + } + } + } + // Return all notes as a single string + if (strlist_count(notes_list)) { + *output = join(notes_list->data, "\n\n"); + } + return result; +} |