add nameserver apis

This commit is contained in:
abhishek9686 2025-08-21 00:27:59 +05:30
parent fc39247042
commit dcd7fe72dd
4 changed files with 342 additions and 0 deletions

View file

@ -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

View file

@ -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), 163 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, 163, 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
}

View file

@ -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"`
}

View file

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