From dcd7fe72dda8d424b52c8e56fedd8ff3db23fef5 Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Thu, 21 Aug 2025 00:27:59 +0530 Subject: [PATCH] add nameserver apis --- controllers/dns.go | 242 +++++++++++++++++++++++++++++++++++++++++++++ logic/dns.go | 89 +++++++++++++++++ models/dnsEntry.go | 10 ++ models/events.go | 1 + 4 files changed, 342 insertions(+) diff --git a/controllers/dns.go b/controllers/dns.go index 257a683a..fd17753e 100644 --- a/controllers/dns.go +++ b/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 diff --git a/logic/dns.go b/logic/dns.go index 23bfed15..c1689317 100644 --- a/logic/dns.go +++ b/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 +} diff --git a/models/dnsEntry.go b/models/dnsEntry.go index 596d9225..beae46a7 100644 --- a/models/dnsEntry.go +++ b/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"` +} diff --git a/models/events.go b/models/events.go index 4a6e1603..b8b1bb92 100644 --- a/models/events.go +++ b/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 {