From 4cd00dc112ec233faf22e6fb3d2bce5b56cf6de2 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Fri, 9 Oct 2026 14:03:19 +0000 Subject: [POC PATCH v2 6/6] Add initdb --config-format=json Allow initdb to create a cluster whose configuration root is postgresql.json, with the settings initdb computes, any --set options and the default client authentication rules inline, instead of postgresql.conf, pg_hba.conf and pg_ident.conf. pg_hosts.conf is not created in that case, as nothing would read it. The default remains the legacy format. The file is generated rather than filled in from a sample: the legacy sample is mostly commentary on every parameter, which strict JSON has no place for. While at it, factor the writing of configuration files and the IPv6 probe out of setup_config() so both formats share them. --- doc/src/sgml/config.sgml | 6 + doc/src/sgml/ref/initdb.sgml | 29 ++ src/bin/initdb/initdb.c | 465 +++++++++++++++++++++++++++------ src/bin/initdb/t/001_initdb.pl | 137 ++++++++++ 4 files changed, 563 insertions(+), 74 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index b1a1226bfb9..181aabffa2b 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -599,6 +599,12 @@ include_dir 'conf.d' ALTER SYSTEM works unchanged. + + creates such a root when given + ; it contains only the settings + initdb chose and the default authentication rules. + + A short example: diff --git a/doc/src/sgml/ref/initdb.sgml b/doc/src/sgml/ref/initdb.sgml index 5e3f8dd0076..992f90c2241 100644 --- a/doc/src/sgml/ref/initdb.sgml +++ b/doc/src/sgml/ref/initdb.sgml @@ -189,6 +189,35 @@ PostgreSQL documentation + + + + + Selects the format of the configuration files that + initdb creates. The default, + conf, writes postgresql.conf, + pg_hba.conf and pg_ident.conf. + With json, a single + postgresql.json takes their place (see and ): it + holds the settings initdb determines, any given + with , and the same client authentication rules + the other files would contain, including those chosen with + and its variants. Parameters left at their + defaults are not listed; see for + those. The file is UTF-8, so initdb fails if a + value it would write, such as a locale name, is not valid UTF-8. + postgresql.auto.conf is created in either case, + pg_hosts.conf only with + conf, since nothing would read it with a JSON + configuration file. + The parameters hba_file, + ident_file and hosts_file + cannot be given with in this format. + + + + diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 86d42a98a27..92b9f57d1ff 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -169,6 +169,7 @@ static char *xlog_dir = NULL; static int wal_segment_size_mb = (DEFAULT_XLOG_SEG_SIZE) / (1024 * 1024); static DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC; static bool sync_data_files = true; +static bool config_json = false; /* internal vars */ @@ -266,8 +267,12 @@ static char **replace_guc_value(char **lines, const char *guc_name, const char *guc_value, bool mark_as_comment); static bool guc_value_requires_quotes(const char *guc_value); +static char *json_escape(const char *value); +static char *json_scalar(const char *value); static char **readfile(const char *path); static void writefile(char *path, char **lines); +static void write_conf_file(const char *filename, char **lines); +static void copy_conf_file(const char *sample, const char *filename); static FILE *popen_check(const char *command, const char *mode); static char *get_id(void); static int get_encoding_id(const char *encoding_name); @@ -278,6 +283,11 @@ static void set_null_conf(void); static void test_config_settings(void); static bool test_specific_config_settings(int test_conns, int test_av_slots, int test_buffs); +static bool have_ipv6_loopback(void); +static void setup_auto_conf(void); +static void json_setting(_stringlist **names, _stringlist **values, + const char *name, const char *value); +static void setup_config_json(void); static void setup_config(void); static void bootstrap_template1(void); static void setup_auth(FILE *cmdfd); @@ -660,6 +670,56 @@ guc_value_requires_quotes(const char *guc_value) return true; /* all else must be quoted */ } +/* + * Escape "value" for use as the contents of a JSON string literal. + * + * Only quotes and backslashes need it in practice; the values initdb writes + * (locale names, time zones, -c arguments) are not expected to contain + * control characters, but escape those too rather than emit an invalid + * document. + */ +static char * +json_escape(const char *value) +{ + PQExpBuffer result = createPQExpBuffer(); + char *data; + + for (; *value; value++) + { + if (*value == '"' || *value == '\\') + appendPQExpBuffer(result, "\\%c", *value); + else if ((unsigned char) *value < 0x20) + appendPQExpBuffer(result, "\\u%04x", (unsigned char) *value); + else + appendPQExpBufferChar(result, *value); + } + + data = result->data; + free(result); /* but don't free result->data */ + + return data; +} + +/* + * Format "value" as a JSON value. + * + * A plain integer is written as a number; anything else, including a number + * with a unit, becomes a string. The server treats both spellings alike. + */ +static char * +json_scalar(const char *value) +{ + const char *p = value; + + if (*p == '-') + p++; + if ((p[0] == '0' && p[1] == '\0') || + (p[0] >= '1' && p[0] <= '9' && strspn(p, "0123456789") == strlen(p))) + return pg_strdup(value); + + return psprintf("\"%s\"", json_escape(value)); +} + /* * get the lines from a text file * @@ -731,6 +791,32 @@ writefile(char *path, char **lines) free(lines); } +/* + * write a configuration file into the data directory + * + * As writefile(), plus the permissions a configuration file gets. + */ +static void +write_conf_file(const char *filename, char **lines) +{ + char path[MAXPGPATH]; + + snprintf(path, sizeof(path), "%s/%s", pg_data, filename); + + writefile(path, lines); + if (chmod(path, pg_file_create_mode) != 0) + pg_fatal("could not change permissions of \"%s\": %m", path); +} + +/* + * install a sample configuration file verbatim + */ +static void +copy_conf_file(const char *sample, const char *filename) +{ + write_conf_file(filename, readfile(sample)); +} + /* * Open a subcommand with suitable error messaging */ @@ -1037,6 +1123,10 @@ write_version_file(const char *extrapath) /* * set up an empty config file so we can check config settings by launching * a test backend + * + * The test backend looks for the root the way the server does, so the file + * must already be the one the chosen format uses: an empty JSON document is + * an empty object, not an empty file. */ static void set_null_conf(void) @@ -1044,10 +1134,13 @@ set_null_conf(void) FILE *conf_file; char *path; - path = psprintf("%s/postgresql.conf", pg_data); + path = psprintf("%s/%s", pg_data, + config_json ? "postgresql.json" : "postgresql.conf"); conf_file = fopen(path, PG_BINARY_W); if (conf_file == NULL) pg_fatal("could not open file \"%s\" for writing: %m", path); + if (config_json && fputs("{}\n", conf_file) < 0) + pg_fatal("could not write file \"%s\": %m", path); if (fclose(conf_file)) pg_fatal("could not write file \"%s\": %m", path); pfree(path); @@ -1271,6 +1364,211 @@ pretty_wal_size(int segment_count) return result; } +/* + * Probe to see if there is really any platform support for IPv6, so that + * the IPv6 loopback entries can be left out of the authentication rules if + * not. This avoids runtime warnings if getaddrinfo doesn't actually cope + * with IPv6. Particularly useful on Windows, where executables built on a + * machine with IPv6 may have to run on a machine without. + */ +static bool +have_ipv6_loopback(void) +{ + struct addrinfo *gai_result; + struct addrinfo hints; + int err = 0; + +#ifdef WIN32 + /* need to call WSAStartup before calling getaddrinfo */ + WSADATA wsaData; + + err = WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif + + /* for best results, this code should match parse_hba_line() */ + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = 0; + hints.ai_protocol = 0; + hints.ai_addrlen = 0; + hints.ai_canonname = NULL; + hints.ai_addr = NULL; + hints.ai_next = NULL; + + return (err == 0 && + getaddrinfo("::1", NULL, &hints, &gai_result) == 0); +} + +/* + * set up postgresql.auto.conf + */ +static void +setup_auto_conf(void) +{ + char **conflines; + + conflines = pg_malloc_array(char *, 3); + conflines[0] = pg_strdup("# Do not edit this file manually!\n"); + conflines[1] = pg_strdup("# It will be overwritten by the ALTER SYSTEM command.\n"); + conflines[2] = NULL; + + write_conf_file("postgresql.auto.conf", conflines); +} + +/* + * Set parameter "name" to "value" in the ordered settings list for + * postgresql.json. A name already present, compared case-insensitively, + * keeps its place and first spelling and takes the new value, as + * replace_guc_value() does for postgresql.conf; the JSON loader would reject + * the name twice. Otherwise the setting is appended. + */ +static void +json_setting(_stringlist **names, _stringlist **values, + const char *name, const char *value) +{ + _stringlist *n, + *v; + + for (n = *names, v = *values; n != NULL; n = n->next, v = v->next) + { + if (pg_strcasecmp(n->str, name) == 0) + { + free(v->str); + v->str = pg_strdup(value); + return; + } + } + + add_stringlist_item(names, name); + add_stringlist_item(values, value); +} + +/* + * set up postgresql.json, the configuration root in JSON format + * + * This writes the settings that setup_config() puts into postgresql.conf, + * and the authentication rules of pg_hba.conf.sample, into one document. + * The legacy sample's commented-out defaults have no place in it, and + * neither does the caution about "trust", which main() prints instead. + */ +static void +setup_config_json(void) +{ + _stringlist *names = NULL; + _stringlist *values = NULL; + _stringlist *n, + *v; + char repltok[MAXPGPATH]; + PQExpBuffer buf; + char *loopback; + char *local; + char *host; + char **conflines; + + snprintf(repltok, sizeof(repltok), "%d", n_connections); + json_setting(&names, &values, "max_connections", repltok); + + snprintf(repltok, sizeof(repltok), "%d", n_av_slots); + json_setting(&names, &values, "autovacuum_worker_slots", repltok); + + if ((n_buffers * (BLCKSZ / 1024)) % 1024 == 0) + snprintf(repltok, sizeof(repltok), "%dMB", + (n_buffers * (BLCKSZ / 1024)) / 1024); + else + snprintf(repltok, sizeof(repltok), "%dkB", + n_buffers * (BLCKSZ / 1024)); + json_setting(&names, &values, "shared_buffers", repltok); + + json_setting(&names, &values, "dynamic_shared_memory_type", + dynamic_shared_memory_type); + + /* Caution: these depend on wal_segment_size_mb, they're not constants */ + json_setting(&names, &values, "min_wal_size", + pretty_wal_size(DEFAULT_MIN_WAL_SEGS)); + json_setting(&names, &values, "max_wal_size", + pretty_wal_size(DEFAULT_MAX_WAL_SEGS)); + + if (default_timezone) + json_setting(&names, &values, "log_timezone", default_timezone); + + switch (locale_date_order(lc_time)) + { + case DATEORDER_YMD: + strcpy(repltok, "iso, ymd"); + break; + case DATEORDER_DMY: + strcpy(repltok, "iso, dmy"); + break; + case DATEORDER_MDY: + default: + strcpy(repltok, "iso, mdy"); + break; + } + json_setting(&names, &values, "datestyle", repltok); + + if (default_timezone) + json_setting(&names, &values, "timezone", default_timezone); + + json_setting(&names, &values, "lc_messages", lc_messages); + json_setting(&names, &values, "lc_monetary", lc_monetary); + json_setting(&names, &values, "lc_numeric", lc_numeric); + json_setting(&names, &values, "lc_time", lc_time); + + snprintf(repltok, sizeof(repltok), "pg_catalog.%s", + default_text_search_config); + json_setting(&names, &values, "default_text_search_config", repltok); + + /* see setup_config() for the reasons behind these */ + if ((strcmp(authmethodlocal, "md5") == 0 && + strcmp(authmethodhost, "scram-sha-256") != 0) || + (strcmp(authmethodhost, "md5") == 0 && + strcmp(authmethodlocal, "scram-sha-256") != 0)) + json_setting(&names, &values, "password_encryption", "md5"); + + if (pg_dir_create_mode == PG_DIR_MODE_GROUP) + json_setting(&names, &values, "log_file_mode", "0640"); + + /* and finally the -c switches */ + for (n = extra_guc_names, v = extra_guc_values; + n != NULL; /* assume lists have the same length */ + n = n->next, v = v->next) + json_setting(&names, &values, n->str, v->str); + + buf = createPQExpBuffer(); + appendPQExpBufferStr(buf, "{\n"); + for (n = names, v = values; n != NULL; n = n->next, v = v->next) + appendPQExpBuffer(buf, " \"%s\": %s,\n", + json_escape(n->str), json_scalar(v->str)); + + /* the authentication rules of pg_hba.conf.sample */ + loopback = have_ipv6_loopback() ? + "[\"127.0.0.1/32\", \"::1/128\"]" : "[\"127.0.0.1/32\"]"; + local = json_escape(authmethodlocal); + host = json_escape(authmethodhost); + appendPQExpBuffer(buf, + " \"hba\": {\"children\": [\n" + " {\"transport\": \"local\", \"auth\": {\"method\": \"%s\"}},\n" + " {\"address\": %s, \"auth\": {\"method\": \"%s\"}},\n" + " {\"databases\": \"replication\", \"children\": [\n" + " {\"transport\": \"local\", \"auth\": {\"method\": \"%s\"}},\n" + " {\"address\": %s, \"auth\": {\"method\": \"%s\"}}\n" + " ]}\n" + " ]},\n", + local, loopback, host, local, loopback, host); + appendPQExpBufferStr(buf, " \"ident\": {\"maps\": {}}\n}\n"); + + /* the server reads the file as UTF-8 */ + if (pg_encoding_verifymbstr(PG_UTF8, buf->data, buf->len) != buf->len) + pg_fatal("generated configuration is not valid UTF-8"); + + conflines = pg_malloc_array(char *, 2); + conflines[0] = buf->data; + conflines[1] = NULL; + free(buf); /* but don't free buf->data */ + + write_conf_file("postgresql.json", conflines); +} + /* * set up all the config files */ @@ -1279,13 +1577,27 @@ setup_config(void) { char **conflines; char repltok[MAXPGPATH]; - char path[MAXPGPATH]; _stringlist *gnames, *gvalues; fputs(_("creating configuration files ... "), stdout); fflush(stdout); + /* + * With a JSON root, postgresql.json takes the place of postgresql.conf, + * pg_hba.conf and pg_ident.conf, and those must not be created: the + * server refuses to choose between two roots. pg_hosts.conf is not + * created either, since nothing would read it. + */ + if (config_json) + { + setup_config_json(); + setup_auto_conf(); + + check_ok(); + return; + } + /* postgresql.conf */ conflines = readfile(conf_file); @@ -1436,70 +1748,27 @@ setup_config(void) } /* ... and write out the finished postgresql.conf file */ - snprintf(path, sizeof(path), "%s/postgresql.conf", pg_data); - - writefile(path, conflines); - if (chmod(path, pg_file_create_mode) != 0) - pg_fatal("could not change permissions of \"%s\": %m", path); + write_conf_file("postgresql.conf", conflines); /* postgresql.auto.conf */ - conflines = pg_malloc_array(char *, 3); - conflines[0] = pg_strdup("# Do not edit this file manually!\n"); - conflines[1] = pg_strdup("# It will be overwritten by the ALTER SYSTEM command.\n"); - conflines[2] = NULL; - - sprintf(path, "%s/postgresql.auto.conf", pg_data); - - writefile(path, conflines); - if (chmod(path, pg_file_create_mode) != 0) - pg_fatal("could not change permissions of \"%s\": %m", path); + setup_auto_conf(); /* pg_hba.conf */ conflines = readfile(hba_file); - /* - * Probe to see if there is really any platform support for IPv6, and - * comment out the relevant pg_hba line if not. This avoids runtime - * warnings if getaddrinfo doesn't actually cope with IPv6. Particularly - * useful on Windows, where executables built on a machine with IPv6 may - * have to run on a machine without. - */ + /* comment out the IPv6 lines if the platform cannot handle them */ + if (!have_ipv6_loopback()) { - struct addrinfo *gai_result; - struct addrinfo hints; - int err = 0; - -#ifdef WIN32 - /* need to call WSAStartup before calling getaddrinfo */ - WSADATA wsaData; - - err = WSAStartup(MAKEWORD(2, 2), &wsaData); -#endif - - /* for best results, this code should match parse_hba_line() */ - hints.ai_flags = AI_NUMERICHOST; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = 0; - hints.ai_protocol = 0; - hints.ai_addrlen = 0; - hints.ai_canonname = NULL; - hints.ai_addr = NULL; - hints.ai_next = NULL; - - if (err != 0 || - getaddrinfo("::1", NULL, &hints, &gai_result) != 0) - { - conflines = replace_token(conflines, - "host all all ::1/128", - "#host all all ::1/128"); - conflines = replace_token(conflines, - "host replication all ::1/128", - "#host replication all ::1/128"); - } + conflines = replace_token(conflines, + "host all all ::1/128", + "#host all all ::1/128"); + conflines = replace_token(conflines, + "host replication all ::1/128", + "#host replication all ::1/128"); } /* Replace default authentication methods */ @@ -1514,30 +1783,16 @@ setup_config(void) "@authcomment@", (strcmp(authmethodlocal, "trust") == 0 || strcmp(authmethodhost, "trust") == 0) ? AUTHTRUST_WARNING : ""); - snprintf(path, sizeof(path), "%s/pg_hba.conf", pg_data); - - writefile(path, conflines); - if (chmod(path, pg_file_create_mode) != 0) - pg_fatal("could not change permissions of \"%s\": %m", path); + write_conf_file("pg_hba.conf", conflines); /* pg_ident.conf */ - conflines = readfile(ident_file); - - snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data); - - writefile(path, conflines); - if (chmod(path, pg_file_create_mode) != 0) - pg_fatal("could not change permissions of \"%s\": %m", path); + copy_conf_file(ident_file, "pg_ident.conf"); /* pg_hosts.conf */ - conflines = readfile(hosts_file); - snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data); - writefile(path, conflines); - if (chmod(path, pg_file_create_mode) != 0) - pg_fatal("could not change permissions of \"%s\": %m", path); + copy_conf_file(hosts_file, "pg_hosts.conf"); check_ok(); } @@ -2531,6 +2786,8 @@ usage(const char *progname) printf(_(" -A, --auth=METHOD default authentication method for local connections\n")); printf(_(" --auth-host=METHOD default authentication method for local TCP/IP connections\n")); printf(_(" --auth-local=METHOD default authentication method for local-socket connections\n")); + printf(_(" --config-format=FORMAT\n" + " configuration file format: conf (default) or json\n")); printf(_(" [-D, --pgdata=]DATADIR location for this database cluster\n")); printf(_(" -E, --encoding=ENCODING set default encoding for new databases\n")); printf(_(" -g, --allow-group-access allow group read/execute on data directory\n")); @@ -3207,6 +3464,7 @@ main(int argc, char *argv[]) {"sync-method", required_argument, NULL, 19}, {"no-data-checksums", no_argument, NULL, 20}, {"no-sync-data-files", no_argument, NULL, 21}, + {"config-format", required_argument, NULL, 22}, {NULL, 0, NULL, 0} }; @@ -3404,6 +3662,14 @@ main(int argc, char *argv[]) case 21: sync_data_files = false; break; + case 22: + if (strcmp(optarg, "conf") == 0) + config_json = false; + else if (strcmp(optarg, "json") == 0) + config_json = true; + else + pg_fatal("unrecognized configuration format: %s", optarg); + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -3412,6 +3678,43 @@ main(int argc, char *argv[]) } + /* + * A JSON root holds the authentication rules and maps itself, so the + * parameters naming separate files for them, and the section names, + * cannot be set with -c. + */ + if (config_json) + { + static const char *const sections[] = {"hba", "ident", "hosts"}; + _stringlist *gnames; + + for (gnames = extra_guc_names; gnames != NULL; gnames = gnames->next) + { + const char *name = gnames->str; + + for (int i = 0; i < lengthof(sections); i++) + { + if (pg_strcasecmp(name, sections[i]) == 0) + { + pg_log_error("\"%s\" is a section of postgresql.json and cannot be set with --set", + name); + pg_log_error_hint("Edit the \"%s\" section of postgresql.json after initdb.", + sections[i]); + exit(1); + } + } + if (pg_strcasecmp(name, "hba_file") == 0 || + pg_strcasecmp(name, "ident_file") == 0 || + pg_strcasecmp(name, "hosts_file") == 0) + { + pg_log_error("parameter \"%s\" cannot be set with --config-format=json", + name); + pg_log_error_hint("With a JSON configuration file, client authentication is configured in its \"hba\", \"ident\" and \"hosts\" sections."); + exit(1); + } + } + } + /* * Non-option argument specifies data directory as long as it wasn't * already specified with -D / --pgdata @@ -3528,8 +3831,22 @@ main(int argc, char *argv[]) { printf("\n"); pg_log_warning("enabling \"trust\" authentication for local connections"); - pg_log_warning_hint("You can change this by editing pg_hba.conf or using the option -A, or " - "--auth-local and --auth-host, the next time you run initdb."); + pg_log_warning_hint("You can change this by editing %s or using the option -A, or " + "--auth-local and --auth-host, the next time you run initdb.", + config_json ? "postgresql.json" : "pg_hba.conf"); + } + + /* + * pg_hba.conf carries a caution about an explicitly chosen "trust" as a + * comment, which strict JSON has no room for, so say it here instead. + */ + if (config_json && !authwarning && + (strcmp(authmethodlocal, "trust") == 0 || + strcmp(authmethodhost, "trust") == 0)) + { + printf("\n"); + pg_log_warning("\"trust\" authentication allows any local user to connect as any PostgreSQL user"); + pg_log_warning_hint("If you do not trust all your local users, use another authentication method."); } if (!noinstructions) diff --git a/src/bin/initdb/t/001_initdb.pl b/src/bin/initdb/t/001_initdb.pl index 081535a22e4..47c62b967f5 100644 --- a/src/bin/initdb/t/001_initdb.pl +++ b/src/bin/initdb/t/001_initdb.pl @@ -9,6 +9,7 @@ use strict; use warnings FATAL => 'all'; use Fcntl ':mode'; use File::stat qw{lstat}; +use JSON::PP qw(decode_json); use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; @@ -331,4 +332,140 @@ command_fails( [ 'pg_checksums', '--pgdata' => $datadir_nochecksums ], "pg_checksums fails with data checksum disabled"); +# JSON configuration format +command_fails_like( + [ + 'initdb', '--no-sync', + '--config-format' => 'bogus', + "$tempdir/dataX" + ], + qr/unrecognized configuration format/, + 'fails for invalid configuration format'); +command_fails_like( + [ + 'initdb', '--no-sync', + '--config-format' => 'json', + '--set' => 'HBA=x', + "$tempdir/dataX" + ], + qr/"HBA" is a section of postgresql\.json and cannot be set with --set.*\n.*Edit the "hba" section/, + 'fails for --set of a JSON section'); +command_fails_like( + [ + 'initdb', '--no-sync', + '--config-format' => 'json', + '--set' => 'hba_file=x', + "$tempdir/dataX" + ], + qr/parameter "hba_file" cannot be set with --config-format=json/, + 'fails for --set of a file parameter with JSON'); + +# TZ is set so that a time zone is written even where none can be +# detected. +my $datadir_json = "$tempdir/data_json"; +{ + local $ENV{TZ} = 'UTC'; + + command_ok( + [ + 'initdb', '--no-sync', + '--config-format' => 'json', + '--locale' => 'C', + '--wal-segsize' => '16', + '--set' => 'work_mem=7MB', + '--set' => 'max_connections=25', + '--set' => 'Max_Connections=26', + '--set' => 'Log_Min_Messages=debug1', + '--set' => 'LOG_MIN_MESSAGES=info', + '--set' => 'application_name=a"b\c', + $datadir_json + ], + 'successful creation with JSON configuration'); +} +ok(-f "$datadir_json/postgresql.json", 'postgresql.json written'); +ok(-f "$datadir_json/postgresql.auto.conf", 'postgresql.auto.conf written'); +ok(!-f "$datadir_json/pg_hosts.conf", 'no pg_hosts.conf'); +ok(!-f "$datadir_json/postgresql.conf", 'no postgresql.conf'); +ok(!-f "$datadir_json/pg_hba.conf", 'no pg_hba.conf'); +ok(!-f "$datadir_json/pg_ident.conf", 'no pg_ident.conf'); + +my $jconf = slurp_file("$datadir_json/postgresql.json"); +my $jdoc = decode_json($jconf); +is($jdoc->{lc_messages}, 'C', 'lc_messages written'); +is($jdoc->{datestyle}, 'iso, mdy', 'datestyle written'); +ok(defined $jdoc->{timezone}, 'timezone written'); +is($jdoc->{log_timezone}, $jdoc->{timezone}, 'log_timezone follows timezone'); +like($jdoc->{dynamic_shared_memory_type}, + qr/^(posix|sysv|windows|mmap)$/, 'dynamic_shared_memory_type written'); +is($jdoc->{min_wal_size}, '80MB', 'min_wal_size written'); +like($jdoc->{shared_buffers}, qr/^\d+[kM]B$/, 'shared_buffers written'); +is($jdoc->{work_mem}, '7MB', '--set value present'); +is($jdoc->{application_name}, 'a"b\c', '--set value escaped'); +is($jdoc->{max_connections}, 26, '--set overrides computed setting'); +like($jconf, qr/^ "max_connections": 26,$/m, + 'override keeps canonical name and place'); +is($jdoc->{Log_Min_Messages}, + 'info', 'repeated --set keeps first spelling and last value'); +ok(!exists $jdoc->{LOG_MIN_MESSAGES}, 'repeated --set emitted once'); +is($jdoc->{hba}{children}[0]{auth}{method}, 'trust', 'auth method set'); +is_deeply($jdoc->{ident}, { maps => {} }, 'ident section present'); + +# Start a cluster from a JSON root, with distinct local and host methods. +# Cluster.pm appended its settings to postgresql.conf, which initdb did not +# create; fold that file into the JSON root as an include so that only one +# root exists. On Windows, pg_regress --config-auth writes pg_hba.conf and +# pg_ident.conf for SSPI; the JSON root ignores them and its own rules +# apply. +my $pwfile = "$tempdir/pwfile"; +append_to_file($pwfile, "secret\n"); +my $jnode = PostgreSQL::Test::Cluster->new('json'); +$jnode->init( + extra => [ + '--config-format' => 'json', + '--auth' => 'md5', + '--auth-host' => 'scram-sha-256', + '--pwfile' => $pwfile + ]); +my $jd = $jnode->data_dir; +rename("$jd/postgresql.conf", "$jd/cluster.conf") or die $!; +my $root = slurp_file("$jd/postgresql.json"); +$root =~ s/^\{/{\n "include": ["cluster.conf"],/ + or die "could not add include to postgresql.json"; +open my $jf, '>', "$jd/postgresql.json" or die $!; +print $jf $root; +close $jf; +$jnode->start; + +{ + local $ENV{PGPASSWORD} = 'secret'; + + is($jnode->safe_psql('postgres', 'SHOW port'), + $jnode->port, 'include of the JSON root was read'); + is( $jnode->safe_psql('postgres', 'SHOW max_connections'), + decode_json($root)->{max_connections}, + 'computed setting of the JSON root applied'); + + # initdb leaves out the ::1/128 entries when the platform has no IPv6, + # so the expected rules follow what it wrote. + my $ipv6 = ($root =~ /::1\/128/); + is( $jnode->safe_psql( + 'postgres', + q{SELECT string_agg(type || ':' || database[1] || ':' || + coalesce(address, '-') || ':' || auth_method, ' ' + ORDER BY rule_number) + FROM pg_hba_file_rules WHERE error IS NULL}), + join(' ', + 'local:all:-:md5', + 'host:all:127.0.0.1:scram-sha-256', + ($ipv6 ? 'host:all:::1:scram-sha-256' : ()), + 'local:replication:-:md5', + 'host:replication:127.0.0.1:scram-sha-256', + ($ipv6 ? 'host:replication:::1:scram-sha-256' : ())), + 'default hba rules loaded from JSON'); + is($jnode->safe_psql('postgres', + 'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'), + '0', 'no hba errors'); +} +$jnode->stop; + done_testing(); -- 2.56.0