Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
"snapcraft validate" and "snapcraft gated" commands #813
Conversation
| @@ -49,7 +49,7 @@ python3 -m coverage && coverage="true" | ||
| run_static_tests(){ | ||
| SRC_PATHS="bin snapcraft integration_tests snaps_tests" | ||
| - python3 -m flake8 --max-complexity=10 $SRC_PATHS | ||
| + python3 -m flake8 --max-complexity=12 $SRC_PATHS |
ralsina
Sep 21, 2016
Contributor
Sorry, that was just temporary while I figure out how to make main have less than a 12 complexity with 12 commands, adding a XXX comment :-)
ralsina
Sep 22, 2016
Contributor
Done by adding a specific flake8 comment to disable this check in the function that was failing.
Rationale is: I can make the complexity "go away" by doing things like a dictionary dispatch but IRL it's replacing a straightforward if/elif chain with something much less obvious.
| + return details | ||
| + | ||
| + | ||
| +def gated(snap_name): |
ralsina
Sep 21, 2016
Contributor
I was going with the convention of functions being called the same as the command. If you guys like that better, no problem for me.
| + | ||
| +def _get_snap_data(name, store): | ||
| + search_results = store.cpi.search_package(name, 'stable', None) | ||
| + if search_results is None: |
sergiusens
Sep 21, 2016
Collaborator
is there a scenario where another zero value would make if not search_results not a desirable way to write this?
ralsina
Sep 21, 2016
Contributor
No such scenario. I sorta like this better since None is what the called function returns, but am ok with changing it :-)
| + assertion['valid'] = "false" | ||
| + | ||
| + cmdline = ['snap', 'sign'] | ||
| + if key is not None: |
sergiusens
Sep 21, 2016
Collaborator
if key: reads better here, is there a possibility for it to be a zero value?
ralsina
Sep 21, 2016
Contributor
There is a remote chance of someone doing something like --key='' I guess but that ends up being None. Changing it, since empty strings are invalid key names anyway.
| + try: | ||
| + response_json = response.json() | ||
| + except (AttributeError, JSONDecodeError): | ||
| + raise errors.StoreValidationError(snap_id, response) |
ralsina
Sep 21, 2016
Contributor
Honestly, not very good, like
Expecting value: line 1 column 1 (char 0)
I'd change it to a generic "Invalid response from server" maybe?
sergiusens
Sep 21, 2016
Collaborator
Yes please, the json decode error is just not friendly for a user.
What would help with debugging this later is to add a logger.debug line with response somewhere in the message. Like logger.debug('Invalid response from the server when doing blah: {} {}'.format(response.code, response.text))
| + store = storeapi.StoreClient() | ||
| + # Get data for the gating snap | ||
| + with _requires_login(): | ||
| + snap_data = _get_snap_data(snap_name, store) |
cprov
Sep 23, 2016
Contributor
@ralsina let's drop this search-based query by the new snap-details endpoint caller:
store.cpi.get_package(self, snap_name, channel, arch):
https://github.com/snapcore/snapcraft/blob/master/snapcraft/storeapi/__init__.py#L291
| @@ -186,3 +186,27 @@ def update_name_and_version(self, project_dir, name=None, version=None): | ||
| else: | ||
| print(line) | ||
| return updated_project_dir | ||
| + | ||
| + def gated(self, snap_name, expected_validations, expected_error=None): |
cprov
Sep 23, 2016
Contributor
Roberto, expected_validations has to be optional as well, otherwise you end up passing a and expected result that is completely irrelevant in the testing scenario.
| + self.assertEqual(1, self.gated('ubuntu-core', [ | ||
| + ('snap-1', '3'), | ||
| + ('snap-2', '5'), | ||
| + ], expected_error='Have you run "snapcraft login')) |
cprov
Sep 23, 2016
Contributor
Like this call, the expected_validations is not relevant here, only the expected_error
| @@ -0,0 +1,80 @@ | ||
| +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- |
cprov
Sep 23, 2016
Contributor
I think snapcraft guidelines enforce having individual integration test files for each command. Keeping it this way is simpler than arguing for a guidelines change (small boilerplate)
| + | ||
| +def _get_snap_data(name, store): | ||
| + try: | ||
| + details = store.cpi.get_package(name, 'stable', None) |
cprov
Sep 23, 2016
Contributor
get_package() is not prepared for receiving arch=None, it's pushing a bogus header for CPI waiting for a disaster to happen ... arch is not important for snap-id lookups, so adjust get_package to suppress 'X-Ubuntu-Architecture' is arch is None (similar tweak on errors.SnapNotFound)
Also, I don't see much value in this wrapper if SnapNotFound message is sane, no need to turn it into RuntimeError, 2 different things.
| + snap_data = _get_snap_data(snap_name, store) | ||
| + release = str(snap_data['release'][0]) | ||
| + snap_id = snap_data['snap_id'] | ||
| + developer_id = snap_data['developer_id'] |
cprov
Sep 23, 2016
Contributor
"developer_id" will be the account-id of the uploader of the latest revision available in stable channel, because of package-collaborating (sharing) it might not be the "publisher", there is no "publisher_id" in CPI. More importantly, it's not the currently authenticated "account-id", so I think it will be less ambiguous (little more expensive) if you simply fetch account-info.account-id.
| @@ -296,7 +305,8 @@ def get_package(self, snap_name, channel, arch): | ||
| } | ||
| params = { | ||
| 'channel': channel, | ||
| - 'fields': 'status,download_url,download_sha512', | ||
| + 'fields': 'status,download_url,download_sha512,snap_id,' | ||
| + 'developer_id,release', |
cprov
Sep 23, 2016
Contributor
as commented above, "developer_id" is not what you are looking for for validation assertions, it's always signed by the publisher.
ralsina
changed the title from
[WIP] "snapcraft validate" and "snapcraft gated" commands
to
"snapcraft validate" and "snapcraft gated" commands
Sep 27, 2016
sergiusens
reviewed
Sep 27, 2016
Thanks, looks generally good, but for a better review I need to sit down and read the design docs for validate and gated. Will get back to you again
| except storeapi.errors.SHAMismatchError: | ||
| raise RuntimeError( | ||
| 'Failed to download {} at {} (mismatched SHA)'.format( | ||
| snap_name, download_path)) | ||
| + | ||
| + | ||
| +def _get_snap_data(name, store): |
ralsina
Sep 27, 2016
•
Contributor
Just that it was common code between gated and validate. It used to be more complicated, it is now too trivial not to inline, will change it.
| @@ -37,10 +37,14 @@ class StoreError(SnapcraftError): | ||
| class SnapNotFoundError(StoreError): | ||
| - fmt = 'The "{name}" for {arch} was not found in {channel}.' | ||
| + fmt = 'Snap "{name}" for {arch} cannot be found in the {channel} channel.' |
| + if arch: | ||
| + super().__init__(name=name, channel=channel, arch=arch) | ||
| + else: | ||
| + self.fmt = 'Snap {name} was not found in the {channel} channel.' |
cprov
suggested changes
Sep 27, 2016
Looks good, few points to be improved inline.
Could you, please, squash the ~50 commits to make the PR cleaner.
| +class GatedTestCase(integration_tests.StoreTestCase): | ||
| + | ||
| + def setUp(self): | ||
| + if os.getenv('TEST_STORE') in ['staging', 'production']: |
cprov
Sep 27, 2016
Contributor
The check below seems to be more effective and the pattern used in other places.
if os.getenv('TEST_STORE', 'fake') != 'fake':
| + self.assertEqual(0, self.gated('ubuntu-core', [ | ||
| + ('snap-1', '3'), | ||
| + ('snap-2', '5'), | ||
| + ])) |
cprov
Sep 27, 2016
•
Contributor
this indentation is a little odd ... What about the following:
validations = [('snap-1', '3'), ('snap-2', '5')]
exit_code = self.gated('ubuntu-code', validations)
self.assertEqual(0, exit_code)
(repeated in several tests below)
| - raise RuntimeError( | ||
| - 'Snap {name} for {arch} cannot be found' | ||
| - ' in the {channel} channel'.format(name=snap_name, arch=arch, | ||
| - channel=channel)) |
cprov
Sep 27, 2016
Contributor
this code is gone because SnapNotFoundError will be raised to top-level exception handler with equivalent info, right ?
| + | ||
| +def _get_snap_data(name, store): | ||
| + details = store.cpi.get_package(name, 'stable', None) | ||
| + return details |
cprov
Sep 27, 2016
Contributor
I don't understand what this function buys us. Can't callsites call store.cpi.get_package() directly ?
Also, you've made "arch" optional, no need to pass None.
ralsina
Sep 27, 2016
Contributor
Well, it was more complicated before :-) I agree it has become a bit too simple and can just be inlined.
| @@ -296,7 +305,8 @@ def get_package(self, snap_name, channel, arch): | ||
| } | ||
| params = { | ||
| 'channel': channel, | ||
| - 'fields': 'status,download_url,download_sha512', | ||
| + 'fields': 'status,download_url,download_sha512,snap_id,' | ||
| + 'developer_id,release', |
cprov
Sep 23, 2016
Contributor
as commented above, "developer_id" is not what you are looking for for validation assertions, it's always signed by the publisher.
| + message = ('Invalid response from the server when pushing ' | ||
| + 'validations: {} {}').format( | ||
| + response.status_code, response) | ||
| + logger.debug(message) |
cprov
Sep 27, 2016
Contributor
this is hardly a debug message, it's an error. What about making sure the raised exception has enough information to guide the user and that's it.
If our servers are not returning proper JSON and we don't know already ... it's more like our problem.
ralsina
Sep 27, 2016
Contributor
This error usually will mean something like "we are getting HTML instead of JSON because we are pointing to the wrong URL" and the error message will say "Bad response". I think this was suggested by @sergiusens in the previous review, happy to change it if he's ok with it.
sergiusens
Sep 29, 2016
Collaborator
@cprov I need this for the odd case of having issues in production where I can just tell people to add --debug :-)
| + try: | ||
| + response_json = response.json() | ||
| + # XXX This error is insanely verbose, SCA needs to trim it down | ||
| + response_json['text'] = response.json()['error_list'][0]['message'] |
cprov
Sep 27, 2016
Contributor
mitigated by https://code.launchpad.net/~cprov/software-center-agent/sign-build-errors/+merge/306939
| + | ||
| + expected_output = """Name Approved | ||
| +snap-1 3 | ||
| +snap-2 5""" |
| install: | ||
| - sudo apt-get -qq update | ||
| - sudo apt-get install -y python3-coverage | ||
| script: | ||
| - - docker run -e TEST_USER_PASSWORD=$TEST_USER_PASSWORD -v $(pwd):$(pwd) -t ubuntu:xenial sh -c "export LC_ALL=en_US.UTF-8 && locale-gen en_US.UTF-8 && apt update && cd $(pwd) && $DEPENDENCIES && ./runtests.sh $TEST_SUITE" | ||
| + - docker run -e TEST_USER_EMAIL=$TEST_USER_EMAIL -e TEST_USER_PASSWORD=$TEST_USER_PASSWORD -e TEST_STORE=$TEST_STORE -v $(pwd):$(pwd) -t ubuntu:xenial sh -c "export LC_ALL=en_US.UTF-8 && locale-gen en_US.UTF-8 && apt update && cd $(pwd) && $DEPENDENCIES && ./runtests.sh $TEST_SUITE" | ||
| after_success: | ||
| - pip install coveralls | ||
| - coveralls |
ralsina
Sep 27, 2016
•
Contributor
I did something weird trying to collapse the commits I guess. I'd revert this one, but it's the same as master, right? I have no idea what to do about it :-)
| + def setUp(self): | ||
| + if os.getenv('TEST_STORE') != 'fake': | ||
| + self.skipTest('Right combination of snaps and IDs is not ' | ||
| + 'available in real stores.') |
elopio
Sep 27, 2016
Member
So, we will never be able to test this against a real system? There is no way to seed some valid values there?
ralsina
Sep 27, 2016
•
Contributor
The main problem is that you need the logged in user to match against the publisher of several snaps in the store. I suppose we can make it work by creating a user account, uploading snaps for it and sharing the password between testers? (and also making the name of those snaps match the ones used internally on the tests... one is 'ubuntu-core' at this point)
elopio
Sep 29, 2016
Member
Yes, that sounds "good" to me. We have u1test+snapcraft@canonical.com, I can give you the password.
| + with open(fname, 'wb') as f: | ||
| + f.write(assertion) | ||
| + | ||
| + store.push_validation(snap_id, assertion) |
elopio
Sep 27, 2016
Member
This function is too long, can you split it? You are splitting it with comments, it seems it could be nice to have one smaller function per comment.
ralsina
Sep 28, 2016
Contributor
I split it in 3, there is a largish chunk in the loop that's hard to split without writing functions with many args, which are a separate readability problem. Let me know if you like the current split or want me to split finer.
| @@ -41,6 +41,9 @@ | ||
| snapcraft [options] list-plugins | ||
| snapcraft [options] tour [<directory>] | ||
| snapcraft [options] update | ||
| + snapcraft [options] gated <snap-name> | ||
| + snapcraft [options] validate <snap-name> [--key-name=<key-name>] """ \ | ||
| +"""[<snap-revision> ...] |
| return any(args.get(command) for command in commands) | ||
| -def _run_store_command(args): | ||
| +# This function's complexity is correlated to the number of | ||
| +# commands, no point in checking that. |
elopio
Sep 27, 2016
Member
well, actually ;)
I think it could be nice to split the commands that handle keys from the commands that handle snap uploads and releases. Not a big deal though, just if you agree.
|
Thanks Roberto, this looks pretty good. I've left some questions, I'm just worried about the lack of integration tests against the real server. |
|
Not hated, but docopt is weird once you have these many commands (I know sergio is removing it) |
|
El martes, 27 de septiembre de 2016 20h'19:10 ART, Roberto Alsina
I am sort of settled on click. Not sure if due to everything I read or the Enviado con Dekko desde mi dispositivo Ubuntu |
ralsina
added some commits
Sep 28, 2016
cprov
suggested changes
Sep 28, 2016
Thanks Roberto,
Few points to address and also make sure tests are passing.
| +class GatedTestCase(integration_tests.StoreTestCase): | ||
| + | ||
| + def setUp(self): | ||
| + if os.getenv('TEST_STORE') != 'fake': |
cprov
Sep 28, 2016
Contributor
I would use os.getenv('TEST_STORE', 'fake') just to be on the safe-side in case TEST_STORE is not defined.
| +class ValidateTestCase(integration_tests.StoreTestCase): | ||
| + | ||
| + def setUp(self): | ||
| + if os.getenv('TEST_STORE') != 'fake': |
| + store = storeapi.StoreClient() | ||
| + # Get data for the gating snap | ||
| + with _requires_login(): | ||
| + snap_data = store.cpi.get_package(snap_name, 'stable', None) |
cprov
Sep 28, 2016
Contributor
I think we should resolve the snap-id via account-info (logged users accessible snaps) instead of hitting CPI and require an active publication in 'stable'.
Also 'None' is not needed, arch is optional
ralsina
Sep 28, 2016
Contributor
Argh, forgot to remove it when I made it optional :-P
I'll look into resolving via account-info now.
| + | ||
| + # Get data for the gating snap | ||
| + with _requires_login(): | ||
| + snap_data = store.cpi.get_package(snap_name, 'stable', None) |
| + # Then, for each requested validation, generate assertion | ||
| + for validation in validations: | ||
| + gated_name, rev = validation.split('=', 1) | ||
| + approved_data = store.cpi.get_package(gated_name, 'stable', None) |
cprov
Sep 28, 2016
Contributor
Right, for the validated snaps (3rd-part most likely) we hit CPI and require stable publications __ for now, because I wonder if we would even set things up while all snaps are in unstable channels, but I am sure we can sort this out later.
None is not needed.
ralsina
Sep 28, 2016
Contributor
This only affects automatic updates, right? do we even do those for other channels?
cprov
Sep 28, 2016
Contributor
You are right, the happy path is enable refresh-control, no validation nothing gets updated even if the controlled snaps reach 'stable'. Once there one overrides refresh-control and installs the not-yet-validation snaps, run tests, then create the corresponding validations.
Thanks for clarifying this.
| + with open(fname, 'wb') as f: | ||
| + f.write(assertion) | ||
| + | ||
| + store.push_validation(snap_id, assertion) |
elopio
Sep 27, 2016
Member
This function is too long, can you split it? You are splitting it with comments, it seems it could be nice to have one smaller function per comment.
ralsina
Sep 28, 2016
Contributor
I split it in 3, there is a largish chunk in the loop that's hard to split without writing functions with many args, which are a separate readability problem. Let me know if you like the current split or want me to split finer.
| + message = ('Invalid response from the server when pushing ' | ||
| + 'validations: {} {}').format( | ||
| + response.status_code, response) | ||
| + logger.debug(message) |
cprov
Sep 27, 2016
Contributor
this is hardly a debug message, it's an error. What about making sure the raised exception has enough information to guide the user and that's it.
If our servers are not returning proper JSON and we don't know already ... it's more like our problem.
ralsina
Sep 27, 2016
Contributor
This error usually will mean something like "we are getting HTML instead of JSON because we are pointing to the wrong URL" and the error message will say "Bad response". I think this was suggested by @sergiusens in the previous review, happy to change it if he's ok with it.
sergiusens
Sep 29, 2016
Collaborator
@cprov I need this for the odd case of having issues in production where I can just tell people to add --debug :-)
| + try: | ||
| + response_json = response.json() | ||
| + # XXX This error is insanely verbose, SCA needs to trim it down | ||
| + response_json['text'] = response.json()['error_list'][0]['message'] |
cprov
Sep 27, 2016
Contributor
mitigated by https://code.launchpad.net/~cprov/software-center-agent/sign-build-errors/+merge/306939
ralsina
added some commits
Sep 28, 2016
sergiusens
requested changes
Sep 29, 2016
i think I have the last set of feedback from your Initial skeleton for validate
| snapcraft [options] upload <snap-file> | ||
| snapcraft [options] push <snap-file> [--release <channels>] | ||
| snapcraft [options] release <snap-name> <revision> <channel> | ||
| snapcraft [options] list-plugins | ||
| snapcraft [options] tour [<directory>] | ||
| snapcraft [options] update | ||
| + snapcraft [options] gated <snap-name> | ||
| + snapcraft [options] validate <snap-name> [<snap-revision> ...] [--key-name=<key-name>] |
sergiusens
Sep 29, 2016
Collaborator
Making [<snap-revision> ...] [--key-name=<key-name>] optional allows me to run
snapcraft validate <snap-name> which provides no useful output and probably invalid:
$ snapcraft validate telegram-sergiusens
Getting details for telegram-sergiusens
$
sergiusens
Sep 29, 2016
Collaborator
<snap-revision> is f the form <snap-name>=<revision>? This needs a better arg-name.
ralsina
Sep 29, 2016
Contributor
Better arg-name ... maybe "validation"?
Yes, it's always snap-name=revision
And it's most definitely not optional, there must be at least one. How does one tell docopt "one or more of these"?
Yes, calling "snapcraft validate telegram-sergiusens" currently does nothing. I can add a check for "at least one validation" if there's no way to tell docopt to do it.
ralsina
Sep 29, 2016
•
Contributor
@sergiusens I just pushed a change where it now is <validation>... which renames it, and makes it required at least once.
| + message = ('Invalid response from the server when pushing ' | ||
| + 'validations: {} {}').format( | ||
| + response.status_code, response) | ||
| + logger.debug(message) |
cprov
Sep 27, 2016
Contributor
this is hardly a debug message, it's an error. What about making sure the raised exception has enough information to guide the user and that's it.
If our servers are not returning proper JSON and we don't know already ... it's more like our problem.
ralsina
Sep 27, 2016
Contributor
This error usually will mean something like "we are getting HTML instead of JSON because we are pointing to the wrong URL" and the error message will say "Bad response". I think this was suggested by @sergiusens in the previous review, happy to change it if he's ok with it.
sergiusens
Sep 29, 2016
Collaborator
@cprov I need this for the odd case of having issues in production where I can just tell people to add --debug :-)
|
ok to test |
ralsina commentedSep 19, 2016
•
Edited 1 time
-
ralsina
Sep 27, 2016
Implements snapcraft validate and gated commands, according to discussions and specs.