Module: EasyCaddy::Caddy

Defined in:
lib/easy_caddy/caddy.rb

Constant Summary collapse

BINARY =
'caddy'
LOG_FILE_MODE =

Group-writable mode for Caddy log files. A root-run Caddy (needed to bind :443/:80) creates logs the unprivileged user can’t open during ‘caddy validate`/`reload`; 0660 + macOS staff-group inheritance keeps them openable. See the log-permission fix.

'0660'
ADMIN_ENDPOINT =
'http://localhost:2019/pki/ca/local'

Class Method Summary collapse

Class Method Details

.admin_endpoint_reachable?Boolean

Returns:

  • (Boolean)


97
98
99
100
101
102
103
104
# File 'lib/easy_caddy/caddy.rb', line 97

def self.admin_endpoint_reachable?
  uri = URI(ADMIN_ENDPOINT)
  Net::HTTP.start(uri.host, uri.port, open_timeout: 1, read_timeout: 1) do |http|
    http.get(uri.request_uri).is_a?(Net::HTTPSuccess)
  end
rescue StandardError
  false
end

.brew_service_pidObject



119
120
121
122
123
# File 'lib/easy_caddy/caddy.rb', line 119

def self.brew_service_pid
  output = `brew services list 2>/dev/null | grep '^caddy '`
  m = output.match(/(\d+)/)
  m&.captures&.first&.to_i
end

.install_via_brewObject



132
133
134
# File 'lib/easy_caddy/caddy.rb', line 132

def self.install_via_brew
  system('brew install caddy')
end

.installed?Boolean

Returns:

  • (Boolean)


18
19
20
# File 'lib/easy_caddy/caddy.rb', line 18

def self.installed?
  system('which caddy > /dev/null 2>&1')
end

.process_pidObject



125
126
127
128
129
130
# File 'lib/easy_caddy/caddy.rb', line 125

def self.process_pid
  out = `pgrep -f 'caddy run' 2>/dev/null`.strip
  return nil if out.empty?

  out.lines.first.to_i
end

.reload(caddyfile = Paths.caddyfile) ⇒ Object

rubocop:enable Metrics/MethodLength

Raises:



59
60
61
62
63
64
65
66
67
# File 'lib/easy_caddy/caddy.rb', line 59

def self.reload(caddyfile = Paths.caddyfile)
  unless caddyfile.exist?
    warn '  [ecaddy] Skipping reload — global Caddyfile not found. Run `ecaddy setup` first.'
    return
  end

  out = `#{BINARY} reload --config #{caddyfile} 2>&1`
  raise Error, "Caddy reload failed:\n#{out}" unless $CHILD_STATUS.success?
end

.restart_serviceObject



115
116
117
# File 'lib/easy_caddy/caddy.rb', line 115

def self.restart_service
  system('brew services restart caddy')
end

.running?Boolean

Returns:

  • (Boolean)


106
107
108
109
# File 'lib/easy_caddy/caddy.rb', line 106

def self.running?
  pid = brew_service_pid
  pid && pid > 0
end

.start_serviceObject



111
112
113
# File 'lib/easy_caddy/caddy.rb', line 111

def self.start_service
  system('brew services start caddy')
end

.translate_validate_error(output) ⇒ Object

Caddy validate emits a wall of JSON log lines on stderr. Pull out the actual error and, for common cases (log file permission), turn it into an actionable message. rubocop:disable Metrics/MethodLength



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/easy_caddy/caddy.rb', line 39

def self.translate_validate_error(output)
  error_line = output.lines.find { |l| l.start_with?('Error:') }&.strip

  if error_line && error_line.match?(/setting up custom log.*permission denied/i)
    path = error_line[%r{open\s+(/\S+):\s*permission denied}, 1]
    hint =
      if path
        "Caddy runs as root and created this log 0600; validation runs as you and can't " \
        "open it.\nFix it once with:  sudo chmod #{LOG_FILE_MODE} #{path}\n" \
        'or run `ecaddy audit --fix` to do it interactively.'
      else
        'Check ownership of the log file referenced above, or run `ecaddy audit --fix`.'
      end
    return "Caddy config invalid — log file not writable:\n  #{error_line}\n\n#{hint}"
  end

  "Caddy config invalid:\n#{error_line || output}"
end

.trustObject



69
70
71
# File 'lib/easy_caddy/caddy.rb', line 69

def self.trust
  system("#{BINARY} trust")
end

.trust_with_outputArray(String, Boolean)

Runs ‘caddy trust` and captures stdout+stderr so callers can inspect failures.

Returns:

  • (Array(String, Boolean))

    combined output and whether the command succeeded



76
77
78
79
# File 'lib/easy_caddy/caddy.rb', line 76

def self.trust_with_output
  out = `#{BINARY} trust 2>&1`
  [out, $CHILD_STATUS.success?]
end

.validate(caddyfile = Paths.caddyfile) ⇒ Object



22
23
24
# File 'lib/easy_caddy/caddy.rb', line 22

def self.validate(caddyfile = Paths.caddyfile)
  system("#{BINARY} validate --config #{caddyfile} 2>&1")
end

.validate!(caddyfile = Paths.caddyfile) ⇒ Object

Raises:



26
27
28
29
30
31
32
33
# File 'lib/easy_caddy/caddy.rb', line 26

def self.validate!(caddyfile = Paths.caddyfile)
  return unless caddyfile.exist?

  out = `#{BINARY} validate --config #{caddyfile} 2>&1`
  return if $CHILD_STATUS.success?

  raise Error, translate_validate_error(out)
end

.wait_for_admin_endpoint(timeout: 5) ⇒ Boolean

Polls Caddy’s admin API until it responds or the timeout elapses.

Parameters:

  • timeout (Numeric) (defaults to: 5)

    seconds to wait before giving up

Returns:

  • (Boolean)

    true if the admin endpoint responded



87
88
89
90
91
92
93
94
95
# File 'lib/easy_caddy/caddy.rb', line 87

def self.wait_for_admin_endpoint(timeout: 5)
  deadline = Time.now + timeout
  until Time.now > deadline
    return true if admin_endpoint_reachable?

    sleep 0.25
  end
  false
end