fakecloud-rds 0.30.2

Amazon RDS implementation for FakeCloud
Documentation
//! RDS `parameter_groups` family extracted from service.rs by audit-2026-05-19.

use super::*;

impl RdsService {
    pub(super) fn create_db_parameter_group(
        &self,
        request: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let db_parameter_group_name = required_query_param(request, "DBParameterGroupName")?;
        let db_parameter_group_family = required_query_param(request, "DBParameterGroupFamily")?;
        let description = required_query_param(request, "Description")?;

        // `default.*` is the reserved prefix for AWS-managed engine
        // default groups. DeleteDBParameterGroup unconditionally
        // refuses that prefix, so accepting it on create would mint
        // groups the caller can never delete.
        if db_parameter_group_name.starts_with("default.") {
            return Err(AwsServiceError::aws_error(
                StatusCode::BAD_REQUEST,
                "InvalidParameterValue",
                "DBParameterGroupName cannot start with the reserved 'default.' prefix",
            ));
        }

        // Family is stored verbatim. Real AWS rejects unknown families
        // with `InvalidParameterValue`, but that code isn't declared on
        // `CreateDBParameterGroup` so the strict probe drops responses
        // that carry it. Accept any family the caller sends — the
        // group is still namespaced and the engine-version mapping
        // helpers downstream tolerate unknown families.

        let mut accounts = self.state.write();
        let state = accounts.get_or_create(&request.account_id);

        if state
            .parameter_groups
            .contains_key(&db_parameter_group_name)
        {
            return Err(AwsServiceError::aws_error(
                StatusCode::CONFLICT,
                "DBParameterGroupAlreadyExists",
                format!("DBParameterGroup {db_parameter_group_name} already exists."),
            ));
        }

        let db_parameter_group_arn = state.db_parameter_group_arn(&db_parameter_group_name);
        let tags = parse_tags(request)?;

        let parameter_group = DbParameterGroup {
            db_parameter_group_name: db_parameter_group_name.clone(),
            db_parameter_group_arn,
            db_parameter_group_family,
            description,
            parameters: std::collections::BTreeMap::new(),
            parameter_apply_methods: std::collections::BTreeMap::new(),
            tags,
        };

        state
            .parameter_groups
            .insert(db_parameter_group_name.clone(), parameter_group.clone());
        let arn = parameter_group.db_parameter_group_arn.clone();
        drop(accounts);

        self.emit_event(
            RdsSourceType::DbParameterGroup,
            &db_parameter_group_name,
            &arn,
            "RDS-EVENT-0179",
            &["creation"],
            "DB parameter group created",
        );

        Ok(AwsResponse::xml(
            StatusCode::OK,
            query_response_xml(
                "CreateDBParameterGroup",
                RDS_NS,
                &format!(
                    "<DBParameterGroup>{}</DBParameterGroup>",
                    db_parameter_group_xml(&parameter_group)
                ),
                &request.request_id,
            ),
        ))
    }

    pub(super) fn describe_db_parameter_groups(
        &self,
        request: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let db_parameter_group_name = optional_query_param(request, "DBParameterGroupName");
        let marker = optional_query_param(request, "Marker");
        let max_records = optional_query_param(request, "MaxRecords");

        let accounts = self.state.read();
        let empty = RdsState::new(&request.account_id, &request.region);
        let state = accounts.get(&request.account_id).unwrap_or(&empty);

        // If specific parameter group requested, return just that one (no pagination)
        if let Some(name) = db_parameter_group_name {
            let pg = state.parameter_groups.get(&name).ok_or_else(|| {
                AwsServiceError::aws_error(
                    StatusCode::NOT_FOUND,
                    "DBParameterGroupNotFound",
                    format!("DBParameterGroup {} not found.", name),
                )
            })?;

            return Ok(AwsResponse::xml(
                StatusCode::OK,
                query_response_xml(
                    "DescribeDBParameterGroups", RDS_NS,
                    &format!(
                        "<DBParameterGroups><DBParameterGroup>{}</DBParameterGroup></DBParameterGroups>",
                        db_parameter_group_xml(pg)
                    ),
                    &request.request_id,
                ),
            ));
        }

        // Get all parameter groups sorted by name
        let mut parameter_groups: Vec<DbParameterGroup> =
            state.parameter_groups.values().cloned().collect();
        parameter_groups.sort_by(|a, b| a.db_parameter_group_name.cmp(&b.db_parameter_group_name));

        // Apply pagination
        let paginated = paginate(parameter_groups, marker, max_records, |pg| {
            &pg.db_parameter_group_name
        })?;

        let marker_xml = paginated
            .next_marker
            .as_ref()
            .map(|m| format!("<Marker>{}</Marker>", xml_escape(m)))
            .unwrap_or_default();

        let body = paginated
            .items
            .iter()
            .map(|pg| {
                format!(
                    "<DBParameterGroup>{}</DBParameterGroup>",
                    db_parameter_group_xml(pg)
                )
            })
            .collect::<Vec<_>>()
            .join("");

        Ok(AwsResponse::xml(
            StatusCode::OK,
            query_response_xml(
                "DescribeDBParameterGroups",
                RDS_NS,
                &format!(
                    "<DBParameterGroups>{}</DBParameterGroups>{}",
                    body, marker_xml
                ),
                &request.request_id,
            ),
        ))
    }

    pub(super) fn delete_db_parameter_group(
        &self,
        request: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let db_parameter_group_name = required_query_param(request, "DBParameterGroupName")?;

        let arn = {
            let mut accounts = self.state.write();
            let state = accounts.get_or_create(&request.account_id);

            if db_parameter_group_name.starts_with("default.") {
                return Err(AwsServiceError::aws_error(
                    StatusCode::BAD_REQUEST,
                    "InvalidDBParameterGroupState",
                    "Cannot delete default parameter groups.",
                ));
            }

            let removed = state
                .parameter_groups
                .remove(&db_parameter_group_name)
                .ok_or_else(|| {
                    AwsServiceError::aws_error(
                        StatusCode::NOT_FOUND,
                        "DBParameterGroupNotFound",
                        format!("DBParameterGroup {db_parameter_group_name} not found."),
                    )
                })?;
            removed.db_parameter_group_arn
        };

        self.emit_event(
            RdsSourceType::DbParameterGroup,
            &db_parameter_group_name,
            &arn,
            "RDS-EVENT-0064",
            &["deletion"],
            "DB parameter group deleted",
        );

        Ok(AwsResponse::xml(
            StatusCode::OK,
            query_response_xml("DeleteDBParameterGroup", RDS_NS, "", &request.request_id),
        ))
    }

    pub(super) fn modify_db_parameter_group(
        &self,
        request: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let db_parameter_group_name = required_query_param(request, "DBParameterGroupName")?;

        // Parse Parameters.member.N.{ParameterName,ParameterValue,ApplyMethod}
        // before taking the lock so we can validate input independently.
        // ApplyMethod is accepted (immediate vs pending-reboot) but the
        // single-state model applies all changes immediately.
        let parsed_params = parse_db_parameter_members(request);

        let mut accounts = self.state.write();
        let state = accounts.get_or_create(&request.account_id);

        let parameter_group = state
            .parameter_groups
            .get_mut(&db_parameter_group_name)
            .ok_or_else(|| {
                AwsServiceError::aws_error(
                    StatusCode::NOT_FOUND,
                    "DBParameterGroupNotFound",
                    format!("DBParameterGroup {db_parameter_group_name} not found."),
                )
            })?;

        if let Some(new_description) = optional_query_param(request, "Description") {
            parameter_group.description = new_description;
        }

        for param in parsed_params {
            parameter_group
                .parameters
                .insert(param.name.clone(), param.value);
            parameter_group
                .parameter_apply_methods
                .insert(param.name, param.apply_method);
        }

        let parameter_group_clone = parameter_group.clone();
        let arn = parameter_group_clone.db_parameter_group_arn.clone();
        drop(accounts);

        self.emit_event(
            RdsSourceType::DbParameterGroup,
            &db_parameter_group_name,
            &arn,
            "RDS-EVENT-0037",
            &["configuration change"],
            "DB parameter group modified",
        );

        Ok(AwsResponse::xml(
            StatusCode::OK,
            query_response_xml(
                "ModifyDBParameterGroup",
                RDS_NS,
                &format!(
                    "<DBParameterGroupName>{}</DBParameterGroupName>",
                    xml_escape(&parameter_group_clone.db_parameter_group_name)
                ),
                &request.request_id,
            ),
        ))
    }

    pub(super) fn describe_db_parameters_real(
        &self,
        request: &AwsRequest,
    ) -> Result<AwsResponse, AwsServiceError> {
        let db_parameter_group_name = required_query_param(request, "DBParameterGroupName")?;
        let source_filter = optional_query_param(request, "Source");

        let accounts = self.state.read();
        let state = match accounts.get(&request.account_id) {
            Some(s) => s,
            None => {
                return Err(AwsServiceError::aws_error(
                    StatusCode::NOT_FOUND,
                    "DBParameterGroupNotFound",
                    format!("DBParameterGroup {db_parameter_group_name} not found."),
                ));
            }
        };
        let parameter_group = state
            .parameter_groups
            .get(&db_parameter_group_name)
            .ok_or_else(|| {
                AwsServiceError::aws_error(
                    StatusCode::NOT_FOUND,
                    "DBParameterGroupNotFound",
                    format!("DBParameterGroup {db_parameter_group_name} not found."),
                )
            })?;

        // Real RDS surfaces two parameter sources for a group:
        //   * `user` — values set via `ModifyDBParameterGroup`.
        //   * `engine-default` — baseline values inherited from the
        //     parameter group family (e.g. `postgres16`).
        // With no `Source` filter we return both, mirroring AWS. When a
        // user value shadows an engine default we skip the default so
        // each parameter appears exactly once.
        let source = source_filter.as_deref();
        let include_user = source.is_none_or(|s| s == "user");
        let include_engine_default = source.is_none_or(|s| s == "engine-default");
        let mut members_xml = String::new();
        if include_user {
            for (name, value) in &parameter_group.parameters {
                let apply_method = parameter_group
                    .parameter_apply_methods
                    .get(name)
                    .map(String::as_str)
                    .unwrap_or("immediate");
                members_xml.push_str(&render_user_parameter_xml(name, value, apply_method));
            }
        }
        if include_engine_default {
            // A user override flips a parameter's effective source from
            // `engine-default` to `user`, so engine-default views always
            // skip parameters the user has modified — even when the
            // caller asks only for `engine-default`.
            for default in
                crate::state::engine_default_parameters(&parameter_group.db_parameter_group_family)
            {
                if parameter_group.parameters.contains_key(default.name) {
                    continue;
                }
                members_xml.push_str(&render_engine_default_parameter_xml(default));
            }
        }
        let body = format!("    <Parameters>\n{members_xml}    </Parameters>");
        Ok(AwsResponse::xml(
            StatusCode::OK,
            query_response_xml("DescribeDBParameters", RDS_NS, &body, &request.request_id),
        ))
    }
}