New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the --cert-name flag to select a lineage by its name. #3785
Conversation
Pushing to github to see missing comment updates and test coverage, not ready for review. |
Update: this also contains a Once coverage and tests are happy I expect this to be ready for review. |
Test 1: I have an
But I was interactively prompted for an authenticator, which I didn't expect. I then tried
In this case the renewal appeared to work, but I got mysterious behavior on the install attempt:
(Also not sure why I can get "Congratulations" with the error: I guess because auth worked but install didn't?)
Can any of these behaviors be attributed to this PR? I can try to run the same commands with an older branch for comparison. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are all textual/documentation related but I also wonder if you can repeat my tests in an environment of your own and see if you get the same problems, or if it succeeds for you.
|
||
""" | ||
if not config.certname: | ||
raise errors.ConfigurationError("Specify a certificate name with " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might be clearer as "Specify an existing certificate name with" (for parallelism with the other message).
"--cert-name", dest="certname", | ||
metavar="CERTNAME", default=None, | ||
help="Certificate name to apply. Only one certificate name can be used " | ||
"per Certbot run. Show certificate names by running certificates " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the switch between addressees of the imperative verbs here ("Show certificate names" and "create a new certificate") will confuse readers, because one is saying "[you can] show certificate names by..." and the other is saying "[the program will] create a new certificate...".
Could we change the text for the "Show certificate names" sentence to something like
(You can see certificate names by running certificates command.)
or
(To see certificate names, run certificates command.)
or
(Names can be found with the certificates command.)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and maybe also better to say something like by running "certbot certificates"
or by running cerbot with the "certificates" subcommand
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I like To see certificate names, run "certbot certificates".
logger.debug("Traceback was:\n%s", traceback.format_exc()) | ||
continue | ||
rv = func(candidate_lineage, rv) | ||
return rv |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was originally wondering about the case where there's more than one matching lineage and whether we'd simply get func()
of the last one, but now I see that since the intermediate rv
is an argument to func
, someone writing a func
could also choose to have it accumulate values as it goes, like in a list, if they want information about multiple lineages. So, nice, flexible design there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flexibility is my goal! I'll put this in the docstring to make it clear.
"rename", | ||
"--new-cert-name", dest="new_certname", | ||
metavar="NEW_CERTNAME", default=None, | ||
help="New name for the certificate. Must be a valid filename.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it valid to use only --new-cert-name
with a new lineage, instead of --cert-name
? (Are they synonyms when there is no existing lineage with the specified name?) I can definitely imagine some users mentally parsing this as (new-cert)-name instead of new-(cert-name) and so trying to use it in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good point. Maybe --updated-cert-name
is better? Or just --new-certname
and then also call it --certname
in other places? I don't want to make this flag for anything other than setting a new cert name.
"automation", "--renew-with-new-domains", | ||
action="store_true", dest="renew_with_new_domains", help="If a " | ||
"certificate already exists for the requested certificate name " | ||
"but does not match the request domains, renew it now, " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should probably be "requested domains" here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it should.
@@ -172,7 +172,7 @@ def _handle_identical_cert_request(config, lineage): | |||
return "reinstall", lineage | |||
question = ( | |||
"You have an existing certificate that contains exactly the same " | |||
"domains you requested and isn't close to expiry." | |||
"domains or certificate name you requested and isn't close to expiry." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this would be clearer as "has exactly the same domains or certificate name", since people might not think of the certificate as "containing" its own certificate name? (At least knowing the implementation details, I don't.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're feeling fancy, provide a question whose wording varies depending on whether the user actually provided domains or a certificate name (or if you agree that's a good idea but don't want to do it in this PR, open a new ticket for it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll put it to has
for now.
""" | ||
if config.renew_with_new_domains: | ||
return | ||
msg = ("Confirm that you intend to update certificate {0} " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be helpful or unhelpful to put quotes around the lineage name when displaying it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no opinion about this. Let me know if you want me to change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm also not sure, so I won't suggest a change.
@@ -96,6 +96,25 @@ def write_renewal_config(o_filename, n_filename, archive_dir, target, relevant_d | |||
config.write(outfile=f) | |||
return config | |||
|
|||
def rename_renewal_config(prev_name, new_name, cli_config): | |||
"""Rename's cli_config.certname's config to cli_config.new_certname. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be "Renames"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of changes requested to use the IDisplay
mechanisms for [non]interactivity in standard ways, plus a small change in storage, plus some nits/optional suggestions.
new_domains, | ||
old_domains)) | ||
obj = zope.component.getUtility(interfaces.IDisplay) | ||
if not obj.yesno(msg, "Update cert", "Cancel"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks to @PaulSD for noticing this: you should add default=True
to this obj.yesno()
call, so that this path can be run non-interactively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had added a cli flag because I didn't know about this default=True
thing. It seems like we usually have both a noninteractive option and a cli flag, is that right? Or should I remove the cli flag given that it can be run noninteractively?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. In most cases the CLI flags of that sort (like --keep-until-expiring
) predated the -n
flag, so we've never asked ourselves that question!
Argument 1: -n
does the job, so let's just make everyone use it if they want non-interactive execution
Argument 2: the user might want to be asked about --domains
interactively, but not this update confirm thing, so they might want to provide the flag for "Update" on the command line (or put it in /etc/letsencrypt/cli.ini
) while still running interactively.
Which do we find more persuasive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep both, because a user might not know about non-interactive mode and just look for the specific command-line flag they want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I don't feel strongly, I think it is the user's responsibility to look at the documentation of the available flags more closely if (s)he doesn't immediately find what (s)he needs.
Furthermore, if you're running in interactive mode, I don't think it is so bad to get an extra question; in other words, I don't think there needs to be a switch to mute specific aspects of the interactive mode. (The number of questions is low anyways.) It's a trade-off with code simplicity, which I also consider valuable.
raise errors.ConfigurationError("Specify a certificate name with " | ||
"with flag --cert-name.") | ||
if not config.new_certname: | ||
raise errors.ConfigurationError("Specify a new name for certificate {0} " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be errors.MissingCommandlineFlag
.
But even better would be to do something like calling disp.input("Enter the new name for certificate {0}".format(config.certname), flag="--new-cert-name")
. That will raise a nicely formatted errors.MissingCommandlineFlag
for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah! I didn't know we had that but I like it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition, the previous check can probably provide a list of cert names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok it now does that.
def lineage_for_certname(config, certname): | ||
"""Find a lineage object with name certname. | ||
""" | ||
def func(candidate_lineage, rv): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: Maybe nicer to call this matches_certname
or certname_match_fn
or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function returns a lineage object for a given certname. Is it that the name is too long?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My nit was suggesting something slightly more descriptive instead of func
. But feel free to ignore this suggestion!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that makes more sense! I can do that.
"--cert-name", dest="certname", | ||
metavar="CERTNAME", default=None, | ||
help="Certificate name to apply. Only one certificate name can be used " | ||
"per Certbot run. Show certificate names by running certificates " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and maybe also better to say something like by running "certbot certificates"
or by running cerbot with the "certificates" subcommand
?
@@ -68,17 +70,17 @@ def _report_successful_dry_run(config): | |||
reporter_util.HIGH_PRIORITY, on_crash=False) | |||
|
|||
|
|||
def _auth_from_domains(le_client, config, domains, lineage=None): | |||
def _auth_from_available(le_client, config, domains=None, certname=None, lineage=None): | |||
"""Authenticate and enroll certificate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should document what is going on here a little bit more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha yes. Especially given that this is the primary main
authentication method, as far as I can tell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup; it was already mysterious, and is becoming more so :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about the current documentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -172,7 +172,7 @@ def _handle_identical_cert_request(config, lineage): | |||
return "reinstall", lineage | |||
question = ( | |||
"You have an existing certificate that contains exactly the same " | |||
"domains you requested and isn't close to expiry." | |||
"domains or certificate name you requested and isn't close to expiry." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're feeling fancy, provide a question whose wording varies depending on whether the user actually provided domains or a certificate name (or if you agree that's a good idea but don't want to do it in this PR, open a new ticket for it).
cli_config.renewal_configs_dir, prev_name) + ".conf" | ||
new_filename = os.path.join( | ||
cli_config.renewal_configs_dir, new_name) + ".conf" | ||
if os.path.isfile(new_filename): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should probably be os.path.exists(new_filename)
, so that it errors on directories, links, etc.
@ohemorange, yes, I'll try getting a new lineage with master, then running the closest equivalents to those, then running those with this branch, and see if the problem recurs or not. |
self.assertTrue(mock_renewer_config) | ||
|
||
|
||
class RenameLineageTest(storage_test.BaseRenewableCertTest): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a BaseCertManagerTest
that will work here, why not use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What a good idea, I should do that!
Make coveralls report back? |
Step 3 of #3615.