Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions api/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pub struct DaemonConf {
pub log_level: String,
}

// Set/update global configuration.
pub type Config = std::collections::HashMap<String, String>;

/// Identifier for cached blob objects.
///
/// Domains are used to control the blob sharing scope. All blobs associated with the same domain
Expand Down Expand Up @@ -106,6 +109,10 @@ pub enum ApiRequest {
ExportFsFilesMetrics(Option<String>, bool),
/// Get information about filesystem inflight requests.
ExportFsInflightMetrics,
/// Get global configuration.
GetConfig(Option<String>),
/// Update global configuration.
UpdateConfig(Option<String>, Config),

// Nydus API v2
/// Get daemon information excluding filesystem backends.
Expand Down Expand Up @@ -193,6 +200,8 @@ pub enum ApiResponsePayload {
FsBackendInfo(String),
// Filesystem Inflight Requests, v1.
FsInflightMetrics(String),
// Global configuration, v1.
Config(Config),

/// List of blob objects, v2
BlobObjectList(String),
Expand Down
31 changes: 30 additions & 1 deletion api/src/http_endpoint_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use dbs_uhttp::{Method, Request, Response};

use crate::http::{ApiError, ApiRequest, ApiResponse, ApiResponsePayload, HttpError};
use crate::http::{ApiError, ApiRequest, ApiResponse, ApiResponsePayload, Config, HttpError};
use crate::http_handler::{
error_response, extract_query_part, parse_body, success_response, translate_status_code,
EndpointHandler, HttpResult,
Expand All @@ -34,6 +34,10 @@ fn convert_to_response<O: FnOnce(ApiError) -> HttpError>(api_resp: ApiResponse,
FsFilesPatterns(d) => success_response(Some(d)),
FsBackendInfo(d) => success_response(Some(d)),
FsInflightMetrics(d) => success_response(Some(d)),
Config(conf) => {
let json = serde_json::to_string(&conf).unwrap_or_else(|_| "{}".to_string());
success_response(Some(json))
}
_ => panic!("Unexpected response message from API service"),
}
}
Expand Down Expand Up @@ -166,3 +170,28 @@ impl EndpointHandler for MetricsFsInflightHandler {
}
}
}

/// Update global configuration of the daemon.
pub struct ConfigHandler {}
impl EndpointHandler for ConfigHandler {
fn handle_request(
&self,
req: &Request,
kicker: &dyn Fn(ApiRequest) -> ApiResponse,
) -> HttpResult {
match (req.method(), req.body.as_ref()) {
(Method::Get, None) => {
let id = extract_query_part(req, "id");
let r = kicker(ApiRequest::GetConfig(id));
Ok(convert_to_response(r, HttpError::Configure))
}
(Method::Put, Some(body)) => {
let conf: Config = parse_body(body)?;
let id = extract_query_part(req, "id");
let r = kicker(ApiRequest::UpdateConfig(id, conf));
Ok(convert_to_response(r, HttpError::Configure))
}
_ => Err(HttpError::BadRequest),
}
}
}
5 changes: 3 additions & 2 deletions api/src/http_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use crate::http_endpoint_common::{
SendFuseFdHandler, StartHandler, TakeoverFuseFdHandler,
};
use crate::http_endpoint_v1::{
FsBackendInfo, InfoHandler, MetricsFsAccessPatternHandler, MetricsFsFilesHandler,
MetricsFsGlobalHandler, MetricsFsInflightHandler, HTTP_ROOT_V1,
ConfigHandler, FsBackendInfo, InfoHandler, MetricsFsAccessPatternHandler,
MetricsFsFilesHandler, MetricsFsGlobalHandler, MetricsFsInflightHandler, HTTP_ROOT_V1,
};
use crate::http_endpoint_v2::{BlobObjectListHandlerV2, InfoV2Handler, HTTP_ROOT_V2};

Expand Down Expand Up @@ -158,6 +158,7 @@ lazy_static! {
r.routes.insert(endpoint_v1!("/metrics/files"), Box::new(MetricsFsFilesHandler{}));
r.routes.insert(endpoint_v1!("/metrics/inflight"), Box::new(MetricsFsInflightHandler{}));
r.routes.insert(endpoint_v1!("/metrics/pattern"), Box::new(MetricsFsAccessPatternHandler{}));
r.routes.insert(endpoint_v1!("/config"), Box::new(ConfigHandler{}));

// Nydus API, v2
r.routes.insert(endpoint_v2!("/daemon"), Box::new(InfoV2Handler{}));
Expand Down
45 changes: 45 additions & 0 deletions docs/nydusd.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,48 @@ mnt
├── pseudo_1
└── pseudo_2
```

### Hot Reload Configuration

Nydusd supports hot reloading of configuration without restarting the daemon. This is useful for updating credentials or other settings at runtime.

#### Update Configuration

To update configuration (e.g., registry authentication):

```shell
curl --unix-socket /path/to/api.sock \
-X PUT "http://localhost/api/v1/config?id=/" \
-H "Content-Type: application/json" \
-d '{
"registry_auth": "<base64_encoded_auth>"
}'
```

#### Query Current Configuration

To retrieve the current configuration:

```shell
curl --unix-socket /path/to/api.sock \
-X GET "http://localhost/api/v1/config?id=/"
```

Example response:

```json
{
"registry_auth": "<base64_encoded_auth>"
}
```

> **Note**: The `id` parameter specifies which mountpoint to configure. Use `/` for the root mountpoint or specify a sub-mountpoint path for multi-mount scenarios.

#### Supported Configuration Fields

The following fields can be updated via the hot reload API:

| Field | Description |
| --------------- | ------------------------------------------------------------------------- |
| `registry_auth` | Base64-encoded `username:password` credential for registry authentication |

24 changes: 17 additions & 7 deletions rafs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,30 @@ pub struct Rafs {

impl Rafs {
/// Create a new instance of `Rafs`.
pub fn new(cfg: &Arc<ConfigV2>, id: &str, path: &Path) -> RafsResult<(Self, RafsIoReader)> {
pub fn new(
cfg: &Arc<ConfigV2>,
mountpoint: &str,
metadata_path: &Path,
) -> RafsResult<(Self, RafsIoReader)> {
// Assume all meta/data blobs are accessible, otherwise it will always cause IO errors.
cfg.internal.set_blob_accessible(true);

let cache_cfg = cfg.get_cache_config().map_err(RafsError::LoadConfig)?;
let rafs_cfg = cfg.get_rafs_config().map_err(RafsError::LoadConfig)?;
let (sb, reader) = RafsSuper::load_from_file(path, cfg.clone(), false)
let (sb, reader) = RafsSuper::load_from_file(metadata_path, cfg.clone(), false)
.map_err(RafsError::FillSuperBlock)?;
let blob_infos = sb.superblock.get_blob_infos();
let device = BlobDevice::new(cfg, &blob_infos).map_err(RafsError::CreateDevice)?;
let device =
BlobDevice::new(cfg, &blob_infos, mountpoint).map_err(RafsError::CreateDevice)?;

if cfg.is_chunk_validation_enabled() && sb.meta.has_inlined_chunk_digest() {
sb.superblock.set_blob_device(device.clone());
}

let rafs = Rafs {
id: id.to_string(),
id: mountpoint.to_string(),
device,
ios: metrics::FsIoStats::new(id),
ios: metrics::FsIoStats::new(mountpoint),
sb: Arc::new(sb),

initialized: false,
Expand Down Expand Up @@ -140,7 +145,12 @@ impl Rafs {
}

/// Update storage backend for blobs.
pub fn update(&self, r: &mut RafsIoReader, conf: &Arc<ConfigV2>) -> RafsResult<()> {
pub fn update(
&self,
r: &mut RafsIoReader,
conf: &Arc<ConfigV2>,
mountpoint: &str,
) -> RafsResult<()> {
info!("update");
if !self.initialized {
warn!("Rafs is not yet initialized");
Expand All @@ -159,7 +169,7 @@ impl Rafs {
// step 2: update device (only localfs is supported)
let blob_infos = self.sb.superblock.get_blob_infos();
self.device
.update(conf, &blob_infos, self.fs_prefetch)
.update(conf, &blob_infos, self.fs_prefetch, mountpoint)
.map_err(RafsError::SwapBackend)?;
info!("update device is successful");

Expand Down
2 changes: 1 addition & 1 deletion rafs/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ impl RafsSuper {
/// The `BlobDevice` object is needed to get meta information from RAFS V6 data blobs.
pub fn create_blob_device(&self, config: Arc<ConfigV2>) -> Result<()> {
let blobs = self.superblock.get_blob_infos();
let device = BlobDevice::new(&config, &blobs)?;
let device = BlobDevice::new(&config, &blobs, "/")?;
self.superblock.set_blob_device(device);
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion service/src/blob_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ impl DataBlob {
pub fn new(config: &Arc<DataBlobConfig>) -> Result<Self> {
let blob_id = config.blob_info().blob_id();
let blob = BLOB_FACTORY
.new_blob_cache(config.config_v2(), &config.blob_info)
.new_blob_cache(config.config_v2(), &config.blob_info, "/")
.inspect_err(|_e| {
warn!(
"blob_cache: failed to create cache object for blob {}",
Expand Down
2 changes: 1 addition & 1 deletion service/src/fs_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ impl FsCacheHandler {
let mut blob_info = config.blob_info().deref().clone();
blob_info.set_fscache_file(Some(file));
let blob_ref = Arc::new(blob_info);
BLOB_FACTORY.new_blob_cache(config.config_v2(), &blob_ref)
BLOB_FACTORY.new_blob_cache(config.config_v2(), &blob_ref, "/")
}

fn fill_bootstrap_cache(bootstrap: Arc<FsCacheBootstrap>) -> Result<u64> {
Expand Down
2 changes: 1 addition & 1 deletion service/src/fs_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub trait FsService: Send + Sync {
let rafs_cfg = ConfigV2::from_str(&cmd.config).map_err(RafsError::LoadConfig)?;
let rafs_cfg = Arc::new(rafs_cfg);

rafs.update(&mut bootstrap, &rafs_cfg)
rafs.update(&mut bootstrap, &rafs_cfg, &cmd.mountpoint)
.map_err(|e| match e {
RafsError::Unsupported => Error::Unsupported,
e => Error::Rafs(e),
Expand Down
68 changes: 68 additions & 0 deletions smoke/tests/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,74 @@ func (a *APIV1TestSuite) TestMount(t *testing.T) {
nydusd.VerifyByPath(t, rootFs.FileTree, config.MountPath)
}

func (a *APIV1TestSuite) TestHotReloadConfig(t *testing.T) {

ctx := tool.DefaultContext(t)

ctx.PrepareWorkDir(t)
defer ctx.Destroy(t)

rootFs := texture.MakeLowerLayer(t, filepath.Join(ctx.Env.WorkDir, "root-fs"))

rafs := a.buildLayer(t, ctx, rootFs)

nydusd, err := tool.NewNydusd(tool.NydusdConfig{
NydusdPath: ctx.Binary.Nydusd,
BootstrapPath: rafs,
ConfigPath: filepath.Join(ctx.Env.WorkDir, "nydusd-config.fusedev.json"),
MountPath: ctx.Env.MountDir,
APISockPath: filepath.Join(ctx.Env.WorkDir, "nydusd-api.sock"),
BackendType: "localfs",
BackendConfig: fmt.Sprintf(`{"dir": "%s"}`, ctx.Env.BlobDir),
EnablePrefetch: ctx.Runtime.EnablePrefetch,
BlobCacheDir: ctx.Env.CacheDir,
CacheType: ctx.Runtime.CacheType,
CacheCompressed: ctx.Runtime.CacheCompressed,
RafsMode: ctx.Runtime.RafsMode,
DigestValidate: false,
})
require.NoError(t, err)

err = nydusd.Mount()
require.NoError(t, err)
defer func() {
if err := nydusd.Umount(); err != nil {
log.L.WithError(err).Errorf("umount")
}
}()

// Get initial configuration for root mountpoint
config, err := nydusd.GetConfig("/")
require.NoError(t, err)
require.NotNil(t, config)

// Update configuration with new registry auth
testAuth := "dGVzdDp0ZXN0" // base64 encoded "test:test"
newConfig := &tool.Config{
RegistryAuth: testAuth,
}
err = nydusd.UpdateConfig("/", newConfig)
require.NoError(t, err)

// Query configuration to verify the update
updatedConfig, err := nydusd.GetConfig("/")
require.NoError(t, err)
require.Equal(t, testAuth, updatedConfig.RegistryAuth)

// Update with different auth value
testAuth2 := "dXNlcjpwYXNzd29yZA==" // base64 encoded "user:password"
newConfig2 := &tool.Config{
RegistryAuth: testAuth2,
}
err = nydusd.UpdateConfig("/", newConfig2)
require.NoError(t, err)

// Verify the second update
updatedConfig2, err := nydusd.GetConfig("/")
require.NoError(t, err)
require.Equal(t, testAuth2, updatedConfig2.RegistryAuth)
}

func (a *APIV1TestSuite) buildLayer(t *testing.T, ctx *tool.Context, rootFs *tool.Layer) string {
digest := rootFs.Pack(t,
converter.PackOption{
Expand Down
53 changes: 53 additions & 0 deletions smoke/tests/tool/nydusd.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,59 @@ func (nydusd *Nydusd) GetInflightMetrics() (*InflightMetrics, error) {
return &info, err
}

// Config represents the configuration that can be hot reloaded.
type Config struct {
RegistryAuth string `json:"registry_auth,omitempty"`
}

// GetConfig retrieves the current configuration for the specified mountpoint.
func (nydusd *Nydusd) GetConfig(id string) (*Config, error) {
resp, err := nydusd.client.Get(fmt.Sprintf("http://unix/api/v1/config?id=%s", id))
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var config Config
if err = json.Unmarshal(body, &config); err != nil {
return nil, err
}

return &config, nil
}

// UpdateConfig updates the configuration for the specified mountpoint.
func (nydusd *Nydusd) UpdateConfig(id string, config *Config) error {
body, err := json.Marshal(config)
if err != nil {
return err
}

req, err := http.NewRequest("PUT", fmt.Sprintf("http://unix/api/v1/config?id=%s", id), bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")

resp, err := nydusd.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to update config: %s", string(body))
}

return nil
}

func (nydusd *Nydusd) Verify(t *testing.T, expectedFileTree map[string]*File) {
nydusd.VerifyByPath(t, expectedFileTree, "")
}
Expand Down
Loading
Loading