diff --git a/src/controller/config_repl.c b/src/controller/config_repl.c index 06f08eb..152907c 100644 --- a/src/controller/config_repl.c +++ b/src/controller/config_repl.c @@ -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) diff --git a/src/controller/config_repl.h b/src/controller/config_repl.h index 4c15a11..2f2ebd2 100644 --- a/src/controller/config_repl.h +++ b/src/controller/config_repl.h @@ -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 diff --git a/src/controller/myconf.c b/src/controller/myconf.c index 19afb08..ad6b041 100644 --- a/src/controller/myconf.c +++ b/src/controller/myconf.c @@ -1,9 +1,15 @@ +#define _GNU_SOURCE #include +#include +#include +#include #include #include #include #include +#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: / */ + 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; + } } diff --git a/src/controller/testsuite/test_config_repl.c b/src/controller/testsuite/test_config_repl.c index 4e00694..33d2edf 100644 --- a/src/controller/testsuite/test_config_repl.c +++ b/src/controller/testsuite/test_config_repl.c @@ -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 }