Class: Nvoi::External::Dns::Cloudflare

Inherits:
Object
  • Object
show all
Defined in:
lib/nvoi/external/dns/cloudflare.rb

Overview

Cloudflare handles Cloudflare API operations for DNS and tunnels

Constant Summary collapse

BASE_URL =
"https://api.cloudflare.com/client/v4"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token, account_id) ⇒ Cloudflare

Returns a new instance of Cloudflare.



16
17
18
19
20
21
22
23
24
# File 'lib/nvoi/external/dns/cloudflare.rb', line 16

def initialize(token, )
  @token = token
  @account_id = 
  @conn = Faraday.new(url: BASE_URL) do |f|
    f.request :json
    f.response :json
    f.adapter Faraday.default_adapter
  end
end

Instance Attribute Details

#account_idObject (readonly)

Returns the value of attribute account_id.



14
15
16
# File 'lib/nvoi/external/dns/cloudflare.rb', line 14

def 
  @account_id
end

Instance Method Details

#create_dns_record(zone_id, name, record_type, content, proxied: true) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/nvoi/external/dns/cloudflare.rb', line 176

def create_dns_record(zone_id, name, record_type, content, proxied: true)
  url = "zones/#{zone_id}/dns_records"

  response = post(url, {
    type: record_type,
    name:,
    content:,
    proxied:,
    ttl: 1
  })

  result = response["result"]
  Types::Record.new(
    id: result["id"],
    type: result["type"],
    name: result["name"],
    content: result["content"],
    proxied: result["proxied"],
    ttl: result["ttl"]
  )
end

#create_or_update_dns_record(zone_id, name, record_type, content, proxied: true) ⇒ Object



220
221
222
223
224
225
226
227
228
# File 'lib/nvoi/external/dns/cloudflare.rb', line 220

def create_or_update_dns_record(zone_id, name, record_type, content, proxied: true)
  existing = find_dns_record(zone_id, name, record_type)

  if existing
    update_dns_record(zone_id, existing.id, name, record_type, content, proxied:)
  else
    create_dns_record(zone_id, name, record_type, content, proxied:)
  end
end

#create_tunnel(name) ⇒ Object

Tunnel operations



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/nvoi/external/dns/cloudflare.rb', line 28

def create_tunnel(name)
  url = "accounts/#{@account_id}/cfd_tunnel"
  tunnel_secret = generate_tunnel_secret

  response = post(url, {
    name:,
    tunnel_secret:,
    config_src: "cloudflare"
  })

  result = response["result"]
  Types::Tunnel::Record.new(
    id: result["id"],
    name: result["name"],
    token: result["token"]
  )
end

#delete_dns_record(zone_id, record_id) ⇒ Object



230
231
232
233
# File 'lib/nvoi/external/dns/cloudflare.rb', line 230

def delete_dns_record(zone_id, record_id)
  url = "zones/#{zone_id}/dns_records/#{record_id}"
  delete(url)
end

#delete_tunnel(tunnel_id) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/nvoi/external/dns/cloudflare.rb', line 111

def delete_tunnel(tunnel_id)
  # Clean up all connections first
  connections_url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/connections"
  begin
    delete(connections_url)
  rescue StandardError
    # Ignore connection cleanup errors
  end

  # Now delete the tunnel
  url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}"
  delete(url)
end

#find_dns_record(zone_id, name, record_type) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/nvoi/external/dns/cloudflare.rb', line 156

def find_dns_record(zone_id, name, record_type)
  url = "zones/#{zone_id}/dns_records"
  response = get(url)

  results = response["result"]
  return nil unless results

  record_data = results.find { |r| r["name"] == name && r["type"] == record_type }
  return nil unless record_data

  Types::Record.new(
    id: record_data["id"],
    type: record_data["type"],
    name: record_data["name"],
    content: record_data["content"],
    proxied: record_data["proxied"],
    ttl: record_data["ttl"]
  )
end

#find_tunnel(name) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/nvoi/external/dns/cloudflare.rb', line 46

def find_tunnel(name)
  url = "accounts/#{@account_id}/cfd_tunnel"
  response = get(url, { name:, is_deleted: false })

  results = response["result"]
  return nil if results.nil? || results.empty?

  result = results[0]
  Types::Tunnel::Record.new(
    id: result["id"],
    name: result["name"],
    token: result["token"]
  )
end

#find_zone(domain) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/nvoi/external/dns/cloudflare.rb', line 137

def find_zone(domain)
  url = "zones"
  response = get(url)

  results = response["result"]
  return nil unless results

  zone_data = results.find { |z| z["name"] == domain }
  return nil unless zone_data

  Types::Zone.new(id: zone_data["id"], name: zone_data["name"])
end

#get_tunnel_token(tunnel_id) ⇒ Object



61
62
63
64
65
# File 'lib/nvoi/external/dns/cloudflare.rb', line 61

def get_tunnel_token(tunnel_id)
  url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/token"
  response = get(url)
  response["result"]
end

#list_zonesObject

DNS operations



127
128
129
130
131
132
133
134
135
# File 'lib/nvoi/external/dns/cloudflare.rb', line 127

def list_zones
  url = "zones"
  response = get(url)

  results = response["result"] || []
  results.map do |z|
    { id: z["id"], name: z["name"], status: z["status"] }
  end
end

#subdomain_available?(zone_id, subdomain, domain) ⇒ Boolean

Returns:

  • (Boolean)


150
151
152
153
154
# File 'lib/nvoi/external/dns/cloudflare.rb', line 150

def subdomain_available?(zone_id, subdomain, domain)
  fqdn = subdomain.empty? ? domain : "#{subdomain}.#{domain}"
  # Check for CNAME or A record
  !find_dns_record(zone_id, fqdn, "CNAME") && !find_dns_record(zone_id, fqdn, "A")
end

#update_dns_record(zone_id, record_id, name, record_type, content, proxied: true) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/nvoi/external/dns/cloudflare.rb', line 198

def update_dns_record(zone_id, record_id, name, record_type, content, proxied: true)
  url = "zones/#{zone_id}/dns_records/#{record_id}"

  response = patch(url, {
    type: record_type,
    name:,
    content:,
    proxied:,
    ttl: 1
  })

  result = response["result"]
  Types::Record.new(
    id: result["id"],
    type: result["type"],
    name: result["name"],
    content: result["content"],
    proxied: result["proxied"],
    ttl: result["ttl"]
  )
end

#update_tunnel_configuration(tunnel_id, hostnames, service_url) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/nvoi/external/dns/cloudflare.rb', line 67

def update_tunnel_configuration(tunnel_id, hostnames, service_url)
  hostnames = Array(hostnames)
  url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"

  ingress_rules = hostnames.map do |hostname|
    rule = { hostname:, service: service_url }
    # Only set httpHostHeader for non-wildcard hostnames
    # Wildcards should pass through the original Host header
    rule[:originRequest] = { httpHostHeader: hostname } unless hostname.start_with?("*.")
    rule
  end
  ingress_rules << { service: "http_status:404" }

  config = { ingress: ingress_rules }
  put(url, { config: })
end

#validate_credentialsObject

Validation



237
238
239
240
241
242
# File 'lib/nvoi/external/dns/cloudflare.rb', line 237

def validate_credentials
  get("user/tokens/verify")
  true
rescue Errors::CloudflareError => e
  raise Errors::ValidationError, "cloudflare credentials invalid: #{e.message}"
end

#verify_tunnel_configuration(tunnel_id, expected_hostnames, expected_service, max_attempts) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/nvoi/external/dns/cloudflare.rb', line 84

def verify_tunnel_configuration(tunnel_id, expected_hostnames, expected_service, max_attempts)
  expected_hostnames = Array(expected_hostnames)
  url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"

  max_attempts.times do
    begin
      response = get(url)

      if response["success"]
        config = response.dig("result", "config")
        ingress = config&.dig("ingress") || []
        configured_hostnames = ingress.map { |r| r["hostname"] }.compact

        if expected_hostnames.all? { |h| configured_hostnames.include?(h) }
          return true
        end
      end
    rescue StandardError
      # Continue retrying
    end

    sleep(2)
  end

  raise Errors::TunnelError, "tunnel configuration not propagated after #{max_attempts} attempts"
end