Skip to content
Open
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
138 changes: 120 additions & 18 deletions tools/cli_commands/erv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,34 +537,46 @@ def commit(self, source: TerraformCli) -> None:
self.upload_state()
source.upload_state()

def _elasticache_import_password(
self, destination: TfResource, password: str
def _import_random_password(
self, destination: TfResource, password: str, provider: str
) -> None:
"""Import the elasticache auth_token random_password."""
"""Import a password into a random_password terraform resource."""
self._tf_import(address=destination.address, value=password)

if (
not destination.change
or not destination.change.after
or not destination.change.after.get("override_special")
):
# nothing to change, nothing to do
if not destination.change or not destination.change.after:
return

if provider == "rds":
if not destination.change.after.get("keepers"):
return
elif not destination.change.after.get("override_special"):
return

state_data = json.loads(self.state_file.read_text())
state_data["serial"] += 1
if self._dry_run:
# in dry-run mode, tf_import is a no-op, therefore, the password is not in the state yet.
return

state_data = json.loads(self.state_file.read_text())
state_data["serial"] += 1
# Strip index suffix (e.g. "this[0]" -> "this") since the state
# stores the name without the index.
resource_name = destination.id.split("[")[0]
state_password_obj = next(
r for r in state_data["resources"] if r["name"] == destination.id
r
for r in state_data["resources"]
if r["type"] == destination.type and r["name"] == resource_name
)

# Set the "override_special" to disable the password reset
state_password_obj["instances"][0]["attributes"]["override_special"] = (
destination.change.after["override_special"]
)
# Write the state,
# Align state attributes with the ERv2 random_password resource
# configuration to prevent replacement on the next plan.
attrs = state_password_obj["instances"][0]["attributes"]
if provider == "rds":
attrs["keepers"] = destination.change.after["keepers"]
attrs["special"] = destination.change.after.get("special", False)
attrs["min_numeric"] = destination.change.after.get("min_numeric", 0)
else:
attrs["override_special"] = destination.change.after["override_special"]

self.state_file.write_text(json.dumps(state_data, indent=2))

def migrate_elasticache_resources(self, source: TerraformCli) -> None:
Expand All @@ -584,9 +596,10 @@ def migrate_elasticache_resources(self, source: TerraformCli) -> None:
current_auth_token = source_ec.change.before.get("auth_token")
if current_auth_token:
with suppress(KeyError):
self._elasticache_import_password(
self._import_random_password(
destination_resources.get_resource_by_type("random_password"),
current_auth_token,
"",
)

# migrate resources
Expand All @@ -607,6 +620,95 @@ def migrate_elasticache_resources(self, source: TerraformCli) -> None:
)
self.commit(source)

def migrate_rds_resources(self, source: TerraformCli) -> None:
"""Migrate RDS resources from terraform-resources to ERv2.

The random_password resource doesn't exist in terraform-resources
(the password was generated in Python). We extract the current
password from the source aws_db_instance state and import it into
the destination's random_password resource.
"""
source_resources = source.resource_changes(TfAction.DESTROY)
destination_resources = self.resource_changes(TfAction.CREATE)
if not source_resources or not destination_resources:
raise ValueError("No resource changes found!")

# Extract the current password from the source aws_db_instance
source_rds = source_resources.get_resource_by_type("aws_db_instance")
if not source_rds.change or not source_rds.change.before:
raise ValueError("Something went wrong with the source RDS instance!")

current_password = source_rds.change.before.get("password")
if current_password:
with suppress(KeyError):
self._import_random_password(
destination_resources.get_resource_by_type("random_password"),
current_password,
"rds",
)

# migrate resources, skipping random_password (handled above)
movable_destinations = [
r
for r in destination_resources
if not (current_password and r.type == "random_password")
]

if len(source_resources) != len(movable_destinations):
with pause_progress_spinner(self.progress_spinner):
rich_print(
"[b red]The number of changes (ERv2 vs terraform-resource) does not match! Please review them carefully![/]"
)
rich_print("ERv2:")
rich_print(
"\n".join([
f" {i}: {r.address}"
for i, r in enumerate(movable_destinations, start=1)
])
)
rich_print("Terraform:")
rich_print(
"\n".join([
f" {i}: {r.address}"
for i, r in enumerate(source_resources, start=1)
])
)
if not Confirm.ask("Would you like to continue anyway?", default=False):
return

for destination_resource in movable_destinations:
possible_source_resouces = source_resources[destination_resource]
if not possible_source_resouces:
raise ValueError(
f"Source resource for {destination_resource} not found!"
)
elif len(possible_source_resouces) == 1:
source_resource = possible_source_resouces[0]
else:
with pause_progress_spinner(self.progress_spinner):
rich_print(
f"[b red]{destination_resource.address} not found![/] Please select the related source ID manually!"
)
for i, r in enumerate(possible_source_resouces, start=1):
print(f"{i}: {r.address}")

index = IntPrompt.ask(
":boom: Enter the number",
choices=[
str(i) for i in range(1, len(possible_source_resouces) + 1)
],
show_choices=False,
)
source_resource = possible_source_resouces[index - 1]

self.move_resource(
source_state_file=source.state_file,
source=source_resource,
destination=destination_resource,
)

self.commit(source)

def migrate_resources(self, source: TerraformCli) -> None:
"""Migrate the resources from source."""
# if not self.initialized or not source.initialized:
Expand Down
6 changes: 2 additions & 4 deletions tools/qontract_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4440,9 +4440,6 @@ def migrate(ctx: click.Context, dry_run: bool, skip_build: bool) -> None:

E.g: qontract-reconcile --config=<config> external-resources migrate aws app-sre-stage rds dashdotdb-stage
"""
if ctx.obj["provider"] == "rds":
# The "random_password" is not an AWS resource. It's just in the outputs and can't be migrated :(
raise NotImplementedError("RDS migration is not supported yet!")

if not Confirm.ask(
dedent("""
Expand Down Expand Up @@ -4529,8 +4526,9 @@ def migrate(ctx: click.Context, dry_run: bool, skip_build: bool) -> None:
"Migrating the resources from terraform-resources to ERv2",
):
if ctx.obj["provider"] == "elasticache":
# Elasticache migration is a bit different
erv2_tf_cli.migrate_elasticache_resources(source=tfr_tf_cli)
elif ctx.obj["provider"] == "rds":
erv2_tf_cli.migrate_rds_resources(source=tfr_tf_cli)
else:
erv2_tf_cli.migrate_resources(source=tfr_tf_cli)

Expand Down