Răsfoiți Sursa

create integration for bind api

Alexander Belanger 4 ani în urmă
părinte
comite
bc05d0f8e6

+ 1 - 1
api/server/handlers/release/create_subdomain.go

@@ -67,7 +67,7 @@ func (c *CreateSubdomainHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	_record := domain.DNSRecord(*record)
 
-	err = _record.CreateDomain(c.Config().IngressAgent.Clientset)
+	err = _record.CreateDomain(c.Config().BindClient)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 4 - 4
api/server/shared/config/config.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
+	"github.com/porter-dev/porter/internal/integrations/bind"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -76,15 +77,14 @@ type Config struct {
 	// jobs
 	ProvisionerAgent *kubernetes.Agent
 
-	// IngressAgent is the kubernetes client responsible for creating new ingress
-	// resources
-	IngressAgent *kubernetes.Agent
-
 	// DB is the gorm DB instance
 	DB *gorm.DB
 
 	// AnalyticsClient if Segment analytics reporting is enabled on the API instance
 	AnalyticsClient analytics.AnalyticsSegmentClient
+
+	// BindClient is a client for Bind DNS, if the Porter instance supports vanity URLs
+	BindClient *bind.Client
 }
 
 type ConfigLoader interface {

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

@@ -55,6 +55,10 @@ type ServerConf struct {
 	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
 	SegmentClientKey           string `env:"SEGMENT_CLIENT_KEY"`
 
+	// Bind client API key and the host of the Bind DNS API server
+	BindAPIServerURL string `env:"BIND_API_SERVER_URL"`
+	BindAPIKey       string `env:"BIND_API_KEY"`
+
 	// Email for an admin user. On a self-hosted instance of Porter, the
 	// admin user is the only user that can log in and register. After the admin
 	// user has logged in, registration is turned off.

+ 4 - 25
api/server/shared/config/loader/loader.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
+	"github.com/porter-dev/porter/internal/integrations/bind"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/local"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -190,16 +191,12 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.Metadata.Provisioning = true
 	}
 
-	ingressAgent, err := getIngressAgent(sc)
+	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
 
-	if err != nil {
-		return nil, err
+	if sc.BindAPIKey != "" && sc.BindAPIServerURL != "" {
+		res.BindClient = bind.NewClient(sc.BindAPIServerURL, sc.BindAPIKey)
 	}
 
-	res.IngressAgent = ingressAgent
-
-	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
-
 	return res, nil
 }
 
@@ -220,21 +217,3 @@ func getProvisionerAgent(sc *env.ServerConf) (*kubernetes.Agent, error) {
 
 	return agent, nil
 }
-
-func getIngressAgent(sc *env.ServerConf) (*kubernetes.Agent, error) {
-	if sc.IngressCluster == "kubeconfig" && sc.SelfKubeconfig != "" {
-		agent, err := local.GetSelfAgentFromFileConfig(sc.SelfKubeconfig)
-
-		if err != nil {
-			return nil, fmt.Errorf("could not get in-cluster agent: %v", err)
-		}
-
-		return agent, nil
-	} else if sc.ProvisionerCluster == "kubeconfig" {
-		return nil, fmt.Errorf(`"kubeconfig" cluster option requires path to kubeconfig`)
-	}
-
-	agent, _ := kubernetes.GetAgentInClusterConfig()
-
-	return agent, nil
-}

+ 96 - 0
internal/integrations/bind/bind.go

@@ -0,0 +1,96 @@
+package bind
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// Client contains an API client for a Bind DNS server wrapped
+// with a lightweight API
+type Client struct {
+	apiKey    string
+	serverURL string
+
+	httpClient *http.Client
+}
+
+// NewClient creates a new bind API client
+func NewClient(serverURL, apiKey string) *Client {
+	httpClient := &http.Client{
+		Timeout: time.Minute,
+	}
+
+	return &Client{apiKey, serverURL, httpClient}
+}
+
+// CNAMEData represents the data required to create or delete a CNAME record
+// for the nameserver
+type CNAMEData struct {
+	Host     string `json:"host"`
+	Hostname string `json:"hostname"`
+}
+
+// CreateCNAMERecord creates a new CNAME record for the nameserver
+func (c *Client) CreateCNAMERecord(data *CNAMEData) error {
+	return c.sendRequest("POST", data)
+}
+
+// DeleteCNAMERecord deletes a new CNAME record for the nameserver
+func (c *Client) DeleteCNAMERecord(data *CNAMEData) error {
+	return c.sendRequest("DELETE", data)
+}
+
+func (c *Client) sendRequest(method string, data *CNAMEData) error {
+	reqURL, err := url.Parse(c.serverURL)
+
+	if err != nil {
+		return nil
+	}
+
+	reqURL.Path = "/dns"
+
+	strData, err := json.Marshal(data)
+
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest(
+		method,
+		reqURL.String(),
+		strings.NewReader(string(strData)),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+	req.Header.Set("X-Api-Key", c.apiKey)
+
+	res, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return err
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode != http.StatusOK {
+		resBytes, err := ioutil.ReadAll(res.Body)
+
+		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: %s\n", res.StatusCode, string(resBytes))
+	}
+
+	return nil
+}

+ 10 - 140
internal/kubernetes/domain/domain.go

@@ -6,14 +6,13 @@ import (
 	"net"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/integrations/bind"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/api/extensions/v1beta1"
 	"k8s.io/client-go/kubernetes"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/intstr"
 )
 
 // GetNGINXIngressServiceIP retrieves the external address of the nginx-ingress service
@@ -80,147 +79,18 @@ func (c *CreateDNSRecordConfig) NewDNSRecordForEndpoint() *models.DNSRecord {
 	}
 }
 
-func (e *DNSRecord) CreateDomain(clientset kubernetes.Interface) error {
-	// determine if IP address or domain
-	err := e.createIngress(clientset)
-
-	if err != nil {
-		return err
-	}
-
-	return e.createServiceWithEndpoint(clientset)
-}
-
-func (e *DNSRecord) createIngress(clientset kubernetes.Interface) error {
-	_, err := clientset.ExtensionsV1beta1().Ingresses("default").Create(
-		context.TODO(),
-		&v1beta1.Ingress{
-			ObjectMeta: metav1.ObjectMeta{
-				Annotations: map[string]string{
-					"kubernetes.io/ingress.class":                  "nginx",
-					"nginx.ingress.kubernetes.io/ssl-redirect":     "true",
-					"nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
-					"nginx.ingress.kubernetes.io/upstream-vhost":   e.Hostname,
-				},
-				Name:      e.SubdomainPrefix,
-				Namespace: "default",
-			},
-			Spec: v1beta1.IngressSpec{
-				TLS: []v1beta1.IngressTLS{
-					{
-						Hosts:      []string{fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)},
-						SecretName: "wildcard-cert-tls",
-					},
-				},
-				Rules: []v1beta1.IngressRule{
-					{
-						Host: fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain),
-						IngressRuleValue: v1beta1.IngressRuleValue{
-							HTTP: &v1beta1.HTTPIngressRuleValue{
-								Paths: []v1beta1.HTTPIngressPath{
-									{
-										Backend: v1beta1.IngressBackend{
-											ServiceName: e.SubdomainPrefix,
-											ServicePort: intstr.IntOrString{
-												Type:   intstr.Int,
-												IntVal: 443,
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		metav1.CreateOptions{},
-	)
-
-	return err
-}
-
-func (e *DNSRecord) createServiceWithEndpoint(clientset kubernetes.Interface) error {
-	// determine if endpoint needs to be created or external name is ok
+// CreateDomain creates a new record for the vanity domain
+func (e *DNSRecord) CreateDomain(bindClient *bind.Client) error {
 	isIPv4 := net.ParseIP(e.Endpoint) != nil
+	domain := fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)
 
-	svcSpec := v1.ServiceSpec{
-		Ports: []v1.ServicePort{
-			{
-				Port: 80,
-				TargetPort: intstr.IntOrString{
-					Type:   intstr.Int,
-					IntVal: 80,
-				},
-				Name: "http",
-			},
-			{
-				Port: 443,
-				TargetPort: intstr.IntOrString{
-					Type:   intstr.Int,
-					IntVal: 443,
-				},
-				Name: "https",
-			},
-		},
-	}
-
-	// case service spec on ipv4
 	if isIPv4 {
-		svcSpec.ClusterIP = "None"
-	} else {
-		svcSpec.Type = "ExternalName"
-		svcSpec.ExternalName = e.Endpoint
-	}
-
-	// create service
-	_, err := clientset.CoreV1().Services("default").Create(
-		context.TODO(),
-		&v1.Service{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      e.SubdomainPrefix,
-				Namespace: "default",
-			},
-			Spec: svcSpec,
-		},
-		metav1.CreateOptions{},
-	)
-
-	if err != nil {
-		return err
+		// TODO: create A record
+		panic("unsupported")
 	}
 
-	if isIPv4 {
-		_, err = clientset.CoreV1().Endpoints("default").Create(
-			context.TODO(),
-			&v1.Endpoints{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      e.SubdomainPrefix,
-					Namespace: "default",
-				},
-				Subsets: []v1.EndpointSubset{
-					{
-						Addresses: []v1.EndpointAddress{
-							{
-								IP: e.Endpoint,
-							},
-						},
-						Ports: []v1.EndpointPort{
-							{
-								Name: "http",
-								Port: 80,
-							},
-							{
-								Name: "https",
-								Port: 443,
-							},
-						},
-					},
-				},
-			},
-			metav1.CreateOptions{},
-		)
-	}
-
-	return err
+	return bindClient.CreateCNAMERecord(&bind.CNAMEData{
+		Host:     e.Endpoint,
+		Hostname: domain,
+	})
 }