فهرست منبع

add nameserver apis

abhishek9686 3 هفته پیش
والد
کامیت
dcd7fe72dd
4فایلهای تغییر یافته به همراه342 افزوده شده و 0 حذف شده
  1. 242 0
      controllers/dns.go
  2. 89 0
      logic/dns.go
  3. 10 0
      models/dnsEntry.go
  4. 1 0
      models/events.go

+ 242 - 0
controllers/dns.go

@@ -1,19 +1,25 @@
 package controller
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
 	"strings"
+	"time"
 
+	"github.com/google/uuid"
 	"github.com/gorilla/mux"
 	"github.com/gravitl/netmaker/database"
+	"github.com/gravitl/netmaker/db"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/mq"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/gravitl/netmaker/servercfg"
+	"gorm.io/datatypes"
 )
 
 func dnsHandlers(r *mux.Router) {
@@ -34,6 +40,242 @@ func dnsHandlers(r *mux.Router) {
 		Methods(http.MethodPost)
 	r.HandleFunc("/api/dns/{network}/{domain}", logic.SecurityCheck(true, http.HandlerFunc(deleteDNS))).
 		Methods(http.MethodDelete)
+	r.HandleFunc("/api/v1/nameserver", logic.SecurityCheck(true, http.HandlerFunc(createNs))).Methods(http.MethodPost)
+	r.HandleFunc("/api/v1/nameserver", logic.SecurityCheck(true, http.HandlerFunc(listNs))).Methods(http.MethodGet)
+	r.HandleFunc("/api/v1/nameserver", logic.SecurityCheck(true, http.HandlerFunc(updateNs))).Methods(http.MethodPut)
+	r.HandleFunc("/api/v1/nameserver", logic.SecurityCheck(true, http.HandlerFunc(deleteEgress))).Methods(http.MethodDelete)
+}
+
+// @Summary     Create Nameserver
+// @Router      /api/v1/nameserver [post]
+// @Tags        DNS
+// @Accept      json
+// @Param       body body models.NameserverReq
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func createNs(w http.ResponseWriter, r *http.Request) {
+
+	var req models.NameserverReq
+	err := json.NewDecoder(r.Body).Decode(&req)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if err := logic.ValidateNameserverReq(req); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if req.Tags == nil {
+		req.Tags = []string{}
+	}
+	tagMap := make(datatypes.JSONMap)
+	for _, tagI := range req.Tags {
+		tagMap[tagI] = struct{}{}
+	}
+	ns := schema.Nameserver{
+		ID:          uuid.New().String(),
+		Name:        req.Name,
+		Network:     req.Network,
+		Description: req.Description,
+		MatchDomain: req.MatchDomain,
+		Servers:     req.Servers,
+		Tags:        tagMap,
+		Status:      true,
+		CreatedBy:   r.Header.Get("user"),
+		CreatedAt:   time.Now().UTC(),
+	}
+
+	err = ns.Create(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating nameserver "+err.Error()), logic.Internal),
+		)
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Create,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   ns.ID,
+			Name: ns.Name,
+			Type: models.NameserverSub,
+		},
+		NetworkID: models.NetworkID(ns.Network),
+		Origin:    models.Dashboard,
+	})
+
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, ns, "created nameserver")
+}
+
+// @Summary     List Nameservers
+// @Router      /api/v1/nameserver [get]
+// @Tags        Auth
+// @Accept      json
+// @Param       query network string
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func listNs(w http.ResponseWriter, r *http.Request) {
+
+	network := r.URL.Query().Get("network")
+	if network == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
+		return
+	}
+	ns := schema.Nameserver{Network: network}
+	list, err := ns.ListByNetwork(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error listing egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	logic.ReturnSuccessResponseWithJson(w, r, list, "fetched nameservers")
+}
+
+// @Summary     Update Nameserver
+// @Router      /api/v1/nameserver [put]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.NameserverReq
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func updateNs(w http.ResponseWriter, r *http.Request) {
+
+	var updateNs schema.Nameserver
+	err := json.NewDecoder(r.Body).Decode(&updateNs)
+	if err != nil {
+		logger.Log(0, "error decoding request body: ",
+			err.Error())
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+
+	if err := logic.ValidateUpdateNameserverReq(updateNs); err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	if updateNs.Tags == nil {
+		updateNs.Tags = make(datatypes.JSONMap)
+	}
+
+	ns := schema.Nameserver{ID: updateNs.ID}
+	err = ns.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
+		return
+	}
+	var updateStatus bool
+	if updateNs.Status != ns.Status {
+		updateStatus = true
+	}
+	event := &models.Event{
+		Action: models.Update,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   ns.ID,
+			Name: updateNs.Name,
+			Type: models.NameserverSub,
+		},
+		Diff: models.Diff{
+			Old: ns,
+			New: updateNs,
+		},
+		NetworkID: models.NetworkID(ns.Network),
+		Origin:    models.Dashboard,
+	}
+	ns.Servers = updateNs.Servers
+	ns.Tags = updateNs.Tags
+	ns.Description = updateNs.Description
+	ns.Name = updateNs.Name
+	ns.Status = updateNs.Status
+	ns.UpdatedAt = time.Now().UTC()
+
+	err = ns.Update(db.WithContext(context.TODO()))
+	if err != nil {
+		logic.ReturnErrorResponse(
+			w,
+			r,
+			logic.FormatError(errors.New("error creating egress resource"+err.Error()), "internal"),
+		)
+		return
+	}
+	if updateStatus {
+		ns.UpdateStatus(db.WithContext(context.TODO()))
+	}
+	logic.LogEvent(event)
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, ns, "updated nameserver")
+}
+
+// @Summary     Delete Nameserver Resource
+// @Router      /api/v1/nameserver [delete]
+// @Tags        Auth
+// @Accept      json
+// @Param       body body models.Egress
+// @Success     200 {object} models.SuccessResponse
+// @Failure     400 {object} models.ErrorResponse
+// @Failure     401 {object} models.ErrorResponse
+// @Failure     500 {object} models.ErrorResponse
+func deleteNs(w http.ResponseWriter, r *http.Request) {
+
+	id := r.URL.Query().Get("id")
+	if id == "" {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
+		return
+	}
+	ns := schema.Nameserver{ID: id}
+	err := ns.Get(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
+		return
+	}
+	err = ns.Delete(db.WithContext(r.Context()))
+	if err != nil {
+		logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
+		return
+	}
+	logic.LogEvent(&models.Event{
+		Action: models.Delete,
+		Source: models.Subject{
+			ID:   r.Header.Get("user"),
+			Name: r.Header.Get("user"),
+			Type: models.UserSub,
+		},
+		TriggeredBy: r.Header.Get("user"),
+		Target: models.Subject{
+			ID:   ns.ID,
+			Name: ns.Name,
+			Type: models.NameserverSub,
+		},
+		NetworkID: models.NetworkID(ns.Network),
+		Origin:    models.Dashboard,
+	})
+
+	go mq.PublishPeerUpdate(false)
+	logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted nameserver resource")
 }
 
 // @Summary     Gets node DNS entries associated with a network

+ 89 - 0
logic/dns.go

@@ -7,11 +7,13 @@ import (
 	"os"
 	"regexp"
 	"sort"
+	"strings"
 
 	validator "github.com/go-playground/validator/v10"
 	"github.com/gravitl/netmaker/database"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/models"
+	"github.com/gravitl/netmaker/schema"
 	"github.com/txn2/txeh"
 )
 
@@ -325,3 +327,90 @@ func CreateDNS(entry models.DNSEntry) (models.DNSEntry, error) {
 	err = database.Insert(k, string(data), database.DNS_TABLE_NAME)
 	return entry, err
 }
+
+func ValidateNameserverReq(ns models.NameserverReq) error {
+	if ns.Name == "" {
+		return errors.New("name is required")
+	}
+	if len(ns.Servers) == 0 {
+		return errors.New("atleast one nameserver should be specified")
+	}
+	if !IsValidMatchDomain(ns.MatchDomain) {
+		return errors.New("invalid match domain")
+	}
+	return nil
+}
+
+func ValidateUpdateNameserverReq(updateNs schema.Nameserver) error {
+	if updateNs.Name == "" {
+		return errors.New("name is required")
+	}
+	if len(updateNs.Servers) == 0 {
+		return errors.New("atleast one nameserver should be specified")
+	}
+	if !IsValidMatchDomain(updateNs.MatchDomain) {
+		return errors.New("invalid match domain")
+	}
+	return nil
+}
+
+// IsValidMatchDomain reports whether s is a valid "match domain".
+// Rules (simple/ASCII):
+//   - "~." is allowed (match all).
+//   - Optional leading "~" allowed (e.g., "~example.com").
+//   - Optional single trailing "." allowed (FQDN form).
+//   - No wildcards "*", no leading ".", no underscores.
+//   - Labels: letters/digits/hyphen (LDH), 1–63 chars, no leading/trailing hyphen.
+//   - Total length (without trailing dot) ≤ 253.
+func IsValidMatchDomain(s string) bool {
+	s = strings.TrimSpace(s)
+	if s == "" {
+		return false
+	}
+	if s == "~." { // special case: match-all
+		return true
+	}
+
+	// Strip optional leading "~"
+	if strings.HasPrefix(s, "~") {
+		s = s[1:]
+		if s == "" {
+			return false
+		}
+	}
+
+	// Allow exactly one trailing dot
+	if strings.HasSuffix(s, ".") {
+		s = s[:len(s)-1]
+		if s == "" {
+			return false
+		}
+	}
+
+	// Disallow leading dot, wildcards, underscores
+	if strings.HasPrefix(s, ".") || strings.Contains(s, "*") || strings.Contains(s, "_") {
+		return false
+	}
+
+	// Lowercase for ASCII checks
+	s = strings.ToLower(s)
+
+	// Length check
+	if len(s) > 253 {
+		return false
+	}
+
+	// Label regex: LDH, 1–63, no leading/trailing hyphen
+	reLabel := regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`)
+
+	parts := strings.Split(s, ".")
+	for _, lbl := range parts {
+		if len(lbl) == 0 || len(lbl) > 63 {
+			return false
+		}
+		if !reLabel.MatchString(lbl) {
+			return false
+		}
+	}
+	return true
+}

+ 10 - 0
models/dnsEntry.go

@@ -47,3 +47,13 @@ type DNSEntry struct {
 	Name     string `json:"name" validate:"required,name_unique,min=1,max=192,whitespace"`
 	Network  string `json:"network" validate:"network_exists"`
 }
+
+type NameserverReq struct {
+	Name        string   `json:"name"`
+	Network     string   `json:"network"`
+	Description string   ` json:"description"`
+	Servers     []string `json:"servers"`
+	MatchDomain string   `json:"match_domain"`
+	Tags        []string `json:"tags"`
+	Status      bool     `gorm:"status" json:"status"`
+}

+ 1 - 0
models/events.go

@@ -55,6 +55,7 @@ const (
 	DashboardSub       SubjectType = "DASHBOARD"
 	EnrollmentKeySub   SubjectType = "ENROLLMENT_KEY"
 	ClientAppSub       SubjectType = "CLIENT-APP"
+	NameserverSub      SubjectType = "NAMESERVER"
 )
 
 func (sub SubjectType) String() string {