Calling the admin api from within a route route causes a deadlock

I am writing a caddy app which configures other apps like http. My caddy app is triggered via a http.handler module.

What I’m saying is my http.handler calls the admin API as a part of its logic. This results in some sort of a deadlock and the admin API does not return indefinitely.

The problem gets resolved if I call the admin API asynchronously in a goroutine.

Is there a better way to do this? It’s important for me to make this call synchronous.

See this discussion:

What’s happening is the admin endpoint is trying to shut down the server that is proxying the request to the admin endpoint through it, which is waiting for the admin endpoint to return, but the admin endpoint is waiting for the proxy to finish responding, which won’t respond until the admin endpoint does, which won’t until… you get the idea.

It will eventually finish, but forcefully after some time, and you won’t get a valid HTTP response. So you won’t know if the config change succeeded or not.

While you may proxy the admin endpoint, it is equipped to be accessed directly. If you do use a proxy, it should be a proxy that does not depend on config from the admin endpoint you’re trying to proxy.

I recommend accessing the admin endpoint directly.

It’s impossible.

If you want a valid HTTP response, something has to give somewhere: you will have to introduce asynchronicity into the system.

This works, but we do not do this for you because it would require dropping some information on the floor: either we respond with 202 Accepted, meaning, “We’ll try to load this config. You’ll have to make another request later to find out if it worked.” The only way to get an error detail would be in the logs. Or, we can get that information but risk resource depletion. Alternatively still, we can drop the synchronization entirely and risk memory corruption (race condition).

Be aware that with a goroutine you still have to synchronize any return value (error output) if you want that (which you need, if you want to write a useful HTTP response other than 202), and you can also deplete resources if you create goroutines unbounded.

My highest recommendation is to not proxy the admin endpoint through the server that is controlled by the admin endpoint.

Otherwise, you’ll have to implement asynchronicity in the way that is best for your specific use case. There is no good “right” answer here, so we do not do it for you.

I see. So are you suggesting I go for a Sidecar pattern here?

My http.Handler isn’t doing a transparent proxy here. It takes config in one format, validates it, then generates a caddy config format and applies it to caddy.

My downstream client isn’t caddy aware and it cannot be.

After thinking about this for 5 more minutes, I’ve come up with a feature request.

Can we make the admin API pluggable as well? As in you let me inject handlers exposing custom endpoints on the admin server. It need not be super flexible like caddy. It could be as simple as /apply/HANDLER_NAME. Then in my handler, give me the methods i can call to set the config after doing my checks and transformations.

This way I can make my own variations of /load

That’s basically what a config adapter does. Why not make your code into one of those? Then you can use the existing /load endpoint with your format’s Content-Type header.

It already is. The /load endpoint is a plugin:

Does that help?

This does sound like what I need. I’m still not sure on how and where can I register a custom adapter…

Edit:

I see there is a caddyconfig.RegisterAdapter i can use to add a adapter in my init() function. Then i can pass the content type header to the /load endpoint with my adapter name. My Adapt function will have to return the entire json payload caddy needs.

Am I understanding this right?

1 Like

So this is what I ended up doing finally.

I made a module in the admin.api namespace. This lets me create custom endpoints. My endpoints then call methods on a singleton struct (since it is stateful) I’ve made on the global level which internally calls caddy.Load. This eliminates the need to right any asynchronous code since the request is being handled by the admin module.

I do have some complains though.

  • There is no caddy.ReadConfig method to read existing config. I see there is a method which seems to do the same but that isn’t exposed.
  • There is no way supply custom config to the admin module. Due to this, I have to rely on env variables to load an external config file. Also, the admin.api modules should have access to the AdminConfig.

Both these issues can we resolved easily in my opinion:

  • Simply expose caddy.readConfig to resolve the first issue.
  • admin.api can optionally implement a caddy.Provisioner interface from where they can get access to the AdminConfig.
  • Add a Extras map in the AdminConfig to accommodate extra config.

Yep! That’s it.

Ok so I read all that, but it still seems like way more work than is necessary. A config adapter is all you need to write that will transform any config format into Caddy JSON. You can use the existing /load endpoint for that. You don’t need to write your own admin endpoint handler. I want to clear this up before we continue because it seems like something you’re doing is making things harder than is necessary.

As per my requirements, I need to have a custom endpoint in the form of /v1/config/{type} to accept my configuration. Also, my downstream client cannot set a custom value in the Content-Type header.

Like caddy, my users can update specific parts of my configuration. That is why I need a stateful config adapter.

That’s unfortunate. It will make things a little tricker but not much. You should file a bug with your backend and see if they will fix that.

Just perform a GET /config/ request.

I am not sure what you mean by this?

Why is that needed/relevant?

That’s unfortunate. It will make things a little tricker but not much. You should file a bug with your backend and see if they will fix that.

I’m building a replacement for an existing tool so can’t do much here.

Just perform a GET /config/ request.

That’s what I’m doing rn. Seems like a round about way of doing it though. Would be great if I could access the config via a fn call.

I am not sure what you mean by this?

There are static parameters my config adapter needs. Things like configFormat (json | yaml). In order words, the behavior of the adapter needs to be configurable. There is no way to provide such information to my config adapter.

Just to be clear, I’m calling my admin.api module an adapter. But I guess the same thing holds true for caddyconfig.Adapter.

I can consider it, but in the meantime, your stateful config adapter could also keep it, right? It already knows all the JSON that’s going to Caddy, so…

Ah, gotcha. Yeah, well, I think that makes sense though: config adapters don’t have any JSON config exposed because their purpose is to produce the JSON config, not be produced by it. In other words, JSON is their output, not their input.

I can consider it, but in the meantime, your stateful config adapter could also keep it, right? It already knows all the JSON that’s going to Caddy, so…

Yes. But things like the logging & admin config has to be the same. So i need to retrieve everything at least once since my module doesn’t really have that information on startup. And I can’t use your APIs to modify parts of the config since I’ll end up in a deadlock again.

Hitting the API to get the config works for now.

Ah, gotcha. Yeah, well, I think that makes sense though: config adapters don’t have any JSON config exposed because their purpose is to produce the JSON config, not be produced by it. In other words, JSON is their output, not their input.

I totally agree with you. But if caddy wants to be a mechanism to build apps in, it needs to make the admin api more configurable. I really like the admin.api approach since it lets me make my custom endpoints. All I need is something like the caddy.Provisoner support for admin.api from where I can access the admin config.

In a real world use case, people would like to plugin a store to distribute the config throughout a cluster. The config for the store and things like that should be, in my opinion, in the admin config. It cannot be an app since that limits what i can do due to the deadlock issue.

Also, i noticed, we setup the admin module before the logging Module. Not sure why that’s the case, but I can’t seem to get a handle of my custom loggers in my admin.api module. I’ll create another topic for this

1 Like

I didn’t see Matt link this yet but I think it’s relevant, so I will; the 3rd feature from this recently merged PR seems relevant to you:

1 Like

I’m still not understanding… the admin and logging config are also part of the JSON config, meaning, your adapter will have access to that, too. Just feed all config through the adapter, right?

You can already implement Provisioner for your admin.api module, ah, but you mean that you want to be able to access the current Admin config from within the provisioning step? We already expose Storage, Logging, and Apps through the caddy.Context, so I guess we can expose the Admin config. Please file an issue so we can track that? Should be easy to do.

I recently revisited this, actually, in the PR that Francis just linked to above. I even switched it around momentarily while I was trying some things. I switched it back because it turns out I didn’t need to put logging first, but if you have a need for it, then I guess we should look at that too. Please open an issue and I’ll investigate further. Thanks!


To clarify, I’m open to making changes that export new functions, but I do that only carefully, after considering other possibilities, because I want to make sure they satisfy real use cases first.

I’m still not understanding… the admin and logging config are also part of the JSON config, meaning, your adapter will have access to that, too. Just feed all config through the adapter, right?

I was just explaining why i need to pull the json config again. It because my adapter only generates JSON config for the http app. The rest needs to be carry forwarded as is.

You can already implement Provisioner for your admin.api module, ah, but you mean that you want to be able to access the current Admin config from within the provisioning step? We already expose Storage, Logging, and Apps through the caddy.Context, so I guess we can expose the Admin config. Please file an issue so we can track that? Should be easy to do.

That would be super helpful. I’ll raise an issue right away. Also, can you make the admin config extendable as well while you at it?

I recently revisited this, actually, in the PR that Francis just linked to above. I even switched it around momentarily while I was trying some things. I switched it back because it turns out I didn’t need to put logging first, but if you have a need for it, then I guess we should look at that too. Please open an issue and I’ll investigate further. Thanks!

I wanted the logging level for my code to be debug while the rest of caddy could use info. So when I do logs in my admin.api, i couldn’t see my debug logs. I suspect its because logging wasn’t setup till then.

I also believe its a good practice to setup logging first unless there is some issue preventing it.

To clarify, I’m open to making changes that export new functions, but I do that only carefully, after considering other possibilities, because I want to make sure they satisfy real use cases first.

I totally understand this. I’m open to coming on a quick call to discuss my use case. You can then decide if you want that to be within the scope of Caddy.

There won’t be any other config though, unless it is put into the config by your adapter. Or if config is otherwise bypassing your adapter (but that’s in your control).

In what way? It already is, as you’ve discovered, you have a custom endpoint for it.

My priority was to set up the admin endpoint first, so that if anything goes wrong, the admin endpoint can be used to change config and recover from problems. If you’ve got a caddy instance running with no admin endpoint, it’s just dead in the water. I’ll have to think about if logging is worth risking that.

Just type it out here when you have a chance – that’ll be easiest!

There won’t be any other config though, unless it is put into the config by your adapter. Or if config is otherwise bypassing your adapter (but that’s in your control).

But, I don’t know what it is unless I read it.

Wait. I think i know what the mismatch is about. I’m starting the server with a default config caddy.json file. So my adapter doesn’t need know what the logging config and stuff like that is. So the op guy can start Caddy with any default config (to configure logging and storage), my adapter takes ownership of just the http app and nothing else.

In what way? It already is, as you’ve discovered, you have a custom endpoint for it.

My application needs setup parameters which I cannot take over the API. So we have another config file where all of that goes. We have two config files. One is an initial caddy.json with the initial logging and storage config, the other is a myapp.yaml which has my setup parameters. Right now i have to start caddy with an env variable (with the location of my setup file) which my adapter reads to load myapp.yaml cause I could not find any way to provide such information to my adapter.

Maintaining two files is painful. It would be great if I could embed my config in the caddy.json we have. I can’t put all of that config in a custom app since that will cause the admin module to depend on an app which sounds wrong. The only logical place to embed this config would be in the admin config of caddy.json.

My priority was to set up the admin endpoint first, so that if anything goes wrong, the admin endpoint can be used to change config and recover from problems. If you’ve got a caddy instance running with no admin endpoint, it’s just dead in the water. I’ll have to think about if logging is worth risking that.

That makes sense since logging itself can be configured via the API. I can do without configurable logging in my admin module.

But I don’t see myself changing logging config at runtime.

Well, I’ll leave that upto you.

Ahhh okay, so some config is bypassing your adapter. Thanks for clearing that up.

Would it be easier if Caddy could pull its config at startup? The latest commits on master make this possible (as Francis linked to earlier, I just want to be sure you saw it):

Anyway, your use case is definitely a unique one! I’ll try to accommodate best we can. Please feel free to open issues for any remaining concerns and that way we won’t forget to look at them. :+1:

I saw that PR. But i don’t have a external server the config can be pulled from. All i need is ability to add custom parameters in the admin config and make it accessible in modules through caddy.Context. Both seem to be small changes in my opinion.

Will raise an issue for it.