diff --git a/controllers/controller.go b/controllers/controller.go index 75423b6e..5a1396d3 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -34,6 +34,7 @@ var HttpHandlers = []interface{}{ loggerHandlers, hostHandlers, enrollmentKeyHandlers, + tagHandlers, legacyHandlers, } diff --git a/controllers/tags.go b/controllers/tags.go new file mode 100644 index 00000000..5fb167b6 --- /dev/null +++ b/controllers/tags.go @@ -0,0 +1,80 @@ +package controller + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/gravitl/netmaker/logger" + "github.com/gravitl/netmaker/logic" + "github.com/gravitl/netmaker/models" +) + +func tagHandlers(r *mux.Router) { + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(getAllTags))). + Methods(http.MethodGet) + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(createTag))). + Methods(http.MethodPost) + r.HandleFunc("/api/v1/tags", logic.SecurityCheck(true, http.HandlerFunc(updateTag))). + Methods(http.MethodPut) + +} + +// @Summary Get all Tag entries +// @Router /api/v1/tags [get] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func getAllTags(w http.ResponseWriter, r *http.Request) { + tags, err := logic.ListTagsWithHosts() + if err != nil { + logger.Log(0, r.Header.Get("user"), "failed to get all DNS entries: ", err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) + return + } + logic.SortTagEntrys(tags[:]) + logic.ReturnSuccessResponseWithJson(w, r, tags, "fetched all tags") +} + +// @Summary Create Tag +// @Router /api/v1/tags [post] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func createTag(w http.ResponseWriter, r *http.Request) { + var tag models.Tag + err := json.NewDecoder(r.Body).Decode(&tag) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + err = logic.InsertTag(tag) + if err != nil { + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + logic.ReturnSuccessResponseWithJson(w, r, tag, "created tag successfully") +} + +// @Summary Update Tag +// @Router /api/v1/tags [put] +// @Tags TAG +// @Accept json +// @Success 200 {array} models.SuccessResponse +// @Failure 500 {object} models.ErrorResponse +func updateTag(w http.ResponseWriter, r *http.Request) { + var updateTag models.UpdateTagReq + err := json.NewDecoder(r.Body).Decode(&updateTag) + if err != nil { + logger.Log(0, "error decoding request body: ", + err.Error()) + logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest")) + return + } + go logic.UpdateTag(updateTag) + logic.ReturnSuccessResponse(w, r, "updating tags") +} diff --git a/database/database.go b/database/database.go index f8508b3f..2a950b6c 100644 --- a/database/database.go +++ b/database/database.go @@ -67,6 +67,8 @@ const ( PENDING_USERS_TABLE_NAME = "pending_users" // USER_INVITES - table for user invites USER_INVITES_TABLE_NAME = "user_invites" + // TAG_TABLE_NAME - table for tags + TAG_TABLE_NAME = "tags" // == ERROR CONSTS == // NO_RECORD - no singular result found NO_RECORD = "no result found" @@ -152,6 +154,7 @@ func createTables() { CreateTable(PENDING_USERS_TABLE_NAME) CreateTable(USER_PERMISSIONS_TABLE_NAME) CreateTable(USER_INVITES_TABLE_NAME) + CreateTable(TAG_TABLE_NAME) } func CreateTable(tableName string) error { diff --git a/logic/hosts.go b/logic/hosts.go index 0fa8887e..b8a90e8c 100644 --- a/logic/hosts.go +++ b/logic/hosts.go @@ -572,3 +572,35 @@ func SortApiHosts(unsortedHosts []models.ApiHost) { return unsortedHosts[i].ID < unsortedHosts[j].ID }) } + +func GetTagMapWithHosts() (tagHostMap map[models.TagID][]models.Host) { + tagHostMap = make(map[models.TagID][]models.Host) + hosts, _ := GetAllHosts() + for _, hostI := range hosts { + if hostI.Tags == nil { + continue + } + for hostTagID := range hostI.Tags { + if _, ok := tagHostMap[hostTagID]; ok { + tagHostMap[hostTagID] = append(tagHostMap[hostTagID], hostI) + } else { + tagHostMap[hostTagID] = []models.Host{hostI} + } + } + } + return +} + +func GetHostsWithTag(tagID models.TagID) map[string]models.Host { + hMap := make(map[string]models.Host) + hosts, _ := GetAllHosts() + for _, hostI := range hosts { + if hostI.Tags == nil { + continue + } + if _, ok := hostI.Tags[tagID]; ok { + hMap[hostI.ID.String()] = hostI + } + } + return hMap +} diff --git a/logic/tags.go b/logic/tags.go new file mode 100644 index 00000000..aad6d5cc --- /dev/null +++ b/logic/tags.go @@ -0,0 +1,104 @@ +package logic + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/gravitl/netmaker/database" + "github.com/gravitl/netmaker/models" +) + +func GetTag(tagID string) (models.Tag, error) { + data, err := database.FetchRecord(database.TAG_TABLE_NAME, tagID) + if err != nil && !database.IsEmptyRecord(err) { + return models.Tag{}, err + } + tag := models.Tag{} + err = json.Unmarshal([]byte(data), &tag) + if err != nil { + return tag, err + } + return tag, nil +} + +func InsertTag(tag models.Tag) error { + _, err := database.FetchRecord(database.TAG_TABLE_NAME, tag.ID.String()) + if err == nil { + return fmt.Errorf("tag `%s` exists already", tag.ID) + } + d, err := json.Marshal(tag) + if err != nil { + return err + } + return database.Insert(tag.ID.String(), string(d), database.TAG_TABLE_NAME) +} + +func DeleteTag(tagID string) error { + return database.DeleteRecord(database.TAG_TABLE_NAME, tagID) +} + +func ListTagsWithHosts() ([]models.TagListResp, error) { + tags, err := ListTags() + if err != nil { + return []models.TagListResp{}, err + } + tagsHostMap := GetTagMapWithHosts() + resp := []models.TagListResp{} + for _, tagI := range tags { + tagRespI := models.TagListResp{ + Tag: tagI, + UsedByCnt: len(tagsHostMap[tagI.ID]), + TaggedHosts: tagsHostMap[tagI.ID], + } + resp = append(resp, tagRespI) + } + return resp, nil +} + +func ListTags() ([]models.Tag, error) { + + data, err := database.FetchRecords(database.TAG_TABLE_NAME) + if err != nil && !database.IsEmptyRecord(err) { + return []models.Tag{}, err + } + tags := []models.Tag{} + for _, dataI := range data { + tag := models.Tag{} + err := json.Unmarshal([]byte(dataI), &tag) + if err != nil { + continue + } + tags = append(tags, tag) + } + return tags, nil +} + +func UpdateTag(req models.UpdateTagReq) { + tagHostsMap := GetHostsWithTag(req.ID) + for _, hostI := range req.TaggedHosts { + hostI := hostI + + if _, ok := tagHostsMap[hostI.ID.String()]; !ok { + if hostI.Tags == nil { + hostI.Tags = make(map[models.TagID]struct{}) + } + hostI.Tags[req.ID] = struct{}{} + UpsertHost(&hostI) + } else { + delete(tagHostsMap, hostI.ID.String()) + } + } + for _, deletedTaggedHost := range tagHostsMap { + deletedTaggedHost := deletedTaggedHost + delete(deletedTaggedHost.Tags, req.ID) + UpsertHost(&deletedTaggedHost) + } +} + +// SortTagEntrys - Sorts slice of Tag entries by their id +func SortTagEntrys(tags []models.TagListResp) { + sort.Slice(tags, func(i, j int) bool { + return tags[i].ID < tags[j].ID + }) +} diff --git a/models/host.go b/models/host.go index 2781dee0..8b336dfa 100644 --- a/models/host.go +++ b/models/host.go @@ -41,37 +41,38 @@ const ( // Host - represents a host on the network type Host struct { - ID uuid.UUID `json:"id" yaml:"id"` - Verbosity int `json:"verbosity" yaml:"verbosity"` - FirewallInUse string `json:"firewallinuse" yaml:"firewallinuse"` - Version string `json:"version" yaml:"version"` - IPForwarding bool `json:"ipforwarding" yaml:"ipforwarding"` - DaemonInstalled bool `json:"daemoninstalled" yaml:"daemoninstalled"` - AutoUpdate bool `json:"autoupdate" yaml:"autoupdate"` - HostPass string `json:"hostpass" yaml:"hostpass"` - Name string `json:"name" yaml:"name"` - OS string `json:"os" yaml:"os"` - Interface string `json:"interface" yaml:"interface"` - Debug bool `json:"debug" yaml:"debug"` - ListenPort int `json:"listenport" yaml:"listenport"` - WgPublicListenPort int `json:"wg_public_listen_port" yaml:"wg_public_listen_port"` - MTU int `json:"mtu" yaml:"mtu"` - PublicKey wgtypes.Key `json:"publickey" yaml:"publickey"` - MacAddress net.HardwareAddr `json:"macaddress" yaml:"macaddress"` - TrafficKeyPublic []byte `json:"traffickeypublic" yaml:"traffickeypublic"` - Nodes []string `json:"nodes" yaml:"nodes"` - Interfaces []Iface `json:"interfaces" yaml:"interfaces"` - DefaultInterface string `json:"defaultinterface" yaml:"defaultinterface"` - EndpointIP net.IP `json:"endpointip" yaml:"endpointip"` - EndpointIPv6 net.IP `json:"endpointipv6" yaml:"endpointipv6"` - IsDocker bool `json:"isdocker" yaml:"isdocker"` - IsK8S bool `json:"isk8s" yaml:"isk8s"` - IsStaticPort bool `json:"isstaticport" yaml:"isstaticport"` - IsStatic bool `json:"isstatic" yaml:"isstatic"` - IsDefault bool `json:"isdefault" yaml:"isdefault"` - NatType string `json:"nat_type,omitempty" yaml:"nat_type,omitempty"` - TurnEndpoint *netip.AddrPort `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"` - PersistentKeepalive time.Duration `json:"persistentkeepalive" yaml:"persistentkeepalive"` + ID uuid.UUID `json:"id" yaml:"id"` + Verbosity int `json:"verbosity" yaml:"verbosity"` + FirewallInUse string `json:"firewallinuse" yaml:"firewallinuse"` + Version string `json:"version" yaml:"version"` + IPForwarding bool `json:"ipforwarding" yaml:"ipforwarding"` + DaemonInstalled bool `json:"daemoninstalled" yaml:"daemoninstalled"` + AutoUpdate bool `json:"autoupdate" yaml:"autoupdate"` + HostPass string `json:"hostpass" yaml:"hostpass"` + Name string `json:"name" yaml:"name"` + OS string `json:"os" yaml:"os"` + Interface string `json:"interface" yaml:"interface"` + Debug bool `json:"debug" yaml:"debug"` + ListenPort int `json:"listenport" yaml:"listenport"` + WgPublicListenPort int `json:"wg_public_listen_port" yaml:"wg_public_listen_port"` + MTU int `json:"mtu" yaml:"mtu"` + PublicKey wgtypes.Key `json:"publickey" yaml:"publickey"` + MacAddress net.HardwareAddr `json:"macaddress" yaml:"macaddress"` + TrafficKeyPublic []byte `json:"traffickeypublic" yaml:"traffickeypublic"` + Nodes []string `json:"nodes" yaml:"nodes"` + Interfaces []Iface `json:"interfaces" yaml:"interfaces"` + DefaultInterface string `json:"defaultinterface" yaml:"defaultinterface"` + EndpointIP net.IP `json:"endpointip" yaml:"endpointip"` + EndpointIPv6 net.IP `json:"endpointipv6" yaml:"endpointipv6"` + IsDocker bool `json:"isdocker" yaml:"isdocker"` + IsK8S bool `json:"isk8s" yaml:"isk8s"` + IsStaticPort bool `json:"isstaticport" yaml:"isstaticport"` + IsStatic bool `json:"isstatic" yaml:"isstatic"` + IsDefault bool `json:"isdefault" yaml:"isdefault"` + NatType string `json:"nat_type,omitempty" yaml:"nat_type,omitempty"` + TurnEndpoint *netip.AddrPort `json:"turn_endpoint,omitempty" yaml:"turn_endpoint,omitempty"` + PersistentKeepalive time.Duration `json:"persistentkeepalive" yaml:"persistentkeepalive"` + Tags map[TagID]struct{} `json:"tags" yaml:"tags"` } // FormatBool converts a boolean to a [yes|no] string diff --git a/models/tags.go b/models/tags.go new file mode 100644 index 00000000..ebd396c6 --- /dev/null +++ b/models/tags.go @@ -0,0 +1,26 @@ +package models + +import "time" + +type TagID string + +func (id TagID) String() string { + return string(id) +} + +type Tag struct { + ID TagID `json:"id"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +type TagListResp struct { + Tag + UsedByCnt int `json:"used_by_count"` + TaggedHosts []Host `json:"tagged_hosts"` +} + +type UpdateTagReq struct { + Tag + TaggedHosts []Host `json:"tagged_hosts"` +}