Selaa lähdekoodia

POR-1703: add support for cloudflare dns (#3559)

jose-fully-ported 2 vuotta sitten
vanhempi
sitoutus
e855b9f05c

+ 7 - 0
api/server/shared/config/env/envconfs.go

@@ -89,6 +89,13 @@ type ServerConf struct {
 
 
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 
 
+	// DnsProvider controls which provider to use for dns (powerdns or cloudflare)
+	// Setting this to empty string will disable external dns
+	DnsProvider string `env:"DNS_PROVIDER,default=powerdns"`
+
+	// Cloudflare API Key
+	CloudflareAPIToken string `env:"CLOUDFLARE_API_TOKEN"`
+
 	// PowerDNS client API key and the host of the PowerDNS API server
 	// PowerDNS client API key and the host of the PowerDNS API server
 	PowerDNSAPIServerURL string `env:"POWER_DNS_API_SERVER_URL"`
 	PowerDNSAPIServerURL string `env:"POWER_DNS_API_SERVER_URL"`
 	PowerDNSAPIKey       string `env:"POWER_DNS_API_KEY"`
 	PowerDNSAPIKey       string `env:"POWER_DNS_API_KEY"`

+ 15 - 2
api/server/shared/config/loader/loader.go

@@ -25,6 +25,7 @@ import (
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
+	"github.com/porter-dev/porter/internal/integrations/cloudflare"
 	"github.com/porter-dev/porter/internal/integrations/dns"
 	"github.com/porter-dev/porter/internal/integrations/dns"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/notifier"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -304,8 +305,20 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
 	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
 	res.Logger.Info().Msg("Created analytics client")
 	res.Logger.Info().Msg("Created analytics client")
 
 
-	if sc.PowerDNSAPIKey != "" && sc.PowerDNSAPIServerURL != "" {
-		res.DNSClient = &dns.Client{Client: powerdns.NewClient(sc.PowerDNSAPIServerURL, sc.PowerDNSAPIKey, sc.AppRootDomain)}
+	switch sc.DnsProvider {
+	case "powerdns":
+		if sc.PowerDNSAPIKey != "" && sc.PowerDNSAPIServerURL != "" {
+			res.DNSClient = &dns.Client{Client: powerdns.NewClient(sc.PowerDNSAPIServerURL, sc.PowerDNSAPIKey, sc.AppRootDomain)}
+		}
+	case "cloudflare":
+		if sc.CloudflareAPIToken != "" {
+			cloudflareClient, err := cloudflare.NewClient(sc.CloudflareAPIToken, sc.AppRootDomain)
+			if err != nil {
+				return res, fmt.Errorf("unable to create cloudflare client: %w", err)
+			}
+
+			res.DNSClient = &dns.Client{Client: cloudflareClient}
+		}
 	}
 	}
 
 
 	res.EnableCAPIProvisioner = sc.EnableCAPIProvisioner
 	res.EnableCAPIProvisioner = sc.EnableCAPIProvisioner

+ 8 - 6
go.mod

@@ -46,8 +46,8 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.0
 	github.com/spf13/viper v1.10.0
 	github.com/stretchr/testify v1.8.4
 	github.com/stretchr/testify v1.8.4
-	golang.org/x/crypto v0.6.0
-	golang.org/x/net v0.10.0
+	golang.org/x/crypto v0.12.0
+	golang.org/x/net v0.14.0
 	golang.org/x/oauth2 v0.8.0
 	golang.org/x/oauth2 v0.8.0
 	google.golang.org/api v0.114.0
 	google.golang.org/api v0.114.0
 	google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc
 	google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc
@@ -123,6 +123,7 @@ require (
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
+	github.com/cloudflare/cloudflare-go v0.76.0 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
@@ -131,12 +132,13 @@ require (
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/google/gnostic v0.6.9 // indirect
 	github.com/google/gnostic v0.6.9 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/launchdarkly/ccache v1.1.0 // indirect
 	github.com/launchdarkly/ccache v1.1.0 // indirect
 	github.com/launchdarkly/eventsource v1.6.2 // indirect
 	github.com/launchdarkly/eventsource v1.6.2 // indirect
@@ -341,9 +343,9 @@ require (
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
 	golang.org/x/mod v0.8.0 // indirect
 	golang.org/x/mod v0.8.0 // indirect
 	golang.org/x/sync v0.1.0
 	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.8.0 // indirect
-	golang.org/x/term v0.8.0 // indirect
-	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
+	golang.org/x/term v0.11.0 // indirect
+	golang.org/x/text v0.12.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect

+ 17 - 0
go.sum

@@ -360,6 +360,8 @@ github.com/cli/oauth v0.8.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A
 github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
 github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
 github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
 github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/cloudflare-go v0.76.0 h1:0YsPw0KHWJK2fpivzx38X5onurfM0GkanZtS8HAqlj0=
+github.com/cloudflare/cloudflare-go v0.76.0/go.mod h1:5ocQT9qQ99QsT1Ii2751490Z5J+W/nv6jOj+lSAe4ug=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -733,6 +735,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@@ -957,6 +961,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo=
 github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo=
+github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
@@ -965,6 +970,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
 github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
 github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
+github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@@ -1988,6 +1995,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
 golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -2098,6 +2107,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2264,6 +2275,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2274,6 +2287,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
 golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2286,6 +2301,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

+ 89 - 0
internal/integrations/cloudflare/cloudflare.go

@@ -0,0 +1,89 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/cloudflare/cloudflare-go"
+	"github.com/porter-dev/porter/internal/integrations/dns"
+)
+
+// RecordType strongly types cloudflare dns entry types
+type RecordType string
+
+const (
+	// RecordType_A declares an A record type for cloudflare
+	RecordType_A RecordType = "A"
+
+	// RecordType_CNAME declares an CNME record type for cloudflare
+	RecordType_CNAME = "CNAME"
+)
+
+// TTL sets the TTL for Cloudflare DNS records
+const TTL = 300
+
+// Client is a struct wrapper around the cloudflare client
+type Client struct {
+	zoneID string
+
+	client *cloudflare.API
+}
+
+// NewClient creates a new cloudflare API client
+func NewClient(apiToken string, runDomain string) (Client, error) {
+	client, err := cloudflare.NewWithAPIToken(apiToken)
+	if err != nil {
+		return Client{}, err
+	}
+
+	zoneID, err := client.ZoneIDByName(runDomain)
+	if err != nil {
+		return Client{}, err
+	}
+
+	return Client{client: client, zoneID: zoneID}, nil
+}
+
+// CreateCNAMERecord creates a new CNAME record for the nameserver
+//
+// The method ignores record.RootDomain in favor of the zoneID derived from c.runDomain
+func (c Client) CreateCNAMERecord(record dns.Record) error {
+	proxy := false
+
+	cloudflareRecord := cloudflare.CreateDNSRecordParams{
+		Name:    record.Name,
+		Type:    string(RecordType_CNAME),
+		Content: record.Value,
+		TTL:     TTL,
+		Proxied: &proxy,
+	}
+
+	_, err := c.client.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(c.zoneID), cloudflareRecord)
+	if err != nil {
+		return fmt.Errorf("failed to create CNAME dns record: %w", err)
+	}
+
+	return err
+}
+
+// CreateARecord creates a new A record for the nameserver
+//
+// The method ignores record.RootDomain in favor of the zoneID derived from c.runDomain
+func (c Client) CreateARecord(record dns.Record) error {
+	proxy := false
+
+	cloudflareRecord := cloudflare.CreateDNSRecordParams{
+		Name:    record.Name,
+		Type:    string(RecordType_A),
+		Content: record.Value,
+		TTL:     TTL,
+		Proxied: &proxy,
+	}
+
+	_, err := c.client.CreateDNSRecord(context.Background(), cloudflare.ZoneIdentifier(c.zoneID), cloudflareRecord)
+	if err != nil {
+		return fmt.Errorf("failed to create A dns record: %w", err)
+	}
+
+	return nil
+}

+ 32 - 8
internal/integrations/dns/dns.go

@@ -1,19 +1,43 @@
 package dns
 package dns
 
 
-import "github.com/porter-dev/porter/internal/integrations/powerdns"
+// RecordType strongly types dns record types
+type RecordType int
+
+const (
+	// RecordType_A represents a DNS RecordType_A record
+	RecordType_A RecordType = iota
+
+	// RecordType_CNAME represents a DNS RecordType_CNAME record
+	RecordType_CNAME
+)
+
+// WrappedClient is an interface describing a wrapper
+// around a particular dns implementation
+type WrappedClient interface {
+	CreateARecord(record Record) error
+	CreateCNAMERecord(record Record) error
+}
 
 
 // Client wraps the underlying powerdns client
 // Client wraps the underlying powerdns client
 // providing a stable api around interacting with DNS
 // providing a stable api around interacting with DNS
 type Client struct {
 type Client struct {
-	Client *powerdns.Client
+	Client WrappedClient
 }
 }
 
 
-// CreateARecord creates a new A record
-func (c Client) CreateARecord(value, hostname string) error {
-	return c.Client.CreateARecord(value, hostname)
+// Record describes a specific DNS record to create
+// and can include implementation-specific attributes
+type Record struct {
+	Type       RecordType
+	Name       string
+	RootDomain string
+	Value      string
 }
 }
 
 
-// CreateCNAMERecord creates a new CNAME record
-func (c Client) CreateCNAMERecord(value, hostname string) error {
-	return c.Client.CreateCNAMERecord(value, hostname)
+// CreateRecord creates a new dns record
+func (c Client) CreateRecord(record Record) error {
+	if record.Type == RecordType_A {
+		return c.Client.CreateARecord(record)
+	}
+
+	return c.Client.CreateCNAMERecord(record)
 }
 }

+ 15 - 10
internal/integrations/powerdns/powerdns.go

@@ -3,11 +3,13 @@ package powerdns
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
 	"time"
 	"time"
+
+	"github.com/porter-dev/porter/internal/integrations/dns"
 )
 )
 
 
 // Client contains an API client for a PowerDNS server
 // Client contains an API client for a PowerDNS server
@@ -20,12 +22,12 @@ type Client struct {
 }
 }
 
 
 // NewClient creates a new bind API client
 // NewClient creates a new bind API client
-func NewClient(serverURL, apiKey, runDomain string) *Client {
+func NewClient(serverURL, apiKey, runDomain string) Client {
 	httpClient := &http.Client{
 	httpClient := &http.Client{
 		Timeout: time.Minute,
 		Timeout: time.Minute,
 	}
 	}
 
 
-	return &Client{apiKey, serverURL, runDomain, httpClient}
+	return Client{apiKey, serverURL, runDomain, httpClient}
 }
 }
 
 
 // RecordData represents the data required to create or delete an A/CNAME record
 // RecordData represents the data required to create or delete an A/CNAME record
@@ -34,6 +36,7 @@ type RecordData struct {
 	RRSets []RR `json:"rrsets"`
 	RRSets []RR `json:"rrsets"`
 }
 }
 
 
+// RR represents a dns resource record collection for PowerDNS
 type RR struct {
 type RR struct {
 	Name       string   `json:"name"`
 	Name       string   `json:"name"`
 	Type       string   `json:"type"`
 	Type       string   `json:"type"`
@@ -42,6 +45,8 @@ type RR struct {
 	Records    []Record `json:"records"`
 	Records    []Record `json:"records"`
 }
 }
 
 
+// Record represents an individual record for a given
+// PowerDNS resource record
 type Record struct {
 type Record struct {
 	Content  string `json:"content"`
 	Content  string `json:"content"`
 	Disabled bool   `json:"disabled"`
 	Disabled bool   `json:"disabled"`
@@ -51,9 +56,9 @@ type Record struct {
 }
 }
 
 
 // CreateCNAMERecord creates a new CNAME record for the nameserver
 // CreateCNAMERecord creates a new CNAME record for the nameserver
-func (c *Client) CreateCNAMERecord(value, hostname string) error {
-	valueC := canonicalize(value)
-	hostnameC := canonicalize(hostname)
+func (c Client) CreateCNAMERecord(record dns.Record) error {
+	valueC := canonicalize(record.Value)
+	hostnameC := canonicalize(fmt.Sprintf("%s.%s", record.Name, record.RootDomain))
 
 
 	return c.sendRequest("PATCH", &RecordData{
 	return c.sendRequest("PATCH", &RecordData{
 		RRSets: []RR{{
 		RRSets: []RR{{
@@ -73,8 +78,8 @@ func (c *Client) CreateCNAMERecord(value, hostname string) error {
 }
 }
 
 
 // CreateARecord creates a new A record for the nameserver
 // CreateARecord creates a new A record for the nameserver
-func (c *Client) CreateARecord(value, hostname string) error {
-	hostnameC := canonicalize(hostname)
+func (c Client) CreateARecord(record dns.Record) error {
+	hostnameC := canonicalize(fmt.Sprintf("%s.%s", record.Name, record.RootDomain))
 
 
 	return c.sendRequest("PATCH", &RecordData{
 	return c.sendRequest("PATCH", &RecordData{
 		RRSets: []RR{{
 		RRSets: []RR{{
@@ -83,7 +88,7 @@ func (c *Client) CreateARecord(value, hostname string) error {
 			ChangeType: "REPLACE",
 			ChangeType: "REPLACE",
 			TTL:        300,
 			TTL:        300,
 			Records: []Record{{
 			Records: []Record{{
-				Content:  value,
+				Content:  record.Value,
 				Disabled: false,
 				Disabled: false,
 				Name:     hostnameC,
 				Name:     hostnameC,
 				Type:     "A",
 				Type:     "A",
@@ -136,7 +141,7 @@ func (c *Client) sendRequest(method string, data *RecordData) error {
 	defer res.Body.Close()
 	defer res.Body.Close()
 
 
 	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
 	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
-		resBytes, err := ioutil.ReadAll(res.Body)
+		resBytes, err := io.ReadAll(res.Body)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
 			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
 		}
 		}

+ 8 - 3
internal/kubernetes/domain/domain.go

@@ -101,11 +101,16 @@ func (c *CreateDNSRecordConfig) NewDNSRecordForEndpoint() *models.DNSRecord {
 // CreateDomain creates a new record for the vanity domain
 // CreateDomain creates a new record for the vanity domain
 func (e *DNSRecord) CreateDomain(dnsClient *dns.Client) error {
 func (e *DNSRecord) CreateDomain(dnsClient *dns.Client) error {
 	isIPv4 := net.ParseIP(e.Endpoint) != nil
 	isIPv4 := net.ParseIP(e.Endpoint) != nil
-	domain := fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)
 
 
+	dnsType := dns.RecordType_CNAME
 	if isIPv4 {
 	if isIPv4 {
-		return dnsClient.CreateARecord(e.Endpoint, domain)
+		dnsType = dns.RecordType_A
 	}
 	}
 
 
-	return dnsClient.CreateCNAMERecord(e.Endpoint, domain)
+	return dnsClient.CreateRecord(dns.Record{
+		Type:       dnsType,
+		Value:      e.Endpoint,
+		Name:       e.SubdomainPrefix,
+		RootDomain: e.RootDomain,
+	})
 }
 }

+ 13 - 0
zarf/helm/.serverenv

@@ -15,6 +15,19 @@ ENABLE_CAPI_PROVISIONER=true
 NATS_URL=nats:4222
 NATS_URL=nats:4222
 CLUSTER_CONTROL_PLANE_ADDRESS=http://ccp-web:7833
 CLUSTER_CONTROL_PLANE_ADDRESS=http://ccp-web:7833
 
 
+# Domain we use to generate custom subdomains from
+APP_ROOT_DOMAIN=withporter.run
+
+# Controls which external dns provider to use
+# Specific clients may require extra tokens
+# Options: cloudflare, powerdns, or empty string
+DNS_PROVIDER=powerdns
+
+# Cloudflare DNS provider
+# if set, can be used as an alternative to PowerDNS
+# Create a token here: https://dash.cloudflare.com/profile/api-tokens
+CLOUDFLARE_API_TOKEN=<cloudflare-edit-dns-api-token>
+
 # Github Login OAuth. More information found at https://docs.porter.run/enterprise/self-hosted/integrations/github#setting-up-github-repository-integrations
 # Github Login OAuth. More information found at https://docs.porter.run/enterprise/self-hosted/integrations/github#setting-up-github-repository-integrations
 
 
 GITHUB_LOGIN_ENABLED=false
 GITHUB_LOGIN_ENABLED=false