mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-07-26 07:58:14 +00:00
admin api: expose routing rules, cors rules and lifecycle rules
This commit is contained in:
Generated
+1
@@ -1558,6 +1558,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -2340,6 +2340,16 @@
|
||||
"format": "int64",
|
||||
"description": "Total number of bytes used by objects in this bucket"
|
||||
},
|
||||
"corsRules": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cors.Rule"
|
||||
},
|
||||
"description": "CORS rules for this bucket"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -2363,6 +2373,16 @@
|
||||
},
|
||||
"description": "List of access keys that have permissions granted on this bucket"
|
||||
},
|
||||
"lifecycleRules": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/lifecycle.Rule"
|
||||
},
|
||||
"description": "Object lifecycle rules for this bucket"
|
||||
},
|
||||
"objects": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
@@ -2423,6 +2443,12 @@
|
||||
},
|
||||
"indexDocument": {
|
||||
"type": "string"
|
||||
},
|
||||
"routingRules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/website.RoutingRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4545,6 +4571,205 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"cors.Rule": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"AllowedOrigin",
|
||||
"AllowedMethod"
|
||||
],
|
||||
"properties": {
|
||||
"AllowedHeader": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"AllowedMethod": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"AllowedOrigin": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"ExposeHeader": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"ID": {},
|
||||
"MaxAgeSeconds": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lifecycle.AbortIncompleteMpu": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"DaysAfterInitiation"
|
||||
],
|
||||
"properties": {
|
||||
"DaysAfterInitiation": {
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lifecycle.Expiration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Date": {},
|
||||
"Days": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lifecycle.Filter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"And": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/lifecycle.Filter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ObjectSizeGreaterThan": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ObjectSizeLessThan": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Prefix": {}
|
||||
}
|
||||
},
|
||||
"lifecycle.Rule": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Status"
|
||||
],
|
||||
"properties": {
|
||||
"AbortIncompleteMultipartUpload": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/lifecycle.AbortIncompleteMpu"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Expiration": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/lifecycle.Expiration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Filter": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/lifecycle.Filter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ID": {},
|
||||
"Status": {}
|
||||
}
|
||||
},
|
||||
"website.Condition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"HttpErrorCodeReturnedEquals": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"KeyPrefixEquals": {}
|
||||
}
|
||||
},
|
||||
"website.Redirect": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"HostName": {},
|
||||
"HttpRedirectCode": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/xml.IntValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Protocol": {},
|
||||
"ReplaceKeyPrefixWith": {},
|
||||
"ReplaceKeyWith": {}
|
||||
}
|
||||
},
|
||||
"website.RoutingRule": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Redirect"
|
||||
],
|
||||
"properties": {
|
||||
"Condition": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/website.Condition"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Redirect": {
|
||||
"$ref": "#/components/schemas/website.Redirect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xml.IntValue": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
|
||||
+10
-2
@@ -12,7 +12,7 @@ use garage_rpc::*;
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
|
||||
use garage_api_common::{common_error::CommonError, helpers::is_default};
|
||||
use garage_api_common::{common_error::CommonError, helpers::is_default, xml};
|
||||
|
||||
use crate::api_server::{find_matching_nodes, AdminRpc, AdminRpcResponse};
|
||||
use crate::error::Error;
|
||||
@@ -857,9 +857,15 @@ pub struct GetBucketInfoResponse {
|
||||
pub global_aliases: Vec<String>,
|
||||
/// Whether website access is enabled for this bucket
|
||||
pub website_access: bool,
|
||||
#[serde(default)]
|
||||
/// Website configuration for this bucket
|
||||
pub website_config: Option<GetBucketInfoWebsiteResponse>,
|
||||
// FIXME for v3: remove serde(default) for the two fields below
|
||||
/// CORS rules for this bucket
|
||||
#[serde(default)]
|
||||
pub cors_rules: Option<Vec<xml::cors::CorsRule>>,
|
||||
/// Object lifecycle rules for this bucket
|
||||
#[serde(default)]
|
||||
pub lifecycle_rules: Option<Vec<xml::lifecycle::LifecycleRule>>,
|
||||
/// List of access keys that have permissions granted on this bucket
|
||||
pub keys: Vec<GetBucketInfoKey>,
|
||||
/// Number of objects in this bucket
|
||||
@@ -883,6 +889,8 @@ pub struct GetBucketInfoResponse {
|
||||
pub struct GetBucketInfoWebsiteResponse {
|
||||
pub index_document: String,
|
||||
pub error_document: Option<String>,
|
||||
#[serde(default)]
|
||||
pub routing_rules: Vec<xml::website::RoutingRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -18,6 +18,7 @@ use garage_model::s3::mpu_table;
|
||||
use garage_model::s3::object_table::*;
|
||||
|
||||
use garage_api_common::common_error::CommonError;
|
||||
use garage_api_common::xml;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::error::*;
|
||||
@@ -693,8 +694,24 @@ async fn bucket_info_results(
|
||||
GetBucketInfoWebsiteResponse {
|
||||
index_document: wsc.index_document,
|
||||
error_document: wsc.error_document,
|
||||
routing_rules: wsc
|
||||
.routing_rules
|
||||
.into_iter()
|
||||
.map(xml::website::RoutingRule::from_garage_routing_rule)
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}),
|
||||
cors_rules: state.cors_config.get().as_ref().map(|rules| {
|
||||
rules
|
||||
.iter()
|
||||
.map(xml::cors::CorsRule::from_garage_cors_rule)
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
lifecycle_rules: state.lifecycle_config.get().as_ref().map(|lc| {
|
||||
lc.iter()
|
||||
.map(xml::lifecycle::LifecycleRule::from_garage_lifecycle_rule)
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
keys: relevant_keys
|
||||
.into_values()
|
||||
.filter_map(|key| {
|
||||
|
||||
@@ -44,6 +44,7 @@ url.workspace = true
|
||||
quick-xml.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
opentelemetry.workspace = true
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use hyper::{header::HeaderName, Method};
|
||||
|
||||
@@ -16,7 +17,8 @@ pub struct CorsConfiguration {
|
||||
pub cors_rules: Vec<CorsRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = cors::Rule)]
|
||||
pub struct CorsRule {
|
||||
#[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<Value>,
|
||||
@@ -32,19 +34,22 @@ pub struct CorsRule {
|
||||
pub expose_headers: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = cors::AllowedMethod)]
|
||||
pub struct AllowedMethod {
|
||||
#[serde(rename = "AllowedMethod")]
|
||||
pub allowed_method: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = cors::AllowedHeader)]
|
||||
pub struct AllowedHeader {
|
||||
#[serde(rename = "AllowedHeader")]
|
||||
pub allowed_header: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = cors::ExposedHeader)]
|
||||
pub struct ExposeHeader {
|
||||
#[serde(rename = "ExposeHeader")]
|
||||
pub expose_header: Value,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use garage_model::bucket_table::{
|
||||
parse_lifecycle_date, LifecycleExpiration as GarageLifecycleExpiration,
|
||||
@@ -15,7 +16,8 @@ pub struct LifecycleConfiguration {
|
||||
pub lifecycle_rules: Vec<LifecycleRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[schema(as = lifecycle::Rule)]
|
||||
pub struct LifecycleRule {
|
||||
#[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<Value>,
|
||||
@@ -37,9 +39,13 @@ pub struct LifecycleRule {
|
||||
pub abort_incomplete_mpu: Option<AbortIncompleteMpu>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
#[derive(
|
||||
Debug, ToSchema, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default,
|
||||
)]
|
||||
#[schema(as = lifecycle::Filter)]
|
||||
pub struct Filter {
|
||||
#[serde(rename = "And", skip_serializing_if = "Option::is_none")]
|
||||
#[schema(no_recursion)]
|
||||
pub and: Option<Box<Filter>>,
|
||||
#[serde(rename = "Prefix", skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<Value>,
|
||||
@@ -52,7 +58,8 @@ pub struct Filter {
|
||||
pub size_lt: Option<IntValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[schema(as = lifecycle::Expiration)]
|
||||
pub struct Expiration {
|
||||
#[serde(rename = "Days", skip_serializing_if = "Option::is_none")]
|
||||
pub days: Option<IntValue>,
|
||||
@@ -60,7 +67,8 @@ pub struct Expiration {
|
||||
pub at_date: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[schema(as = lifecycle::AbortIncompleteMpu)]
|
||||
pub struct AbortIncompleteMpu {
|
||||
#[serde(rename = "DaysAfterInitiation")]
|
||||
pub days: IntValue,
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod lifecycle;
|
||||
pub mod website;
|
||||
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, quick_xml::se::SeError> {
|
||||
use quick_xml::se::{self, EmptyElementHandling, QuoteLevel};
|
||||
@@ -32,7 +33,8 @@ pub fn xmlns_xsi_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str("http://www.w3.org/2001/XMLSchema-instance")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = xml::Value)]
|
||||
pub struct Value(#[serde(rename = "$value")] pub String);
|
||||
|
||||
impl From<&str> for Value {
|
||||
@@ -41,5 +43,6 @@ impl From<&str> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = xml::IntValue)]
|
||||
pub struct IntValue(#[serde(rename = "$value")] pub i64);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use garage_model::bucket_table::{self, WebsiteConfig};
|
||||
use garage_model::bucket_table::{self, RoutingRule as GarageRoutingRule, WebsiteConfig};
|
||||
|
||||
use crate::common_error::CommonError as Error;
|
||||
use crate::xml::{xmlns_tag, IntValue, Value};
|
||||
@@ -35,7 +36,8 @@ impl RoutingRules {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::RoutingRule)]
|
||||
pub struct RoutingRule {
|
||||
#[serde(rename = "Condition")]
|
||||
pub condition: Option<Condition>,
|
||||
@@ -43,19 +45,22 @@ pub struct RoutingRule {
|
||||
pub redirect: Redirect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::Key)]
|
||||
pub struct Key {
|
||||
#[serde(rename = "Key")]
|
||||
pub key: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::Suffix)]
|
||||
pub struct Suffix {
|
||||
#[serde(rename = "Suffix")]
|
||||
pub suffix: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::Target)]
|
||||
pub struct Target {
|
||||
#[serde(rename = "HostName")]
|
||||
pub hostname: Value,
|
||||
@@ -63,7 +68,8 @@ pub struct Target {
|
||||
pub protocol: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::Condition)]
|
||||
pub struct Condition {
|
||||
#[serde(
|
||||
rename = "HttpErrorCodeReturnedEquals",
|
||||
@@ -74,7 +80,8 @@ pub struct Condition {
|
||||
pub prefix: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
#[schema(as = website::Redirect)]
|
||||
pub struct Redirect {
|
||||
#[serde(rename = "HostName", skip_serializing_if = "Option::is_none")]
|
||||
pub hostname: Option<Value>,
|
||||
@@ -214,6 +221,22 @@ impl RoutingRule {
|
||||
}
|
||||
self.redirect.validate()
|
||||
}
|
||||
|
||||
pub fn from_garage_routing_rule(rule: GarageRoutingRule) -> Self {
|
||||
RoutingRule {
|
||||
condition: rule.condition.map(|cond| Condition {
|
||||
http_error_code: cond.http_error_code.map(|c| IntValue(c as i64)),
|
||||
prefix: cond.prefix.map(Value),
|
||||
}),
|
||||
redirect: Redirect {
|
||||
hostname: rule.redirect.hostname.map(Value),
|
||||
http_redirect_code: Some(IntValue(rule.redirect.http_redirect_code as i64)),
|
||||
protocol: rule.redirect.protocol.map(Value),
|
||||
replace_full: rule.redirect.replace_key.map(Value),
|
||||
replace_prefix: rule.redirect.replace_key_prefix.map(Value),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
|
||||
+2
-16
@@ -9,7 +9,7 @@ use garage_api_common::xml::website::*;
|
||||
|
||||
use crate::api_server::{ReqBody, ResBody};
|
||||
use crate::error::*;
|
||||
use crate::xml::{to_xml_with_header, IntValue, Value};
|
||||
use crate::xml::{to_xml_with_header, Value};
|
||||
|
||||
pub const X_AMZ_WEBSITE_REDIRECT_LOCATION: HeaderName =
|
||||
HeaderName::from_static("x-amz-website-redirect-location");
|
||||
@@ -31,21 +31,7 @@ pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error>
|
||||
.routing_rules
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|rule| RoutingRule {
|
||||
condition: rule.condition.map(|cond| Condition {
|
||||
http_error_code: cond.http_error_code.map(|c| IntValue(c as i64)),
|
||||
prefix: cond.prefix.map(Value),
|
||||
}),
|
||||
redirect: Redirect {
|
||||
hostname: rule.redirect.hostname.map(Value),
|
||||
http_redirect_code: Some(IntValue(
|
||||
rule.redirect.http_redirect_code as i64,
|
||||
)),
|
||||
protocol: rule.redirect.protocol.map(Value),
|
||||
replace_full: rule.redirect.replace_key.map(Value),
|
||||
replace_prefix: rule.redirect.replace_key_prefix.map(Value),
|
||||
},
|
||||
})
|
||||
.map(RoutingRule::from_garage_routing_rule)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user