Pārlūkot izejas kodu

subdomain creation endpoint

Alexander Belanger 5 gadi atpakaļ
vecāks
revīzija
4aa4ccc7d5

+ 1 - 0
internal/config/config.go

@@ -29,6 +29,7 @@ type ServerConf struct {
 	TimeoutIdle          time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
 	IsLocal              bool          `env:"IS_LOCAL,default=false"`
 	IsTesting            bool          `env:"IS_TESTING,default=false"`
+	AppRootDomain        string        `env:"APP_ROOT_DOMAIN"`
 
 	DefaultHelmRepoURL string `env:"HELM_REPO_URL,default=https://porter-dev.github.io/chart-repo/"`
 

+ 8 - 0
internal/forms/domain.go

@@ -0,0 +1,8 @@
+package forms
+
+// CreateDomainForm represents the accepted values for creating a DNS record
+type CreateDomainForm struct {
+	*K8sForm
+
+	ReleaseName string `json:"release_name" form:"required"`
+}

+ 65 - 16
internal/kubernetes/domain/domain.go

@@ -3,8 +3,11 @@ package domain
 import (
 	"context"
 	"fmt"
+	"math/rand"
 	"net"
+	"strings"
 
+	"github.com/porter-dev/porter/internal/models"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/api/extensions/v1beta1"
 	"k8s.io/client-go/kubernetes"
@@ -13,15 +16,61 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 )
 
-type PorterEndpoint struct {
-	SubdomainPrefix string
-	RootDomain      string
+// GetNGINXIngressServiceIP retrieves the external address of the nginx-ingress service
+func GetNGINXIngressServiceIP(clientset kubernetes.Interface) (string, bool, error) {
+	svcList, err := clientset.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{
+		LabelSelector: "app.kubernetes.io/component=controller,app.kubernetes.io/managed-by=Helm",
+	})
 
-	Endpoint string
-	Hostname string
+	if err != nil {
+		return "", false, err
+	}
+
+	var nginxSvc *v1.Service
+	exists := false
+
+	for _, svc := range svcList.Items {
+		// check that helm chart annotation is correct exists
+		if chartAnn, found := svc.Annotations["helm.sh/chart"]; found {
+			if strings.Contains(chartAnn, "ingress-nginx") && svc.Spec.Type == v1.ServiceTypeLoadBalancer {
+				nginxSvc = &svc
+				exists = true
+			}
+		}
+	}
+
+	return nginxSvc.Spec.LoadBalancerIP, exists, nil
+}
+
+// DNSRecord wraps the gorm DNSRecord model
+type DNSRecord models.DNSRecord
+
+type CreateDNSRecordConfig struct {
+	ReleaseName string
+	RootDomain  string
+	Endpoint    string
+}
+
+// NewDNSRecordForEndpoint generates a random subdomain and returns a DNSRecord
+// model
+func (c *CreateDNSRecordConfig) NewDNSRecordForEndpoint() *models.DNSRecord {
+	const allowed = "123456789abcdefghijklmnopqrstuvwxyz"
+	suffix := make([]byte, 8)
+	for i := range suffix {
+		suffix[i] = allowed[rand.Intn(len(allowed))]
+	}
+
+	subdomain := fmt.Sprintf("%s-%s", c.ReleaseName, string(suffix))
+
+	return &models.DNSRecord{
+		SubdomainPrefix: subdomain,
+		RootDomain:      c.RootDomain,
+		Endpoint:        c.Endpoint,
+		Hostname:        fmt.Sprintf("%s.%s", subdomain, c.RootDomain),
+	}
 }
 
-func (e *PorterEndpoint) CreateDomain(clientset kubernetes.Interface) error {
+func (e *DNSRecord) CreateDomain(clientset kubernetes.Interface) error {
 	// determine if IP address or domain
 	err := e.createIngress(clientset)
 
@@ -32,7 +81,7 @@ func (e *PorterEndpoint) CreateDomain(clientset kubernetes.Interface) error {
 	return e.createServiceWithEndpoint(clientset)
 }
 
-func (e *PorterEndpoint) createIngress(clientset kubernetes.Interface) error {
+func (e *DNSRecord) createIngress(clientset kubernetes.Interface) error {
 	_, err := clientset.ExtensionsV1beta1().Ingresses("default").Create(
 		context.TODO(),
 		&v1beta1.Ingress{
@@ -48,12 +97,12 @@ func (e *PorterEndpoint) createIngress(clientset kubernetes.Interface) error {
 			},
 			Spec: v1beta1.IngressSpec{
 				Rules: []v1beta1.IngressRule{
-					v1beta1.IngressRule{
+					{
 						Host: fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain),
 						IngressRuleValue: v1beta1.IngressRuleValue{
 							HTTP: &v1beta1.HTTPIngressRuleValue{
 								Paths: []v1beta1.HTTPIngressPath{
-									v1beta1.HTTPIngressPath{
+									{
 										Backend: v1beta1.IngressBackend{
 											ServiceName: e.SubdomainPrefix,
 											ServicePort: intstr.IntOrString{
@@ -75,13 +124,13 @@ func (e *PorterEndpoint) createIngress(clientset kubernetes.Interface) error {
 	return err
 }
 
-func (e *PorterEndpoint) createServiceWithEndpoint(clientset kubernetes.Interface) error {
+func (e *DNSRecord) createServiceWithEndpoint(clientset kubernetes.Interface) error {
 	// determine if endpoint needs to be created or external name is ok
 	isIPv4 := net.ParseIP(e.Endpoint) != nil
 
 	svcSpec := v1.ServiceSpec{
 		Ports: []v1.ServicePort{
-			v1.ServicePort{
+			{
 				Port: 80,
 				TargetPort: intstr.IntOrString{
 					Type:   intstr.Int,
@@ -89,7 +138,7 @@ func (e *PorterEndpoint) createServiceWithEndpoint(clientset kubernetes.Interfac
 				},
 				Name: "http",
 			},
-			v1.ServicePort{
+			{
 				Port: 443,
 				TargetPort: intstr.IntOrString{
 					Type:   intstr.Int,
@@ -134,18 +183,18 @@ func (e *PorterEndpoint) createServiceWithEndpoint(clientset kubernetes.Interfac
 					Namespace: "default",
 				},
 				Subsets: []v1.EndpointSubset{
-					v1.EndpointSubset{
+					{
 						Addresses: []v1.EndpointAddress{
-							v1.EndpointAddress{
+							{
 								IP: e.Endpoint,
 							},
 						},
 						Ports: []v1.EndpointPort{
-							v1.EndpointPort{
+							{
 								Name: "http",
 								Port: 80,
 							},
-							v1.EndpointPort{
+							{
 								Name: "https",
 								Port: 443,
 							},

+ 1 - 1
internal/models/dns_record.go

@@ -10,7 +10,7 @@ import (
 type DNSRecord struct {
 	gorm.Model
 
-	SubdomainPrefix string `json:"subdomain_prefix"`
+	SubdomainPrefix string `json:"subdomain_prefix" gorm:"unique"`
 	RootDomain      string `json:"root_domain"`
 
 	Endpoint string `json:"endpoint"`

+ 99 - 0
server/api/dns_record_handler.go

@@ -0,0 +1,99 @@
+package api
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/kubernetes/domain"
+)
+
+// HandleCreateProjectCluster creates a new cluster
+func (app *App) HandleCreateDNSRecord(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.CreateDomainForm{
+		K8sForm: &forms.K8sForm{
+			OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
+			},
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	endpoint, found, err := domain.GetNGINXIngressServiceIP(agent.Clientset)
+
+	if !found {
+		app.handleErrorInternal(fmt.Errorf("target cluster does not have nginx ingress"), w)
+		return
+	}
+
+	createDomain := domain.CreateDNSRecordConfig{
+		ReleaseName: form.ReleaseName,
+		RootDomain:  app.ServerConf.AppRootDomain,
+		Endpoint:    endpoint,
+	}
+
+	record := createDomain.NewDNSRecordForEndpoint()
+
+	record, err = app.Repo.DNSRecord.CreateDNSRecord(record)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	// launch provisioning destruction pod
+	inClusterAgent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	_record := domain.DNSRecord(*record)
+
+	err = _record.CreateDomain(inClusterAgent.Clientset)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+	}
+
+	w.WriteHeader(http.StatusCreated)
+
+	if err := json.NewEncoder(w).Encode(record.Externalize()); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+}

+ 15 - 0
server/router/router.go

@@ -1133,6 +1133,21 @@ func New(a *api.App) *chi.Mux {
 				mw.ReadAccess,
 			),
 		)
+
+		// /api/projects/{project_id}/subdomain routes
+		r.Method(
+			"POST",
+			"/projects/{project_id}/k8s/subdomain",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleCreateDNSRecord, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
 	})
 
 	staticFilePath := a.ServerConf.StaticFilePath