Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Support versioned migrations #2

Merged
merged 6 commits into from
Mar 8, 2023
Merged

Support versioned migrations #2

merged 6 commits into from
Mar 8, 2023

Conversation

eaon
Copy link
Contributor

@eaon eaon commented Oct 11, 2022

Description

Implements freedomofpress/securedrop-workstation#673, a way to ensure all migrations that would be necessary to run are executed in succession, even if there are multiple, and that at time of package install/upgrade. It is a test balloon 🎈 intended to be ported to securedrop-workstation after the next release.

This is work also goes towards Qubes OS R4.2 support, which will have its own updater policy system, which would otherwise be incompatible with our current upgrade/migrate strategy.

Note: this PR only contains the migration engine and its respective tests, it does not contain any actual migrations. This will follow in a separate PR.

Review

  • Documentation in migrations/README.md is helpful enough to start writing new migrations
  • Test coverage is adequate

Testing

  • CI successfully runs
  • make test (in a dev environment)

No dom0 testing necessary.

@eaon eaon added the enhancement New feature or request label Oct 11, 2022
@eaon eaon force-pushed the 673-versioned-migrations branch from c4be4b1 to 5ccd832 Compare October 11, 2022 22:41
@eaon eaon force-pushed the 673-versioned-migrations branch 4 times, most recently from 7e0c108 to e634727 Compare November 28, 2022 23:20
@eaon eaon force-pushed the 673-versioned-migrations branch 3 times, most recently from b724d50 to 513d683 Compare December 15, 2022 20:24
@rocodes
Copy link
Contributor

rocodes commented Dec 16, 2022

Had a meeting with @eaon about this work today, and just documenting some of our conversation here.

istm there are 3 main somewhat independent areas for review:

  • the versioning/parsing logic
  • the actual upgrade process (can we build in robust testing around any migration plans we write, to ensure that they do what is intended and don't break anything?)
  • the containerization portion and changes to rpm build logic

I feel comfortable in the first two areas, but would appreciate the additional eye of someone more familiar with packaging and building to look at the rpm build and test process (maybe a @legoktm 😄 )?

For next steps: @eaon and have set aside some time next week to pair and document test scenarios, particularly for the second point (identifying edge cases/more robust migration testing).

@eaon eaon force-pushed the 673-versioned-migrations branch 3 times, most recently from 47f1cf8 to ffdefe2 Compare December 17, 2022 00:00
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a first pass of just visual review, will poke at the packaging/rpm/etc. stuff tomorrow.

files/migrations.py Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
bootstrap/Dockerfile Outdated Show resolved Hide resolved
@eaon eaon force-pushed the 673-versioned-migrations branch from 9918505 to 4d5e043 Compare January 4, 2023 21:41
@legoktm

This comment was marked as outdated.

@legoktm

This comment was marked as outdated.

@eaon eaon force-pushed the 673-versioned-migrations branch from 61f3c14 to f292f1f Compare January 9, 2023 10:05
@eaon

This comment was marked as outdated.

@legoktm

This comment was marked as outdated.

@eaon eaon force-pushed the 673-versioned-migrations branch from 96edb7b to 1430d0f Compare January 11, 2023 19:00
Makefile Outdated Show resolved Hide resolved
.circleci/config.yml Outdated Show resolved Hide resolved
@eaon eaon force-pushed the 673-versioned-migrations branch 5 times, most recently from be0f387 to 7c3a2f3 Compare January 11, 2023 23:14
migrations/steps.py Outdated Show resolved Hide resolved
migrations/steps.py Outdated Show resolved Hide resolved
migrations/steps.py Outdated Show resolved Hide resolved
@rocodes

This comment was marked as outdated.

Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main concern about the use of inheritance is that it's just difficult to trace what is happening, given any action. You have to be aware of the entire hierarchy, and what each class does for each step.

versus something like:

def remove(path: Path):
    snapshot(path)
    if path.exists():
        try:
            path.unlink()
        except Exception as e:
            raise Rollback(e)

Yes, there will probably be some duplication because of it, but it's very straightforward to understand what's happening and in what order.

rc = None
try:
log.info(f"Running migration for {self}")
process = subprocess.Popen([str(self.path)])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are Python files, what's the advantage gained from shelling out? I know earlier in the PR these were bash scripts, but now shelling out doesn't seem necessary.

I think it would be cleaner if each migration file returned a list of steps to run, and then the main migration entrypoint ran the necessary ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question that I missed to think about when you originally commented… and you're right that's basically just organic growth, it would certainly make logging less icky as well. However I don't quite see a good way to do this yet, do you have any suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use importlib which seemed like the least worst option to continue to use the version file name convention.

return self.path.exists() == self.check_exists
return True

def snapshot(self, tmpdir):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical how useful snapshotting will be. For example, adding/moving/deleting a file will fail because of some I/O error (disk full, permissions, etc.) or a logic issue (code has the wrong name).

In the first case, if we're having I/O errors, copying or moving the tree has no better chance at success and really a lot more ways to go wrong. It could be useful in the second case, but then it seems better to have a global snapshot, rather than per-step ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently I lost a response to this message, but for posterity: as discussed in person, I agree with your view here, the implementation for simple file operations is more of a showcase, as the primary target of this functionality would be for making potential destructive changes to VMs and templates, where this functionality would be much more appropriate.

migrations/steps.py Outdated Show resolved Hide resolved
@rocodes
Copy link
Contributor

rocodes commented Feb 22, 2023

Just chatted with @eaon today about testing + documenting. I suggested either additions to the README that explain the process of writing a migration, or a separate README for this purpose in the migrations directory. I'm happy to help write up any docs that need writing.

My understanding of the main points (to get this readme/explainer started/make sure I understand correctly) is:

  • To write a migration, create a new python file in the migration directory, $version.py, where $version is correct semver. In this file, include an ordered list of steps ([MigrationStep]).
  • Use existing migration steps for commonly-used actions such as remove, rename, etc
  • If a custom action is needed, build it by inheriting from MigrationStep. Implement at minimum, the run method, but also optionally other methods as shown in migration_steps.py. (We should include advice on if/when writing rollback is useful)
  • Error-handling is managed already, so the run method does not need to implement error handling. The premise of the current error handling is try migration -> if error, roll back and quit. (The migrate method should have a comment and the approach to error handling should be documented)
  • If writing any additional MigrationSteps, add unit tests
  • We should also add a quick "When do I write a migration?" FAQ

lmk if you think I'm missing any key info!

@eaon
Copy link
Contributor Author

eaon commented Feb 23, 2023

That's pretty much it! Small nits:

  • There's not just error handling for run, but for all the steps (I even added one for validation now even though that seems a bit overkill 😜)
  • Per-migration custom MigrationSteps that are specific to system state won't support unit tests because we don't have an automated install/testing infrastructure that would support that. So add unit tests please only applies to generic solutions that would get added to migration_steps.py
  • As for when to write a migration, I don't think there's more to it than we need the system state to be different than it was before.

@eaon eaon force-pushed the 673-versioned-migrations branch 2 times, most recently from 9348da7 to e8746e3 Compare February 27, 2023 13:38
@eaon eaon marked this pull request as ready for review February 27, 2023 13:57
eaon added 3 commits February 27, 2023 16:58
This is a test-balloon for `securedrop-workstation`, where we need
complex migrations that don't rely on having to install every package
incrementally, but allows for upgrading to the newest package directly.

When the package is upgraded, a version number of the previous installed
version is read and migrations that form the delta between the target
version are run in succession.

To allow the use of this pattern immediately, the version number is also
checked on installation, letting us deploy the package with a`%post`
scriptlet that ensures an "initial migration" is run.

`%post` scriptlets are always expected to run successfully, and even if
scriplets fail, the package installation isn't marked as failed. Hence,
later down the line we'll need logic in the updater itself to check for
the expected version, and respond appropriately if a migration failed.
@eaon eaon force-pushed the 673-versioned-migrations branch from e8746e3 to cab66d9 Compare February 27, 2023 15:59
@eaon eaon requested a review from legoktm February 28, 2023 19:24
@eaon eaon mentioned this pull request Mar 1, 2023
5 tasks
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK! I hope the number of comments isn't overwhelming, like half of them are pretty minor/trivial issues.

I did not yet get to reviewing the test framework and the tests themselves, will try to do that tomorrow.

return self.__repr__()


def path_remove(path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typically I'd name functions as verb-noun, so remove_path, validate_path, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aye, having a style guide for that sort of thing might be good 😄 My approach to this is typically to start names of things with the part that is shared, sort of sharedbase_variant[_detail] etc. Not sure if there's a name for that though. Totally open to renaming them

files/migration_steps.py Show resolved Hide resolved
files/migration_steps.py Show resolved Hide resolved
files/migration_steps.py Show resolved Hide resolved
files/migration_steps.py Outdated Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
migrations/README.md Show resolved Hide resolved
rpm-build/SPECS/securedrop-updater.spec Outdated Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
files/migrations.py Outdated Show resolved Hide resolved
- Added all the type hints I think
- Removed subclassing for Migration
- Added some docstrings
- Removed unidiomatic uses of exit
- Move per-step tmpdir customisation to MigrationSteps tmpdir method
- Don't ship migration directory's README.md
- Replace error f-strings with logging.exception
@eaon eaon force-pushed the 673-versioned-migrations branch from 7aa739e to 144afc4 Compare March 3, 2023 20:15
RPM's %files macro's -f argument is apparently incompatible with
reproducible builds, so we need to be explicit about what is (not)
included when

Also, RPM spec linter is set to disallow macro expansion in comments, so
instruct developers to account for this when uncommenting.
@eaon eaon force-pushed the 673-versioned-migrations branch from 144afc4 to 3666984 Compare March 3, 2023 20:33
@eaon eaon requested a review from legoktm March 3, 2023 20:35
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the tests look good, the main remaining issue is the types being passed to the MigrationStep constructors being Path or str.

Also a general nit is that the read_text()/write_text() functions are nicer than .open.read() and .open("w").write() :-)

tests/test_migrations.py Outdated Show resolved Hide resolved
tests/test_migrations.py Outdated Show resolved Hide resolved
tests/migrations/success-if-valid.py Outdated Show resolved Hide resolved
tests/migrations/rollback.py Outdated Show resolved Hide resolved
tests/migrations/rollback.py Show resolved Hide resolved
tests/test_migrations.py Outdated Show resolved Hide resolved
tests/test_migrations.py Outdated Show resolved Hide resolved
tests/test_migrations.py Outdated Show resolved Hide resolved
tests/migrations/failure.py Outdated Show resolved Hide resolved
tests/conftest.py Outdated Show resolved Hide resolved
@eaon eaon requested a review from legoktm March 7, 2023 13:15
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One very minor documentation update is needed, otherwise it looks ready to me!!

files/migrations.py Show resolved Hide resolved
- More (and better) documentation for tests
- Consistent API for file migration steps
- Moved migration test infrastructure from fixtures to utils
- Changed return codes to be more informative
- Use pathlib's write_text/read_text where possible
- Move validation exception from failure.py to its own migration test
- Use unique class names for migration tests
- Slight improvements for a variety of tests
@eaon eaon force-pushed the 673-versioned-migrations branch from 9f4cb6d to f7c52a5 Compare March 7, 2023 19:01
@eaon eaon requested a review from legoktm March 7, 2023 19:04
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work!

@legoktm legoktm merged commit 69662b9 into main Mar 8, 2023
@legoktm legoktm deleted the 673-versioned-migrations branch March 8, 2023 17:30
cfm added a commit to freedomofpress/securedrop-workstation that referenced this pull request Apr 10, 2024
This is equivalent to merging in
freedomofpress/securedrop-updater@803c329b8a7323053d3345f3cdf6efc9a6b0308b7
without freedomofpress/securedrop-updater#2 and
freedomofpress/securedrop-updater#32.

Since freedomofpress/securedrop-updater originally diverged from
freedomofpress/securedrop-workstation, squashing the merge avoids
duplicating that shared history.

commit c6179d5
Merge: f73b6bf 803c329
Author: Cory Francis Myers <cory@freedom.press>
Date:   Tue Apr 9 15:38:41 2024 -0700

    Merge branch 'no-migrations' into reabsorb-updater-no-migrations

commit 803c329b8a7323053d3345f3cdf6efc9a6b0308b7
Author: Michael Z <michael@freedom.press>
Date:   Mon Mar 13 16:30:08 2023 +0100

    Catch up with securedrop-workstation#844

    Open SD client with the desktop file introduced in
    securedrop-client#1600

...
cfm added a commit to freedomofpress/securedrop-workstation that referenced this pull request Apr 10, 2024
This is equivalent to merging in
freedomofpress/securedrop-updater@e3930ae
without freedomofpress/securedrop-updater#2 and
freedomofpress/securedrop-updater#32.

Since freedomofpress/securedrop-updater originally diverged from
freedomofpress/securedrop-workstation, squashing the merge avoids
duplicating that shared history.

commit c6179d5
Merge: f73b6bf 803c329
Author: Cory Francis Myers <cory@freedom.press>
Date:   Tue Apr 9 15:38:41 2024 -0700

    Merge branch 'no-migrations' into reabsorb-updater-no-migrations

commit 803c329b8a7323053d3345f3cdf6efc9a6b0308b7
Author: Michael Z <michael@freedom.press>
Date:   Mon Mar 13 16:30:08 2023 +0100

    Catch up with securedrop-workstation#844

    Open SD client with the desktop file introduced in
    securedrop-client#1600

...
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants