• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 25358587425

05 May 2026 04:55AM UTC coverage: 59.127% (+5.6%) from 53.574%
25358587425

push

github

web-flow
Merge pull request #17267 from miodvallat/backport-17000-to-auth-4.9.x

auth 4.9: backport "correctly delete ENT records from the API"

12108 of 27470 branches covered (44.08%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

1502 existing lines in 22 files now uncovered.

39445 of 59721 relevant lines covered (66.05%)

3052393.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/modules/lua2backend/lua2api2.hh
1
/*
2
 * This file is part of PowerDNS or dnsdist.
3
 * Copyright -- PowerDNS.COM B.V. and its contributors
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of version 2 of the GNU General Public License as
7
 * published by the Free Software Foundation.
8
 *
9
 * In addition, for the avoidance of any doubt, permission is granted to
10
 * link this program with OpenSSL and to (re)distribute the binaries
11
 * produced as the result of such linking.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTAPILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
 */
22
#pragma once
23
#include "boost/lexical_cast.hpp"
24
#include "boost/algorithm/string/join.hpp"
25
#include "pdns/arguments.hh"
26

27
class Lua2BackendAPIv2 : public DNSBackend, AuthLua4
28
{
29
private:
30
  typedef std::function<void()> init_call_t;
31
  typedef std::function<void()> deinit_call_t;
32

33
  typedef std::vector<std::pair<string, string>> lookup_context_t;
34

35
  typedef std::vector<std::pair<int, std::vector<std::pair<string, boost::variant<bool, int, DNSName, string, QType>>>>> lookup_result_t;
36
  typedef std::function<lookup_result_t(const QType& qtype, const DNSName& qname, int domain_id, const lookup_context_t& ctx)> lookup_call_t;
37

38
  typedef boost::variant<bool, lookup_result_t> list_result_t;
39
  typedef std::function<list_result_t(const DNSName& qname, int domain_id)> list_call_t;
40

41
  typedef vector<pair<string, boost::variant<bool, long, string, vector<string>>>> domaininfo_result_t;
42
  typedef boost::variant<bool, domaininfo_result_t> get_domaininfo_result_t;
43
  typedef vector<pair<DNSName, domaininfo_result_t>> get_all_domains_result_t;
44
  typedef std::function<get_domaininfo_result_t(const DNSName& domain)> get_domaininfo_call_t;
45
  typedef std::function<get_all_domains_result_t()> get_all_domains_call_t;
46

47
  typedef vector<pair<int, string>> domain_metadata_result_t;
48
  typedef boost::variant<bool, domain_metadata_result_t> get_domain_metadata_result_t;
49
  typedef boost::variant<bool, vector<pair<string, domain_metadata_result_t>>> get_all_domain_metadata_result_t;
50
  typedef std::function<get_domain_metadata_result_t(const DNSName& domain, const string& kind)> get_domain_metadata_call_t;
51
  typedef std::function<get_all_domain_metadata_result_t(const DNSName& domain)> get_all_domain_metadata_call_t;
52

53
  typedef vector<pair<string, boost::variant<bool, int, string>>> keydata_result_t;
54
  typedef boost::variant<bool, vector<pair<int, keydata_result_t>>> get_domain_keys_result_t;
55
  typedef std::function<get_domain_keys_result_t(const DNSName& domain)> get_domain_keys_call_t;
56

57
  typedef std::vector<std::pair<string, boost::variant<string, DNSName>>> before_and_after_names_result_t;
58
  typedef boost::variant<bool, before_and_after_names_result_t> get_before_and_after_names_absolute_result_t;
59
  typedef std::function<get_before_and_after_names_absolute_result_t(int id, const DNSName& qname)> get_before_and_after_names_absolute_call_t;
60

61
  typedef std::function<void(int, long)> set_notified_call_t;
62

63
  typedef std::function<string(const string& cmd)> direct_backend_cmd_call_t;
64

65
public:
66
  Lua2BackendAPIv2(const string& suffix)
UNCOV
67
  {
×
UNCOV
68
    setArgPrefix("lua2" + suffix);
×
UNCOV
69
    d_debug_log = mustDo("query-logging");
×
UNCOV
70
    prepareContext();
×
UNCOV
71
    loadFile(getArg("filename"));
×
UNCOV
72
  }
×
73

74
  ~Lua2BackendAPIv2() override;
75

76
#define logCall(func, var)                                                                               \
UNCOV
77
  {                                                                                                      \
×
UNCOV
78
    if (d_debug_log) {                                                                                   \
×
79
      g_log << Logger::Debug << "[" << getPrefix() << "] Calling " << func << "(" << var << ")" << endl; \
×
80
    }                                                                                                    \
×
UNCOV
81
  }
×
82
#define logResult(var)                                                \
UNCOV
83
  {                                                                   \
×
UNCOV
84
    if (d_debug_log) {                                                \
×
85
      g_log << Logger::Debug << "[" << getPrefix() << "] Got result " \
×
86
            << "'" << var << "'" << endl;                             \
×
87
    }                                                                 \
×
UNCOV
88
  }
×
89

90
  void postPrepareContext() override
UNCOV
91
  {
×
UNCOV
92
    AuthLua4::postPrepareContext();
×
UNCOV
93
  }
×
94

95
  void postLoad() override
UNCOV
96
  {
×
UNCOV
97
    f_lookup = d_lw->readVariable<boost::optional<lookup_call_t>>("dns_lookup").get_value_or(0);
×
UNCOV
98
    f_list = d_lw->readVariable<boost::optional<list_call_t>>("dns_list").get_value_or(0);
×
UNCOV
99
    f_get_all_domains = d_lw->readVariable<boost::optional<get_all_domains_call_t>>("dns_get_all_domains").get_value_or(0);
×
UNCOV
100
    f_get_domaininfo = d_lw->readVariable<boost::optional<get_domaininfo_call_t>>("dns_get_domaininfo").get_value_or(0);
×
UNCOV
101
    f_get_domain_metadata = d_lw->readVariable<boost::optional<get_domain_metadata_call_t>>("dns_get_domain_metadata").get_value_or(0);
×
UNCOV
102
    f_get_all_domain_metadata = d_lw->readVariable<boost::optional<get_all_domain_metadata_call_t>>("dns_get_all_domain_metadata").get_value_or(0);
×
UNCOV
103
    f_get_domain_keys = d_lw->readVariable<boost::optional<get_domain_keys_call_t>>("dns_get_domain_keys").get_value_or(0);
×
UNCOV
104
    f_get_before_and_after_names_absolute = d_lw->readVariable<boost::optional<get_before_and_after_names_absolute_call_t>>("dns_get_before_and_after_names_absolute").get_value_or(0);
×
UNCOV
105
    f_set_notified = d_lw->readVariable<boost::optional<set_notified_call_t>>("dns_set_notified").get_value_or(0);
×
106

UNCOV
107
    auto init = d_lw->readVariable<boost::optional<init_call_t>>("dns_init").get_value_or(0);
×
UNCOV
108
    if (init)
×
109
      init();
×
110

UNCOV
111
    f_deinit = d_lw->readVariable<boost::optional<deinit_call_t>>("dns_deinit").get_value_or(0);
×
112

UNCOV
113
    if (f_lookup == nullptr)
×
114
      throw PDNSException("dns_lookup missing");
×
115

116
    /* see if dnssec support is wanted */
UNCOV
117
    d_dnssec = d_lw->readVariable<boost::optional<bool>>("dns_dnssec").get_value_or(false);
×
UNCOV
118
    if (d_dnssec) {
×
UNCOV
119
      if (f_get_domain_metadata == nullptr)
×
120
        throw PDNSException("dns_dnssec is true but dns_get_domain_metadata is missing");
×
UNCOV
121
      if (f_get_before_and_after_names_absolute == nullptr)
×
122
        throw PDNSException("dns_dnssec is true but dns_get_before_and_after_names_absolute is missing");
×
123
      /* domain keys is not strictly speaking necessary for dnssec backend */
UNCOV
124
      if (f_get_domain_keys == nullptr)
×
125
        g_log << Logger::Warning << "dns_get_domain_keys missing - cannot do live signing" << endl;
×
UNCOV
126
    }
×
UNCOV
127
  }
×
128

129
  bool doesDNSSEC() override
130
  {
×
131
    return d_dnssec;
×
132
  }
×
133

134
  void parseLookup(const lookup_result_t& result)
UNCOV
135
  {
×
UNCOV
136
    for (const auto& row : result) {
×
UNCOV
137
      DNSResourceRecord rec;
×
UNCOV
138
      for (const auto& item : row.second) {
×
UNCOV
139
        if (item.first == "type") {
×
UNCOV
140
          if (item.second.which() == 1)
×
141
            rec.qtype = QType(boost::get<int>(item.second));
×
UNCOV
142
          else if (item.second.which() == 3)
×
143
            rec.qtype = boost::get<string>(item.second);
×
UNCOV
144
          else if (item.second.which() == 4)
×
UNCOV
145
            rec.qtype = boost::get<QType>(item.second);
×
146
          else
×
147
            throw PDNSException("Unsupported value for type");
×
UNCOV
148
        }
×
UNCOV
149
        else if (item.first == "name") {
×
UNCOV
150
          if (item.second.which() == 3)
×
151
            rec.qname = DNSName(boost::get<string>(item.second));
×
UNCOV
152
          else if (item.second.which() == 2)
×
UNCOV
153
            rec.qname = boost::get<DNSName>(item.second);
×
154
          else
×
155
            throw PDNSException("Unsupported value for name");
×
UNCOV
156
        }
×
UNCOV
157
        else if (item.first == "domain_id")
×
UNCOV
158
          rec.domain_id = boost::get<int>(item.second);
×
UNCOV
159
        else if (item.first == "auth")
×
160
          rec.auth = boost::get<bool>(item.second);
×
UNCOV
161
        else if (item.first == "last_modified")
×
162
          rec.last_modified = static_cast<time_t>(boost::get<int>(item.second));
×
UNCOV
163
        else if (item.first == "ttl")
×
UNCOV
164
          rec.ttl = boost::get<int>(item.second);
×
UNCOV
165
        else if (item.first == "content")
×
UNCOV
166
          rec.setContent(boost::get<string>(item.second));
×
167
        else if (item.first == "scopeMask")
×
168
          rec.scopeMask = boost::get<int>(item.second);
×
169
        else
×
170
          g_log << Logger::Warning << "Unsupported key '" << item.first << "' in lookup or list result" << endl;
×
UNCOV
171
      }
×
UNCOV
172
      logResult(rec.qname << " IN " << rec.qtype.toString() << " " << rec.ttl << " " << rec.getZoneRepresentation());
×
UNCOV
173
      d_result.push_back(rec);
×
UNCOV
174
    }
×
UNCOV
175
    if (d_result.empty() && d_debug_log)
×
176
      g_log << Logger::Debug << "[" << getPrefix() << "] Got empty result" << endl;
×
UNCOV
177
  }
×
178

179
  bool list(const DNSName& target, int domain_id, bool /* include_disabled */ = false) override
UNCOV
180
  {
×
UNCOV
181
    if (f_list == nullptr) {
×
182
      g_log << Logger::Error << "[" << getPrefix() << "] dns_list missing - cannot do AXFR" << endl;
×
183
      return false;
×
184
    }
×
185

UNCOV
186
    if (d_result.size() != 0)
×
187
      throw PDNSException("list attempted while another was running");
×
188

UNCOV
189
    logCall("list", "target=" << target << ",domain_id=" << domain_id);
×
UNCOV
190
    list_result_t result = f_list(target, domain_id);
×
191

UNCOV
192
    if (result.which() == 0)
×
193
      return false;
×
194

UNCOV
195
    parseLookup(boost::get<lookup_result_t>(result));
×
196

UNCOV
197
    return true;
×
UNCOV
198
  }
×
199

200
  void lookup(const QType& qtype, const DNSName& qname, int domain_id, DNSPacket* p = nullptr) override
UNCOV
201
  {
×
UNCOV
202
    if (d_result.size() != 0)
×
203
      throw PDNSException("lookup attempted while another was running");
×
204

UNCOV
205
    lookup_context_t ctx;
×
UNCOV
206
    if (p != NULL) {
×
UNCOV
207
      ctx.emplace_back(lookup_context_t::value_type{"source_address", p->getInnerRemote().toString()});
×
UNCOV
208
      ctx.emplace_back(lookup_context_t::value_type{"real_source_address", p->getRealRemote().toString()});
×
UNCOV
209
    }
×
210

UNCOV
211
    logCall("lookup", "qtype=" << qtype.toString() << ",qname=" << qname << ",domain_id=" << domain_id);
×
UNCOV
212
    lookup_result_t result = f_lookup(qtype, qname, domain_id, ctx);
×
UNCOV
213
    parseLookup(result);
×
UNCOV
214
  }
×
215

216
  bool get(DNSResourceRecord& rr) override
UNCOV
217
  {
×
UNCOV
218
    if (d_result.size() == 0)
×
UNCOV
219
      return false;
×
UNCOV
220
    rr = std::move(d_result.front());
×
UNCOV
221
    d_result.pop_front();
×
UNCOV
222
    return true;
×
UNCOV
223
  }
×
224

225
  string directBackendCmd(const string& querystr) override
226
  {
×
227
    string::size_type pos = querystr.find_first_of(" \t");
×
228
    string cmd = querystr;
×
229
    string par = "";
×
230
    if (pos != string::npos) {
×
231
      cmd = querystr.substr(0, pos);
×
232
      par = querystr.substr(pos + 1);
×
233
    }
×
234
    direct_backend_cmd_call_t f = d_lw->readVariable<boost::optional<direct_backend_cmd_call_t>>(cmd).get_value_or(0);
×
235
    if (f == nullptr) {
×
236
      return cmd + "not found";
×
237
    }
×
238
    logCall(cmd, "parameter=" << par);
×
239
    return f(par);
×
240
  }
×
241

242
  void setNotified(uint32_t id, uint32_t serial) override
243
  {
×
244
    if (f_set_notified == NULL)
×
245
      return;
×
246
    logCall("dns_set_notified", "id=" << static_cast<int>(id) << ",serial=" << serial);
×
247
    f_set_notified(static_cast<int>(id), serial);
×
248
  }
×
249

250
  void parseDomainInfo(const domaininfo_result_t& row, DomainInfo& di)
UNCOV
251
  {
×
UNCOV
252
    for (const auto& item : row) {
×
UNCOV
253
      if (item.first == "account")
×
254
        di.account = boost::get<string>(item.second);
×
UNCOV
255
      else if (item.first == "last_check")
×
256
        di.last_check = static_cast<time_t>(boost::get<long>(item.second));
×
UNCOV
257
      else if (item.first == "masters")
×
258
        for (const auto& primary : boost::get<vector<string>>(item.second))
×
259
          di.primaries.push_back(ComboAddress(primary, 53));
×
UNCOV
260
      else if (item.first == "id")
×
UNCOV
261
        di.id = static_cast<int>(boost::get<long>(item.second));
×
UNCOV
262
      else if (item.first == "notified_serial")
×
263
        di.notified_serial = static_cast<unsigned int>(boost::get<long>(item.second));
×
UNCOV
264
      else if (item.first == "serial")
×
UNCOV
265
        di.serial = static_cast<unsigned int>(boost::get<long>(item.second));
×
266
      else if (item.first == "kind")
×
267
        di.kind = DomainInfo::stringToKind(boost::get<string>(item.second));
×
268
      else
×
269
        g_log << Logger::Warning << "Unsupported key '" << item.first << "' in domaininfo result" << endl;
×
UNCOV
270
    }
×
UNCOV
271
    di.backend = this;
×
UNCOV
272
    logResult("zone=" << di.zone << ",serial=" << di.serial << ",kind=" << di.getKindString());
×
UNCOV
273
  }
×
274

275
  bool getDomainInfo(const DNSName& domain, DomainInfo& di, bool /* getSerial */ = true) override
UNCOV
276
  {
×
UNCOV
277
    if (f_get_domaininfo == nullptr) {
×
278
      // use getAuth instead
UNCOV
279
      SOAData sd;
×
UNCOV
280
      if (!getAuth(domain, &sd))
×
281
        return false;
×
282

UNCOV
283
      di.zone = domain;
×
UNCOV
284
      di.backend = this;
×
UNCOV
285
      di.serial = sd.serial;
×
UNCOV
286
      return true;
×
UNCOV
287
    }
×
288

UNCOV
289
    logCall("get_domaininfo", "domain=" << domain);
×
UNCOV
290
    get_domaininfo_result_t result = f_get_domaininfo(domain);
×
291

UNCOV
292
    if (result.which() == 0)
×
293
      return false;
×
294

UNCOV
295
    di.zone = domain;
×
UNCOV
296
    parseDomainInfo(boost::get<domaininfo_result_t>(result), di);
×
297

UNCOV
298
    return true;
×
UNCOV
299
  }
×
300

301
  void getAllDomains(vector<DomainInfo>* domains, bool /* getSerial */, bool /* include_disabled */) override
UNCOV
302
  {
×
UNCOV
303
    if (f_get_all_domains == nullptr)
×
304
      return;
×
305

UNCOV
306
    logCall("get_all_domains", "");
×
UNCOV
307
    for (const auto& row : f_get_all_domains()) {
×
UNCOV
308
      DomainInfo di;
×
UNCOV
309
      di.zone = row.first;
×
UNCOV
310
      logResult(di.zone);
×
UNCOV
311
      parseDomainInfo(row.second, di);
×
UNCOV
312
      domains->push_back(di);
×
UNCOV
313
    }
×
UNCOV
314
  }
×
315

316
  bool getAllDomainMetadata(const DNSName& name, std::map<std::string, std::vector<std::string>>& meta) override
UNCOV
317
  {
×
UNCOV
318
    if (f_get_all_domain_metadata == nullptr)
×
UNCOV
319
      return false;
×
320

321
    logCall("get_all_domain_metadata", "name=" << name);
×
322
    get_all_domain_metadata_result_t result = f_get_all_domain_metadata(name);
×
323
    if (result.which() == 0)
×
324
      return false;
×
325

326
    for (const auto& row : boost::get<vector<pair<string, domain_metadata_result_t>>>(result)) {
×
327
      meta[row.first].clear();
×
328
      for (const auto& item : row.second)
×
329
        meta[row.first].push_back(item.second);
×
330
      logResult("kind=" << row.first << ",value=" << boost::algorithm::join(meta[row.first], ", "));
×
331
    }
×
332

333
    return true;
×
334
  }
×
335

336
  bool getDomainMetadata(const DNSName& name, const std::string& kind, std::vector<std::string>& meta) override
337
  {
×
338
    if (f_get_domain_metadata == nullptr)
×
339
      return false;
×
340

341
    logCall("get_domain_metadata", "name=" << name << ",kind=" << kind);
×
342
    get_domain_metadata_result_t result = f_get_domain_metadata(name, kind);
×
343
    if (result.which() == 0)
×
344
      return false;
×
345

346
    meta.clear();
×
347
    for (const auto& item : boost::get<domain_metadata_result_t>(result))
×
348
      meta.push_back(item.second);
×
349

350
    logResult("value=" << boost::algorithm::join(meta, ", "));
×
351
    return true;
×
352
  }
×
353

354
  bool getDomainKeys(const DNSName& name, std::vector<DNSBackend::KeyData>& keys) override
UNCOV
355
  {
×
UNCOV
356
    if (f_get_domain_keys == nullptr)
×
UNCOV
357
      return false;
×
358

UNCOV
359
    logCall("get_domain_keys", "name=" << name);
×
UNCOV
360
    get_domain_keys_result_t result = f_get_domain_keys(name);
×
361

UNCOV
362
    if (result.which() == 0)
×
363
      return false;
×
364

UNCOV
365
    for (const auto& row : boost::get<vector<pair<int, keydata_result_t>>>(result)) {
×
UNCOV
366
      DNSBackend::KeyData key;
×
UNCOV
367
      key.published = true;
×
UNCOV
368
      for (const auto& item : row.second) {
×
UNCOV
369
        if (item.first == "content")
×
UNCOV
370
          key.content = boost::get<string>(item.second);
×
UNCOV
371
        else if (item.first == "id")
×
UNCOV
372
          key.id = static_cast<unsigned int>(boost::get<int>(item.second));
×
UNCOV
373
        else if (item.first == "flags")
×
UNCOV
374
          key.flags = static_cast<unsigned int>(boost::get<int>(item.second));
×
UNCOV
375
        else if (item.first == "active")
×
UNCOV
376
          key.active = boost::get<bool>(item.second);
×
377
        else if (item.first == "published")
×
378
          key.published = boost::get<bool>(item.second);
×
379
        else
×
380
          g_log << Logger::Warning << "[" << getPrefix() << "] Unsupported key '" << item.first << "' in keydata result" << endl;
×
UNCOV
381
      }
×
UNCOV
382
      logResult("id=" << key.id << ",flags=" << key.flags << ",active=" << (key.active ? "true" : "false") << ",published=" << (key.published ? "true" : "false"));
×
UNCOV
383
      keys.push_back(key);
×
UNCOV
384
    }
×
385

UNCOV
386
    return true;
×
UNCOV
387
  }
×
388

389
  bool getBeforeAndAfterNamesAbsolute(uint32_t id, const DNSName& qname, DNSName& unhashed, DNSName& before, DNSName& after) override
UNCOV
390
  {
×
UNCOV
391
    if (f_get_before_and_after_names_absolute == nullptr)
×
392
      return false;
×
393

UNCOV
394
    logCall("get_before_and_after_names_absolute", "id=<<" << id << ",qname=" << qname);
×
UNCOV
395
    get_before_and_after_names_absolute_result_t result = f_get_before_and_after_names_absolute(id, qname);
×
396

UNCOV
397
    if (result.which() == 0)
×
398
      return false;
×
399

UNCOV
400
    before_and_after_names_result_t row = boost::get<before_and_after_names_result_t>(result);
×
UNCOV
401
    if (row.size() != 3) {
×
402
      g_log << Logger::Error << "Invalid result from dns_get_before_and_after_names_absolute, expected array with 3 items, got " << row.size() << "item(s)" << endl;
×
403
      return false;
×
404
    }
×
UNCOV
405
    for (const auto& item : row) {
×
UNCOV
406
      DNSName value;
×
UNCOV
407
      if (item.second.which() == 0)
×
408
        value = DNSName(boost::get<string>(item.second));
×
UNCOV
409
      else
×
UNCOV
410
        value = DNSName(boost::get<DNSName>(item.second));
×
UNCOV
411
      if (item.first == "unhashed")
×
UNCOV
412
        unhashed = value;
×
UNCOV
413
      else if (item.first == "before")
×
UNCOV
414
        before = value;
×
UNCOV
415
      else if (item.first == "after")
×
UNCOV
416
        after = value;
×
417
      else {
×
418
        g_log << Logger::Error << "Invalid result from dns_get_before_and_after_names_absolute, unexpected key " << item.first << endl;
×
419
        return false;
×
420
      }
×
UNCOV
421
    }
×
422

UNCOV
423
    logResult("unhashed=" << unhashed << ",before=" << before << ",after=" << after);
×
UNCOV
424
    return true;
×
UNCOV
425
  }
×
426

427
private:
428
  std::list<DNSResourceRecord> d_result;
429
  bool d_debug_log{false};
430
  bool d_dnssec{false};
431

432
  lookup_call_t f_lookup;
433
  list_call_t f_list;
434

435
  get_domaininfo_call_t f_get_domaininfo;
436
  get_all_domains_call_t f_get_all_domains;
437

438
  get_domain_metadata_call_t f_get_domain_metadata;
439
  get_all_domain_metadata_call_t f_get_all_domain_metadata;
440

441
  get_domain_keys_call_t f_get_domain_keys;
442

443
  get_before_and_after_names_absolute_call_t f_get_before_and_after_names_absolute;
444

445
  set_notified_call_t f_set_notified;
446

447
  deinit_call_t f_deinit;
448
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc