When to run caddy.Load() in a Caddy app

1. Caddy version (caddy version):

2.4.1

2. How I run Caddy:

Currently developing a custom module, so with xcaddy.

a. System environment:

MacOS 11.5.2 Big Sur

b. Command:

xcaddy run --config config.json

c. Service/unit/compose file:

Paste full file contents here.
Make sure backticks stay on their own lines,
and the post looks nice in the preview pane.

d. My complete Caddyfile or JSON config:

{
  "apps": {
    "http": {
      "servers": {
        "server1": {
          "listen": [":5005"],
          "routes": [
            {
              "handle": [
                {
                  "body": "Healthy",
                  "handler": "static_response",
                  "status_code": 200
                }
              ]
            }
          ]
        }
      }
    },
    "myapp": {
      "instance_var_name": "MY_ENV_VAR",
      "config_api_url": "http://localhost:8081/config"
    }
  }
}

3. The problem I’m having:

When running caddy.Load() in the Start() method, it seems the app cannot exit properly. Notice it takes two attempts to close the process started by xcaddy run (see 4).

Additionally, it seems the config doesn’t seem to get applied (localhost:4001/check returns “Error: Couldn’t connect to server”. Any code after caddy.Load() in my Start() method does not get called either.

This is the applied config (returned by getConfig() in code snippet below):

{
   "apps":{
      "http":{
         "servers":{
            "srv0":{
               "listen":[
                  ":4001"
               ],
               "routes":[
                  {
                     "match":[
                        {
                           "path":[
                              "/check"
                           ]
                        }
                     ],
                     "handle":[
                        {
                           "body":"nice work!",
                           "handler":"static_response",
                           "status_code":200
                        }
                     ]
                  }
               ]
            }
         }
      }
   }
}

So when writing a custom module, what is the proper method to load a new config?
Here is the relevant code:


func (a App) Start() error {
	configData := getConfig(a)
	if configData == nil {
		a.logger.Error("no config available")
		return nil
	}
	// apply config
	if configData != nil {
		a.logger.Info("applying fetched configuration")
		return caddy.Load(configData, false)
	}
	return nil
}

func getConfig(a App) []byte {
	resp, err := http.Get(a.ConfigApiUrl)
	if err != nil {
		a.logger.Error("failed to retrieve config from api\n" + err.Error())
		return nil // we want to continue on if failed
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		a.logger.Error("failed to parse config from api\n" + err.Error())
		return nil
	}
	return body
}

func (a *App) Stop() error {
	a.logger.Info("closing app")
	return nil
}

4. Error messages and/or full log output:

^C2021/09/13 22:23:20 [INFO] SIGINT: Shutting down
2021/09/14 05:23:20.774 INFO    shutting down   {"signal": "SIGINT"}
2021/09/14 05:23:20.774 WARN    exiting; byeee!! đź‘‹     {"signal": "SIGINT"}
^C2021/09/14 05:23:26.476       WARN    force quit      {"signal": "SIGINT"}
2021/09/13 22:23:26 [ERROR] exit status 2

5. What I already tried:

  • Doing defer caddy.Load(...) in Start()

6. Links to relevant resources:

This is a bit of a chicken and egg problem. Since your app is in the config, the config hasn’t finished loading yet before you’re trying to load a new one. I don’t think you’re taking the right approach for this.

I think what you actually want is to write a caddy.config_loaders module (or just use the included http one if it does what you need)

Yup, I didn’t realize that existed. Thank you!

I also wonder if using this approach, the config loading would have to be done asynchronously (in a goroutine).

Cory, let me know if you have troubles getting either of these approaches to work.

I think the http loader is the way to go (although I might eventually do something custom so that it can persist). However, I have the strangest error claiming that I’m not declaring the module in my config when I clearly am.

Error:

caddy run --config config.json
2021/09/14 17:28:19.635 INFO    using provided configuration    {"config_file": "config.json", "config_adapter": ""}
2021/09/14 17:28:19.639 INFO    admin   admin endpoint started  {"address": "tcp/localhost:2019", "enforce_origin": false, "origins": ["localhost:2019", "[::1]:2019", "127.0.0.1:2019"]}
2021/09/14 17:28:19.640 INFO    tls     cleaning storage unit   {"description": "FileStorage:/Users/corycooper/Library/Application Support/Caddy"}
2021/09/14 17:28:19.641 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0xc000450850"}
run: loading initial config: loading new config: loading config loader module: module name not specified with key 'module' in map[http:map[method:GET module:http timeout:2s url:http://localhost:8081/config]]

My config:

{
  "admin": {
    "config": {
      "load": {
        "http": {
          "module": "http",
          "method": "GET",
          "url": "http://localhost:8081/config",
          "timeout": "2s"
        }
      }
    }
  },
  "apps": {
    "http": {
      "servers": {
        "server1": {
          "listen": [":5005"],
          "routes": [
            {
              "handle": [
                {
                  "body": "Healthy",
                  "handler": "static_response",
                  "status_code": 200
                }
              ]
            }
          ]
        }
      }
    }
  }
}

Don’t need to, use --resume

Use the caddy-api.service to use that:

I think it needs to be this:

      "load": {
        "module": "http",
        "method": "GET",
        "url": "http://localhost:8081/config",
        "timeout": "2s"
      }
1 Like