mirror of
https://github.com/neondatabase/neon.git
synced 2026-02-10 06:00:38 +00:00
Compare commits
7 Commits
initdb-cac
...
dkr/unknow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15bde20c4b | ||
|
|
88ad410a81 | ||
|
|
4967355664 | ||
|
|
d551de60d7 | ||
|
|
d287e6a522 | ||
|
|
18f6ccd77e | ||
|
|
fdd504f043 |
@@ -393,69 +393,79 @@ impl PageServerNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tenant_config(&self, tenant_id: TenantId, settings: HashMap<&str, &str>) -> Result<()> {
|
pub fn tenant_config(
|
||||||
|
&self,
|
||||||
|
tenant_id: TenantId,
|
||||||
|
mut settings: HashMap<&str, &str>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let config = {
|
let config = {
|
||||||
// Braces to make the diff easier to read
|
// Braces to make the diff easier to read
|
||||||
models::TenantConfig {
|
models::TenantConfig {
|
||||||
checkpoint_distance: settings
|
checkpoint_distance: settings
|
||||||
.get("checkpoint_distance")
|
.remove("checkpoint_distance")
|
||||||
.map(|x| x.parse::<u64>())
|
.map(|x| x.parse::<u64>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'checkpoint_distance' as an integer")?,
|
.context("Failed to parse 'checkpoint_distance' as an integer")?,
|
||||||
checkpoint_timeout: settings.get("checkpoint_timeout").map(|x| x.to_string()),
|
checkpoint_timeout: settings.remove("checkpoint_timeout").map(|x| x.to_string()),
|
||||||
compaction_target_size: settings
|
compaction_target_size: settings
|
||||||
.get("compaction_target_size")
|
.remove("compaction_target_size")
|
||||||
.map(|x| x.parse::<u64>())
|
.map(|x| x.parse::<u64>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'compaction_target_size' as an integer")?,
|
.context("Failed to parse 'compaction_target_size' as an integer")?,
|
||||||
compaction_period: settings.get("compaction_period").map(|x| x.to_string()),
|
compaction_period: settings.remove("compaction_period").map(|x| x.to_string()),
|
||||||
compaction_threshold: settings
|
compaction_threshold: settings
|
||||||
.get("compaction_threshold")
|
.remove("compaction_threshold")
|
||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'compaction_threshold' as an integer")?,
|
.context("Failed to parse 'compaction_threshold' as an integer")?,
|
||||||
gc_horizon: settings
|
gc_horizon: settings
|
||||||
.get("gc_horizon")
|
.remove("gc_horizon")
|
||||||
.map(|x| x.parse::<u64>())
|
.map(|x| x.parse::<u64>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'gc_horizon' as an integer")?,
|
.context("Failed to parse 'gc_horizon' as an integer")?,
|
||||||
gc_period: settings.get("gc_period").map(|x| x.to_string()),
|
gc_period: settings.remove("gc_period").map(|x| x.to_string()),
|
||||||
image_creation_threshold: settings
|
image_creation_threshold: settings
|
||||||
.get("image_creation_threshold")
|
.remove("image_creation_threshold")
|
||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
||||||
pitr_interval: settings.get("pitr_interval").map(|x| x.to_string()),
|
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
||||||
walreceiver_connect_timeout: settings
|
walreceiver_connect_timeout: settings
|
||||||
.get("walreceiver_connect_timeout")
|
.remove("walreceiver_connect_timeout")
|
||||||
|
.map(|x| x.to_string()),
|
||||||
|
lagging_wal_timeout: settings
|
||||||
|
.remove("lagging_wal_timeout")
|
||||||
.map(|x| x.to_string()),
|
.map(|x| x.to_string()),
|
||||||
lagging_wal_timeout: settings.get("lagging_wal_timeout").map(|x| x.to_string()),
|
|
||||||
max_lsn_wal_lag: settings
|
max_lsn_wal_lag: settings
|
||||||
.get("max_lsn_wal_lag")
|
.remove("max_lsn_wal_lag")
|
||||||
.map(|x| x.parse::<NonZeroU64>())
|
.map(|x| x.parse::<NonZeroU64>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'max_lsn_wal_lag' as non zero integer")?,
|
.context("Failed to parse 'max_lsn_wal_lag' as non zero integer")?,
|
||||||
trace_read_requests: settings
|
trace_read_requests: settings
|
||||||
.get("trace_read_requests")
|
.remove("trace_read_requests")
|
||||||
.map(|x| x.parse::<bool>())
|
.map(|x| x.parse::<bool>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'trace_read_requests' as bool")?,
|
.context("Failed to parse 'trace_read_requests' as bool")?,
|
||||||
eviction_policy: settings
|
eviction_policy: settings
|
||||||
.get("eviction_policy")
|
.remove("eviction_policy")
|
||||||
.map(|x| serde_json::from_str(x))
|
.map(serde_json::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'eviction_policy' json")?,
|
.context("Failed to parse 'eviction_policy' json")?,
|
||||||
min_resident_size_override: settings
|
min_resident_size_override: settings
|
||||||
.get("min_resident_size_override")
|
.remove("min_resident_size_override")
|
||||||
.map(|x| x.parse::<u64>())
|
.map(|x| x.parse::<u64>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'min_resident_size_override' as an integer")?,
|
.context("Failed to parse 'min_resident_size_override' as an integer")?,
|
||||||
evictions_low_residence_duration_metric_threshold: settings
|
evictions_low_residence_duration_metric_threshold: settings
|
||||||
.get("evictions_low_residence_duration_metric_threshold")
|
.remove("evictions_low_residence_duration_metric_threshold")
|
||||||
.map(|x| x.to_string()),
|
.map(|x| x.to_string()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !settings.is_empty() {
|
||||||
|
bail!("Unrecognized tenant settings: {settings:?}")
|
||||||
|
}
|
||||||
|
|
||||||
self.http_request(Method::PUT, format!("{}/tenant/config", self.http_base_url))?
|
self.http_request(Method::PUT, format!("{}/tenant/config", self.http_base_url))?
|
||||||
.json(&models::TenantConfigRequest { tenant_id, config })
|
.json(&models::TenantConfigRequest { tenant_id, config })
|
||||||
.send()?
|
.send()?
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
marker::PhantomData,
|
||||||
num::{NonZeroU64, NonZeroUsize},
|
num::{NonZeroU64, NonZeroUsize},
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
|
unreachable,
|
||||||
};
|
};
|
||||||
|
|
||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
@@ -132,12 +134,13 @@ pub struct TimelineCreateRequest {
|
|||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TenantCreateRequest {
|
pub struct TenantCreateRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub new_tenant_id: Option<TenantId>,
|
pub new_tenant_id: Option<TenantId>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub config: TenantConfig,
|
pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for TenantCreateRequest {
|
impl std::ops::Deref for TenantCreateRequest {
|
||||||
@@ -193,6 +196,7 @@ impl TenantCreateRequest {
|
|||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TenantConfigRequest {
|
pub struct TenantConfigRequest {
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub tenant_id: TenantId,
|
pub tenant_id: TenantId,
|
||||||
|
|||||||
@@ -741,8 +741,11 @@ paths:
|
|||||||
$ref: "#/components/schemas/Error"
|
$ref: "#/components/schemas/Error"
|
||||||
post:
|
post:
|
||||||
description: |
|
description: |
|
||||||
Create a tenant. Returns new tenant id on success.\
|
Create a tenant. Returns new tenant id on success.
|
||||||
|
|
||||||
If no new tenant id is specified in parameters, it would be generated. It's an error to recreate the same tenant.
|
If no new tenant id is specified in parameters, it would be generated. It's an error to recreate the same tenant.
|
||||||
|
|
||||||
|
Invalid fields in the tenant config will cause the request to be rejected with status 400.
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -790,6 +793,8 @@ paths:
|
|||||||
put:
|
put:
|
||||||
description: |
|
description: |
|
||||||
Update tenant's config.
|
Update tenant's config.
|
||||||
|
|
||||||
|
Invalid fields in the tenant config will cause the request to be rejected with status 400.
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
|||||||
@@ -149,11 +149,16 @@ class PageserverHttpClient(requests.Session):
|
|||||||
assert isinstance(res_json, list)
|
assert isinstance(res_json, list)
|
||||||
return res_json
|
return res_json
|
||||||
|
|
||||||
def tenant_create(self, new_tenant_id: Optional[TenantId] = None) -> TenantId:
|
def tenant_create(
|
||||||
|
self, new_tenant_id: Optional[TenantId] = None, conf: Optional[Dict[str, Any]] = None
|
||||||
|
) -> TenantId:
|
||||||
|
if conf is not None:
|
||||||
|
assert "new_tenant_id" not in conf.keys()
|
||||||
res = self.post(
|
res = self.post(
|
||||||
f"http://localhost:{self.port}/v1/tenant",
|
f"http://localhost:{self.port}/v1/tenant",
|
||||||
json={
|
json={
|
||||||
"new_tenant_id": str(new_tenant_id) if new_tenant_id else None,
|
"new_tenant_id": str(new_tenant_id) if new_tenant_id else None,
|
||||||
|
**(conf or {}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.verbose_error(res)
|
self.verbose_error(res)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import json
|
import json
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
import pytest
|
||||||
from fixtures.log_helper import log
|
from fixtures.log_helper import log
|
||||||
from fixtures.neon_fixtures import (
|
from fixtures.neon_fixtures import (
|
||||||
LocalFsStorage,
|
LocalFsStorage,
|
||||||
|
NeonEnv,
|
||||||
NeonEnvBuilder,
|
NeonEnvBuilder,
|
||||||
RemoteStorageKind,
|
RemoteStorageKind,
|
||||||
)
|
)
|
||||||
|
from fixtures.pageserver.http import PageserverApiException
|
||||||
from fixtures.pageserver.utils import assert_tenant_state, wait_for_upload
|
from fixtures.pageserver.utils import assert_tenant_state, wait_for_upload
|
||||||
from fixtures.types import Lsn
|
from fixtures.types import Lsn
|
||||||
from fixtures.utils import wait_until
|
from fixtures.utils import wait_until
|
||||||
@@ -403,3 +407,62 @@ def test_live_reconfig_get_evictions_low_residence_duration_metric_threshold(
|
|||||||
metric = get_metric()
|
metric = get_metric()
|
||||||
assert int(metric.labels["low_threshold_secs"]) == 24 * 60 * 60, "label resets to default"
|
assert int(metric.labels["low_threshold_secs"]) == 24 * 60 * 60, "label resets to default"
|
||||||
assert int(metric.value) == 0, "value resets to default"
|
assert int(metric.value) == 0, "value resets to default"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def unknown_fields_env(neon_env_builder: NeonEnvBuilder) -> Generator[NeonEnv, None, None]:
|
||||||
|
env = neon_env_builder.init_start()
|
||||||
|
yield env
|
||||||
|
env.pageserver.allowed_errors.extend(
|
||||||
|
[
|
||||||
|
".*/v1/tenant .*Error processing HTTP request: Bad request.*",
|
||||||
|
".*/v1/tenant/config .*Error processing HTTP request: Bad request.*",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_fields_cli_create(unknown_fields_env: NeonEnv):
|
||||||
|
"""
|
||||||
|
When specifying an invalid config field during tenant creation on the CLI, the CLI should fail with an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Unrecognized tenant settings"):
|
||||||
|
unknown_fields_env.neon_cli.create_tenant(conf={"unknown_field": "unknown_value"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_fields_http_create(unknown_fields_env: NeonEnv):
|
||||||
|
"""
|
||||||
|
When specifying an invalid config field during tenant creation on the HTTP API, the API should fail with an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ps_http = unknown_fields_env.pageserver.http_client()
|
||||||
|
|
||||||
|
with pytest.raises(PageserverApiException) as excinfo:
|
||||||
|
ps_http.tenant_create(conf={"unknown_field": "unknown_value"})
|
||||||
|
assert excinfo.value.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_fields_cli_config(unknown_fields_env: NeonEnv):
|
||||||
|
"""
|
||||||
|
When specifying an invalid config field during tenant configuration on the CLI, the CLI should fail with an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
(tenant_id, _) = unknown_fields_env.neon_cli.create_tenant()
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Unrecognized tenant settings"):
|
||||||
|
unknown_fields_env.neon_cli.config_tenant(
|
||||||
|
tenant_id, conf={"unknown_field": "unknown_value"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_fields_http_config(unknown_fields_env: NeonEnv):
|
||||||
|
"""
|
||||||
|
When specifying an invalid config field during tenant configuration on the HTTP API, the API should fail with an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
(tenant_id, _) = unknown_fields_env.neon_cli.create_tenant()
|
||||||
|
ps_http = unknown_fields_env.pageserver.http_client()
|
||||||
|
|
||||||
|
with pytest.raises(PageserverApiException) as excinfo:
|
||||||
|
ps_http.set_tenant_config(tenant_id, {"unknown_field": "unknown_value"})
|
||||||
|
assert excinfo.value.status_code == 400
|
||||||
|
|||||||
Reference in New Issue
Block a user