feat: add load_primary_config(), myconf_fini(); remove static cf global from myconf.c

- load_primary_config(): resolves conf/current, reads raw bytes, computes
  SHA-256 tag, decodes Cap'n Proto via load_cluster_config_from_file(),
  checks reserved names, sets config_generation, checks staleness
- myconf_fini(): frees conffile and cluster_config (calls free_ClusterConfig)
- Remove static cf global from myconf.c; get_default_conf() now uses a
  local struct cluster_configuration* via load_cluster_config()
- Add load_primary_config_for_test() under XCE_TESTING in config_repl.c
  to test path resolution without triggering FATAL
- Add xce/cluster_def.h include to config_repl.c and test_config_repl.c
  so cluster_t is complete when XCE_TESTING is active
- TDD: test_load_primary_config_missing_returns_error passes (18/18)
This commit is contained in:
Rongsong Shen 2026-05-13 16:06:18 +00:00
parent ce62406f2c
commit cd02cf40d2
4 changed files with 192 additions and 7 deletions

View file

@ -19,6 +19,7 @@
#include "byteorder.h"
#include "clusterconfig.h"
#include "utils.h"
#include "xce/cluster_def.h"
/* ── internal helpers ──────────────────────────── */
@ -364,6 +365,29 @@ int config_contains_reserved_name(cluster_config_t *cfg)
return 0;
}
/* ── load_primary_config_for_test ───────────────── */
#ifdef XCE_TESTING
int load_primary_config_for_test(cluster_t *c)
{
myconf_t *myconf = c->myconf;
char conf_dir[4096];
snprintf(conf_dir, sizeof(conf_dir), "%s/%s", myconf->workdir, myconf->name);
char fname[256] = {0};
int rc = conf_read_current_filename(conf_dir, fname, sizeof(fname));
if (rc != 0)
return rc;
free(myconf->conffile);
myconf->conffile = NULL;
if (asprintf(&myconf->conffile, "%s/conf/%s", conf_dir, fname) < 0)
return -ENOMEM;
return 0;
}
#endif
/* ── sanitise_cn ────────────────────────────────── */
char *sanitise_cn(const char *src, size_t maxlen)

View file

@ -90,3 +90,13 @@ int config_contains_reserved_name(cluster_config_t *cfg);
* Returns pointer to the static buffer. NOT thread-safe (static storage).
*/
char *sanitise_cn(const char *src, size_t maxlen);
#ifdef XCE_TESTING
/*
* load_primary_config_for_test: test-only wrapper that exercises the path
* resolution logic of load_primary_config() without calling FATAL.
* Returns 0 if conf/current resolves to a valid filename, negative errno
* otherwise. Does NOT perform Cap'n Proto decode or SHA-256 computation.
*/
int load_primary_config_for_test(cluster_t *c);
#endif

View file

@ -1,9 +1,15 @@
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <openssl/sha.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlog.h>
#include "byteorder.h"
#include "config_repl.h"
#include "configobj.h"
#include "controller.capnp.h"
#include "myconf.h"
@ -131,12 +137,10 @@ int load_cluster_config(cluster_t* c,
return 0;
}
static struct cluster_configuration* cf = NULL;
cluster_config_t* get_default_conf(void* cl)
{
cluster_config_t* c;
cluster_t* cls = (cluster_t*)cl;
struct cluster_configuration* cf = NULL;
cluster_t* cls = (cluster_t*)cl;
load_cluster_config(cls, &cf);
if (cf == NULL) {
@ -144,7 +148,109 @@ cluster_config_t* get_default_conf(void* cl)
return NULL;
}
c = cf->cluster;
return c;
return cf->cluster;
}
/* ── load_primary_config ────────────────────────── */
int load_primary_config(cluster_t *c)
{
myconf_t *myconf = c->myconf;
/* Build conf dir path: <workdir>/<name> */
char conf_dir[4096];
snprintf(conf_dir, sizeof(conf_dir), "%s/%s", myconf->workdir, myconf->name);
/* Resolve conf/current → versioned filename */
char fname[256] = {0};
int rc = conf_read_current_filename(conf_dir, fname, sizeof(fname));
if (rc != 0) {
FATAL("Primary has no config at '%s/conf/current' (rc=%d) — "
"provision the config before starting",
conf_dir, rc);
return -ENOENT;
}
/* Heap-allocate conffile so it survives past this stack frame */
free(myconf->conffile);
myconf->conffile = NULL;
if (asprintf(&myconf->conffile, "%s/conf/%s", conf_dir, fname) < 0)
return -ENOMEM;
/* Read raw bytes and check size */
uint8_t *bytes = NULL;
size_t blen = 0;
rc = conf_read_raw(myconf->conffile, &bytes, &blen);
if (rc != 0) {
FATAL("Primary config '%s' unreadable: %d", myconf->conffile, rc);
free(myconf->conffile);
myconf->conffile = NULL;
return rc;
}
/* Compute and cache SHA-256 */
SHA256(bytes, blen, myconf->config_sha256);
uint64_t tag;
memcpy(&tag, myconf->config_sha256, 8);
myconf->config_version_tag = be64toh(tag);
/* Decode Cap'n Proto directly into myconf->cluster_config */
rc = load_cluster_config_from_file(myconf->conffile, &myconf->cluster_config);
if (rc != 0) {
FATAL("Primary config '%s' failed Cap'n Proto decode: %d",
myconf->conffile, rc);
free(bytes);
free(myconf->conffile);
myconf->conffile = NULL;
return rc;
}
if (myconf->cluster_config == NULL) {
FATAL("Primary config '%s' decoded to NULL", myconf->conffile);
free(bytes);
free(myconf->conffile);
myconf->conffile = NULL;
return -EINVAL;
}
free(bytes);
fill_default_cluster_config(myconf->cluster_config);
/* Check for reserved lost_and_found name */
if (config_contains_reserved_name(myconf->cluster_config)) {
FATAL("Config '%s' contains reserved name 'lost_and_found'",
myconf->conffile);
return -EINVAL;
}
/* Default generation to 1 if not set */
if (myconf->cluster_config->config_generation == 0)
myconf->cluster_config->config_generation = 1;
myconf->config_generation = myconf->cluster_config->config_generation;
/* Check staleness */
myconf->conf_stale = conf_check_stale(conf_dir, myconf->conf_stale_timeout);
if (myconf->conf_stale)
WARN("Primary config appears stale (age > %lds)",
(long)myconf->conf_stale_timeout);
INFO("Primary config loaded: tag=%016" PRIx64 " gen=%" PRIu64,
myconf->config_version_tag, myconf->config_generation);
return 0;
}
/* ── myconf_fini ────────────────────────────────── */
void myconf_fini(myconf_t *myconf)
{
if (!myconf)
return;
free(myconf->conffile);
myconf->conffile = NULL;
if (myconf->cluster_config) {
free_ClusterConfig(myconf->cluster_config);
free(myconf->cluster_config);
myconf->cluster_config = NULL;
}
}

View file

@ -16,6 +16,7 @@
#include "config_repl.h"
#include "myconf.h"
#include "xce/cluster_def.h"
/* Per-test directory helpers — not Unity setUp/tearDown (those are in
* controller_test.c). Each test that needs a temp dir calls cr_setup()
@ -24,6 +25,9 @@
static char g_tmpdir[256];
static char g_conf_dir[300];
/* tmpdir string for load_primary_config tests (no conf/current) */
static char tmpdir[256];
static void cr_setup(void)
{
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_cr_%d_%ld",
@ -367,6 +371,44 @@ static void test_sanitise_cn_truncated(void)
printf("test_sanitise_cn_truncated: PASS\n");
}
/* ── load_primary_config (via test-only helper) ── */
#ifdef XCE_TESTING
static void test_load_primary_config_missing_returns_error(void)
{
/* Create a minimal cluster_t with no conf/current file */
cluster_t c;
memset(&c, 0, sizeof(c));
myconf_t myconf;
memset(&myconf, 0, sizeof(myconf));
/* Use a fresh tmpdir with no conf/current */
snprintf(tmpdir, sizeof(tmpdir), "/tmp/test_lpc_%d_%ld",
(int)getpid(), (long)time(NULL));
mkdir(tmpdir, 0700);
myconf.workdir = tmpdir;
myconf.name = "ctrl0";
myconf.conf_keep_n = 2;
myconf.conf_stale_timeout = 86400;
myconf.conffile = NULL;
c.myconf = &myconf;
/* No conf/current exists — must fail (not FATAL in test context) */
int rc = load_primary_config_for_test(&c);
TEST_ASSERT_TRUE(rc < 0);
/* conffile must still be NULL (no heap allocation on failure) */
TEST_ASSERT_NULL(myconf.conffile);
/* Cleanup */
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmpdir);
system(cmd);
printf("test_load_primary_config_missing_returns_error: PASS\n");
}
#endif
/* ── dispatch ───────────────────────────────────── */
void test_config_repl(void)
@ -388,4 +430,7 @@ void test_config_repl(void)
RUN_TEST(test_sanitise_cn_printable);
RUN_TEST(test_sanitise_cn_control_chars);
RUN_TEST(test_sanitise_cn_truncated);
#ifdef XCE_TESTING
RUN_TEST(test_load_primary_config_missing_returns_error);
#endif
}