-
Notifications
You must be signed in to change notification settings - Fork 218
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
Support for headers.dhall configuration #2236
Conversation
@timbertson: Sorry for the delayed review! My first general comment is that I usually do the standardization in dhall-lang first before I begin work on the Haskell implementation, for two main reasons:
That said, I don't foresee any issue standardizing this (it seems pretty easy to specify using our existing notation) |
0ee9fbf
to
e082165
Compare
e082165
to
00a1941
Compare
I think this is getting close to ready. I'm pretty happy with how everything hangs together now. The one thing I'm not quite satisfied with is the error reporting. When loading the headers, we need a fresh Status (because e.g. it shouldn't attempt remote imports, or loading headers recursively). I copy the But it doesn't seem to work quite right. In some cases it reports a good looking stack, but then also reports an additional error:
The first half is good, but I think it's trying to display some dhall code after In other cases, it doesn't show a stack at all:
Is this because of the type of the error? A parse error seems to be reported well, at least:
|
(the error handling code in question is in Import.hs |
@timbertson: So I think the solution to this problem is to stick to dhall-lang/dhall-lang#1192 as written. In particular, this section of the import resolution logic:
The precise interpretation of that is that the |
Thanks, I'll give that a go. Strangely, I was suddenly struck by #2024 so I'm a bit stuck for now :/ |
Hmm, I like that it's simpler, but it gives worse error messages:
I'd really like for any errors to be reported with an evaluation stack containing either Should I give up the notion of merging this with a stack of imports, and just wrap all exceptions from this function with a new SiteHeadersLoadException or something, which explicitly tracks the source (envvar or file)? |
@timbertson: What you could do is create a custom exception just for the headers file. The idea is that instead of using
It still won't have the stack of how it got there, but it's probably enough information for the user to figure out how to fix the immediate problem. |
So it turns out disabling imports is unfortunately not what I want. The primary use case in mind that I have for this feature would use the following headers file:
If I'm sharing dhall expressions it's much easier to have folks set So I definitely would like to support that, does this mean I need to amend the standard? I'd be OK with it supporting arbitrary local imports in the headers expression, although |
@timbertson: I would be okay with that if the transitive import behavior were behind a flag, mainly because it's technically deviating from the standard until the standard is amended |
Do you mean a command line flag? How do you feel about altering the
standard to require this support instead?
Most authentication use cases seem likely to involve environment variables,
so I'd prefer to have that supported everywhere.
|
The main thing I want to avoid is the Haskell implementation making changes ahead of the standard, because I want to ensure that no Dhall implementation is privileged over others. What makes this particular situation a little complicated is that if we interpret the standard as written as broadly as possible to permit transitive import resolution then it could potentially lead to infinite loops for remote imports. That's one of the reasons I'm going in the other direction and reading the standard as narrowly as possible in order to be conservative and not implement behavior that other implementations haven't agreed to via the standardization process. That said, there are multiple possible solutions to that which I would be fine with:
|
Now I feel bad for merging the standard PR right before the 21.0.0 cut 😬 . I'd be happiest with either options 2 or 4. I'd rather not ship this until it does what I intended. For both of those, we probably want to work on this PR and a standard update PR in parallel, and whichever is ready first determines which path we take?
I'd be fine with that, but also: if the standard change is small / uncontroversial, could v1.40 simply support that version of the standard (v21.1.0 or v22.0.0)? Or do you want to release a version of dhall-haskell for each major standard version? |
@timbertson: I think the easiest route would be to cut a 1.40.0 release without merging this and then cut a small 1.40.1 release right after adding support for |
Sounds good to me 👍
|
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 is getting pretty close. Just one more change I can think of
dhall/ghcjs-src/Dhall/Import/HTTP.hs
Outdated
@@ -35,3 +37,6 @@ fetchFromHttpUrl childURL Nothing = do | |||
return body | |||
fetchFromHttpUrl _ _ = | |||
fail "Dhall does not yet support custom headers when built using GHCJS" | |||
|
|||
originHeadersFileExpr :: IO (Expr Src Import) | |||
originHeadersFileExpr = return (Missing) |
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.
originHeadersFileExpr = return (Missing) | |
originHeadersFileExpr = return Missing |
dhall/src/Dhall/Syntax.hs
Outdated
-- OriginHeaders is identical to Code, except local imports with this mode | ||
-- are allowed from any source (local or remote). This type is only used when | ||
-- loading Origin headers, it can't be set by the user. | ||
data ImportMode = OriginHeaders | Code | RawText | Location |
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 don't think we need a new ImportMode
if we implement this in the same way as the standard specifies. I'll comment elsewhere on how I think we can make this work without the OriginHeaders
mode
|
||
fetchFromHttpUrl :: URL -> Maybe [HTTPHeader] -> StateT Status IO Text.Text | ||
fetchFromHttpUrl childURL mheaders = do | ||
Status { _loadOriginHeaders } <- State.get | ||
|
||
originHeaders <- _loadOriginHeaders |
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 made one mistake when I previously suggested using the ambient StateT
for _loadOriginHeaders
. We do still want _loadOriginHeaders
to use StateT
, but we want the _stack
to be temporarily reset to contain just one import, which is the path to origin headers. This is because of this line from the standard:
(ε, headersPath) × Γ₀ ⊢ userHeadersExpr ⇒ userHeaders ⊢ Γ₁ ; Resolve userHeadersExpr with an empty import context
The key bit is the (ε, headersPath)
part, which says that when resolving the userHeadersExpr
you want the stack to contain only the headers path and nothing else.
However, we don't have to reset the stack here within the fetchFromHttpUrl
function. Instead, I think the best place to do that would be inside of the _loadOriginHeaders
function. I'll comment below with some more notes about how to do that.
dhall/src/Dhall/Import.hs
Outdated
originHeadersLoader :: IO (Expr Src Import) -> StateT Status IO OriginHeaders | ||
originHeadersLoader headersExpr = do | ||
partialExpr <- liftIO headersExpr | ||
|
||
let go key₀ key₁ = do | ||
let expected :: Expr Src Void | ||
expected = | ||
App List | ||
( Record $ Core.makeRecordField <$> | ||
Dhall.Map.fromList | ||
[ (key₀, Text) | ||
, (key₁, Text) | ||
] | ||
) | ||
|
||
let suffix_ = Dhall.Pretty.Internal.prettyToStrictText expected | ||
let annot = case loadedExpr of | ||
Note (Src begin end bytes) _ -> | ||
Note (Src begin end bytes') (Annot loadedExpr expected) | ||
where | ||
bytes' = bytes <> " : " <> suffix_ | ||
_ -> | ||
Annot loadedExpr expected | ||
|
||
_ <- case (Dhall.TypeCheck.typeOf annot) of | ||
Left err -> throwMissingImport (Imported _stack err) | ||
Right _ -> return () | ||
|
||
return (Core.normalize loadedExpr) | ||
|
||
let handler₀ (e :: SomeException) = do | ||
{- Try to typecheck using the preferred @mapKey@/@mapValue@ fields | ||
and fall back to @header@/@value@ if that fails. However, if | ||
@header@/@value@ still fails then re-throw the original exception | ||
for @mapKey@ / @mapValue@. -} | ||
let handler₁ (_ :: SomeException) = | ||
throwMissingImport (Imported _stack e) | ||
|
||
handle handler₁ (go "header" "value") | ||
|
||
headersExpression' <- | ||
handle handler₀ (go "mapKey" "mapValue") | ||
|
||
return url { headers = Just (fmap absurd headersExpression') } | ||
|
||
normalizeHeaders url = return url | ||
loaded <- loadWith (ImportAlt partialExpr emptyOriginHeaders) | ||
headers <- liftIO (toOriginHeaders loaded) | ||
|
||
-- return cached headers next time | ||
_ <- State.modify (\state -> state { _loadOriginHeaders = return headers }) | ||
|
||
return headers |
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 is where you will need to reset the stack to contain just the headers file. However, it will require restructuring things a little bit because the type of originHeadersLoader
doesn't permit it to know what path the headersExpr
came from. You might need to change it so that the headersExpr
returns the original Import
that the Expr
came from so that originHeadersLoader
can set the stack
to that Import
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.
You might need to change it so that the headersExpr returns the original Import that the Expr came from so that originHeadersLoader can set the stack to that Import
Hmm, I think I removed my ability to do this 🤦
headersExpr
in the normal case is env:DHALL_HEADERS ? ~/.config/dhall/headers.dhall
. So you can't know which branch was successful until after loadWith
is called, but you need to give loadWith
a stack. (Also, I don't see how loadWith
could give back the successful Import
since it returns an Expr Src Void
)
I've pushed an alternative solution: when loading headers, we just start with the root import. I believe this is generally either .
(for an expression) or the path to the main file (if using --file). So this should always be local.
The trick is I had to ensure reentrant calls to load the headers didn't also reset the stack, since that's how we'd detect the Cycle
error. Unfortunately that didn't work in practice (see this comment), but conceptually that's how it's working.
I think I could remove that hack by also popping the most recent item off the stack in reentrant calls (so that it doesn't look like the headers are being loaded from the remote import), but it didn't seem worth that extra complexity to achieve the same result.
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.
Actually, I think that indicates that there is a bug in the standardized behavior. The bug is that, as you noted, resetting the stack as the standard specifies interferes with cyclic import detection.
What if we were to change the standard to not reset the stack (like you just implemented in your most recent changes) and instead change the logic to:
(Δ, parent, headersPath) × Γ₀ ⊢ userHeadersExpr ⇒ userHeaders ⊢ Γ₁
In other words, we append the headers path to the stack (not including the current URL import that the headers will be used for) when resolving the headers file.
dhall/src/Dhall/Import.hs
Outdated
status <- State.get | ||
|
||
let headerLoadStatus = status { | ||
_stack = pure (NonEmpty.last (_stack status)), |
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 we change this to _stack = headerImport :| NonEmpty.tail (_stack status)
then it will do the right thing and not trigger the referential sanity check. Then I think you wouldn't need the reentrantLoad
work-around.
That does, however, mean that you would need to get the headerImport
which is currently not in scope, but I'll leave a separate comment for that.
defaultOriginHeaders :: IO (Expr Src Import) | ||
defaultOriginHeaders = do | ||
fromFile <- originHeadersFileExpr | ||
return (Note headersSrc (ImportAlt envOriginHeaders (Note headersSrc fromFile))) |
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 we won't be able to use ImportAlt
here since, as you noted, it won't let us remember which import succeeded. i think you'd need to change the type from IO (Expr Src Import)
to IO [Import]
to get this to work. It would also improve the error messages, too, I think
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 went down this path, but I couldn't see how to make it work. The fallback code is implemented by loadWith
. I considered extracting the logic from the ImportAlt
branch, as a function with signature StateT Status IO (Expr Src Void) -> StateT Status IO (Expr Src Void) -> StateT Status IO (Expr Src Void)
. I couldn't just take two Expr Src Void
, since we need to provide a different _stack in each branch (based on the header we're importing).
But it seems we need to use the full loadWith
, since that's also the code which does the Cycle
detection. In particular, if we manually construct _stack = headerImport :| NonEmpty.tail (_stack status)
then that would mean we'd need to also implement the cycle detection here too. That seems messy and repetitive.
Am I missing something?
I did try an alternate, which is to just literally use the parent stack. It removed the need to special-case reentrant loads, which is nice. It requires a runtime assertion (because we can't prove the parent stack is nonempty with types). It also shouldn't ever hit a referentially opaque import error, although that's an implementation detail (because we cache on the first load). I've made that change in c77f5a7
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.
@timbertson: Oh, I just figured out a (hacky) way to do this, but at least it doesn't require the partial function: instead of trying to figure out which headers.dhall
we imported, just add both of them to the stack
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.
Hmm, would errors then report that e.g. ~/.config/dhall/headers.dhall was imported from env:DHALL_HEADERS
(or vice versa)? The partial function shouldn't ever fail (despite being a bit yuck), right? 🤞
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.
Yeah, you're right. Then we'll stick with what you have for now
🥳 hooray, thanks for all the assistance getting this in shape. I'm fixing CI issues, currently stuck with this hydra error:
I'm not sure what to run to reproduce locally. I ran |
CI checks for either "Missing documentation for" or "Warning: … is out of scope" Line 84 in 8f901d2
|
Ah OK. I think I've fixed those, but now one hydra job is dying with:
Which I think means out of memory. Is that a known problem? I couldn't see any way to retry the job, nix is too good at caching ;) |
It's a known problem. The machine sometimes runs out of memory. I can retry the job |
@timbertson: Thank you for doing this! 🙂 |
I'm slightly reluctant to ask 😉 , but does the final approach imply any necessary changes to the standard? IIRC the standard loads headers with an empty context (import chain), but what I ended up doing was to load headers with the regular context. In order to circumvent the referentially opaque check, you need to either:
For efficiency the first is what every implementation will want to do, but I'm not sure which is easier to standardize. |
Yes, the final approach will entail a change to the standard, but I can take care of that. It should be a small change |
Thanks 🙂 |
The motivation for this change is the discussion here: dhall-lang/dhall-haskell#2236 (comment) If we correctly set the parent import for the `headers.dhall` expression then the cyclic import detection will correctly reject loops induced by `headers.dhall` having transitive remote imports of its own.
The motivation for this change is the discussion here: dhall-lang/dhall-haskell#2236 (comment) If we correctly set the parent import for the `headers.dhall` expression then the cyclic import detection will correctly reject loops induced by `headers.dhall` having transitive remote imports of its own.
This PR implements support for per-host header configuration, as standardised in dhall-lang/dhall-lang#1192.
The basic idea is that instead of headers being inline with an import (
URL with toMap { Authorization = "TOKEN" }
), you will be able to specify per-origin headers in a config file, e.g:TODO: