From 64302cfc25c35a8f5e4389142e13bd2df03244a3 Mon Sep 17 00:00:00 2001 From: Dylan Verstraete Date: Thu, 11 Mar 2021 14:05:28 +0100 Subject: [PATCH] feat: add field that dedicates a node to a certain user (#293) * feat: add field that dedicates a node to a certain user * further improve node dedication * update node create * fix CI * wip: dedicated nodes * move the dedicated endpoint * Only check dedication on zos workloads types Co-authored-by: Muhamad Azamy --- models/generated/directory/directory.go | 2 + models/generated/workloads/reservation.go | 31 +++++++++++++++ pkg/directory/node_handlers.go | 48 +++++++++++++++++++++++ pkg/directory/setup.go | 2 + pkg/directory/types/node.go | 7 ++++ pkg/workloads/reservation.go | 23 ++++++++++- 6 files changed, 111 insertions(+), 2 deletions(-) diff --git a/models/generated/directory/directory.go b/models/generated/directory/directory.go index 1bb4fb4d..38d011b2 100644 --- a/models/generated/directory/directory.go +++ b/models/generated/directory/directory.go @@ -114,6 +114,8 @@ type Node struct { WgPorts []int64 `bson:"wg_ports" json:"wg_ports"` Deleted bool `bson:"deleted" json:"deleted"` Reserved bool `bson:"reserved" json:"reserved"` + // optional flag to indicate that a node can only accept workloads from a certain user + Dedicated schema.ID `bson:"dedicated" json:"dedicated"` } func NewNode() (Node, error) { diff --git a/models/generated/workloads/reservation.go b/models/generated/workloads/reservation.go index d6ab5f0c..441f29b9 100644 --- a/models/generated/workloads/reservation.go +++ b/models/generated/workloads/reservation.go @@ -146,6 +146,17 @@ const ( WorkloadTypePublicIP ) +// Any returns true if the type is one of the given types +func (t WorkloadTypeEnum) Any(types ...WorkloadTypeEnum) bool { + for _, typ := range types { + if typ == t { + return true + } + } + + return false +} + // WorkloadTypes is a map of all the supported workload type var WorkloadTypes = map[WorkloadTypeEnum]string{ WorkloadTypeZDB: "zdb", @@ -169,3 +180,23 @@ func (e WorkloadTypeEnum) String() string { } return s } + +// GatewayTypes is a list of all types processed by a gateway +var GatewayTypes = []WorkloadTypeEnum{ + WorkloadTypeProxy, + WorkloadTypeReverseProxy, + WorkloadTypeSubDomain, + WorkloadTypeDomainDelegate, + WorkloadTypeGateway4To6, +} + +// ZeroOSTypes is a list of all types supported by zero-os node +var ZeroOSTypes = []WorkloadTypeEnum{ + WorkloadTypeZDB, + WorkloadTypeContainer, + WorkloadTypeVolume, + WorkloadTypeNetwork, + WorkloadTypeKubernetes, + WorkloadTypeNetworkResource, + WorkloadTypePublicIP, +} diff --git a/pkg/directory/node_handlers.go b/pkg/directory/node_handlers.go index 9e705d5e..04f0c2c7 100644 --- a/pkg/directory/node_handlers.go +++ b/pkg/directory/node_handlers.go @@ -17,6 +17,7 @@ import ( "github.com/threefoldtech/tfexplorer/mw" "github.com/threefoldtech/tfexplorer/pkg/directory/types" directory "github.com/threefoldtech/tfexplorer/pkg/directory/types" + phonebook "github.com/threefoldtech/tfexplorer/pkg/phonebook/types" "github.com/threefoldtech/tfexplorer/schema" "github.com/threefoldtech/zos/pkg/capacity" "github.com/threefoldtech/zos/pkg/capacity/dmi" @@ -56,6 +57,7 @@ func (s *NodeAPI) registerNode(r *http.Request) (interface{}, mw.Response) { n.PublicConfig = nil // and it not immediately deleted n.Deleted = false + if _, err := s.Add(r.Context(), db, n); err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return nil, mw.NotFound(fmt.Errorf("farm with id:%d does not exists", n.FarmId)) @@ -158,6 +160,52 @@ func (s *NodeAPI) registerIfaces(r *http.Request) (interface{}, mw.Response) { return nil, mw.Created() } +func (s *NodeAPI) setNodeDedicated(r *http.Request) (interface{}, mw.Response) { + data := struct { + UserID int64 `json:"user_id"` + }{} + + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return nil, mw.BadRequest(err) + } + + db := mw.Database(r) + nodeID := mux.Vars(r)["node_id"] + + node, err := s.Get(r.Context(), db, nodeID, false) + if err != nil { + return nil, mw.NotFound(err) + } + + // ensure it is the farmer that does the call + authorized, merr := isFarmerAuthorized(r, node, db) + if err != nil { + return nil, merr + } + + if !authorized { + return nil, mw.Forbidden(fmt.Errorf("only the farmer can configured the public interface of its nodes")) + } + + // if a userID is provided we check if the user exists + // otherwise it will be an unset operation + if data.UserID > 0 { + var filter phonebook.UserFilter + filter = filter.WithID(schema.ID(data.UserID)) + _, err = filter.Get(r.Context(), db) + if err != nil { + return nil, mw.NotFound(errors.Wrap(err, "user not found")) + } + } + + err = directory.NodeUpdateDedicated(r.Context(), db, nodeID, data.UserID) + if err != nil { + return nil, mw.Error(err) + } + + return nil, mw.Ok() +} + func (s *NodeAPI) configurePublic(r *http.Request) (interface{}, mw.Response) { var iface generated.PublicIface diff --git a/pkg/directory/setup.go b/pkg/directory/setup.go index 2039e352..b903e01f 100644 --- a/pkg/directory/setup.go +++ b/pkg/directory/setup.go @@ -41,6 +41,7 @@ func Setup(parent *mux.Router, db *mongo.Database) error { farmsAuthenticated.HandleFunc("/ip", mw.AsHandlerFunc(farmAPI.deleteFarmIps)).Methods("DELETE").Name("farm-delete-ip-v1") farmsAuthenticated.HandleFunc("", mw.AsHandlerFunc(farmAPI.updateFarm)).Methods("PUT").Name("farm-update-v1") farmsAuthenticated.HandleFunc("/{node_id}", mw.AsHandlerFunc(nodeAPI.Requires("node_id", farmAPI.deleteNodeFromFarm))).Methods("DELETE").Name("farm-node-delete-v1") + farmsAuthenticated.HandleFunc("/deals", mw.AsHandlerFunc(farmAPI.createOrUpdateFarmCustomPrice)).Methods("POST", "PUT").Name("farm-update-prices-v1") farmsAuthenticated.HandleFunc("/deals/{threebot_id}", mw.AsHandlerFunc(farmAPI.deleteFarmCustomPrice)).Methods("DELETE").Name("farm-delete-prices-v1") @@ -60,6 +61,7 @@ func Setup(parent *mux.Router, db *mongo.Database) error { nodesAuthenticated.HandleFunc("/{node_id}/interfaces", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.registerIfaces))).Methods("POST").Name("node-interfaces-v1") nodesAuthenticated.HandleFunc("/{node_id}/ports", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.registerPorts))).Methods("POST").Name("node-set-ports-v1") userAuthenticated.HandleFunc("/{node_id}/configure_public", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.configurePublic))).Methods("POST").Name("node-configure-public-v1") + userAuthenticated.HandleFunc("/{node_id}/dedicated", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.setNodeDedicated))).Methods("PUT").Name("node-set-node-dedicated") userAuthenticated.HandleFunc("/{node_id}/configure_free", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.configureFreeToUse))).Methods("POST").Name("node-configure-free-v1") nodesAuthenticated.HandleFunc("/{node_id}/capacity", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.registerCapacity))).Methods("POST").Name("node-capacity-v1") nodesAuthenticated.HandleFunc("/{node_id}/uptime", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.updateUptimeHandler))).Methods("POST").Name("node-uptime-v1") diff --git a/pkg/directory/types/node.go b/pkg/directory/types/node.go index a91c709c..4b5ecaa1 100644 --- a/pkg/directory/types/node.go +++ b/pkg/directory/types/node.go @@ -309,6 +309,13 @@ func NodeSetWGPorts(ctx context.Context, db *mongo.Database, nodeID string, port }) } +// NodeUpdateDedicated sets node dedicated user id +func NodeUpdateDedicated(ctx context.Context, db *mongo.Database, nodeID string, dedicated int64) error { + return nodeUpdate(ctx, db, nodeID, bson.M{ + "dedicated": dedicated, + }) +} + // NodePushProof push proof to node func NodePushProof(ctx context.Context, db *mongo.Database, nodeID string, proof generated.Proof) error { if nodeID == "" { diff --git a/pkg/workloads/reservation.go b/pkg/workloads/reservation.go index edb1fc75..bf9b913c 100644 --- a/pkg/workloads/reservation.go +++ b/pkg/workloads/reservation.go @@ -100,6 +100,27 @@ func (a *API) create(r *http.Request) (interface{}, mw.Response) { return nil, mw.UnAuthorized(fmt.Errorf("request user identity does not match the reservation customer-tid")) } + db := mw.Database(r) + + typ := workload.GetWorkloadType() + if typ.Any(generated.ZeroOSTypes...) { + // dedication does not apply on gateways, hence this + // check is here + var nodeFilter directory.NodeFilter + nodeFilter = nodeFilter.WithNodeID(workload.GetNodeID()) + node, err := nodeFilter.Get(r.Context(), db, false) + if err != nil { + return nil, mw.BadRequest(errors.Wrapf(err, "cannot find node with id '%s'", workload.GetNodeID())) + } + + // if a node has a dedicated ID assigned it means it will only take reservations from this user + if node.Dedicated != 0 { + if workload.GetCustomerTid() != int64(node.Dedicated) { + return nil, mw.UnAuthorized(fmt.Errorf("node accepts only reservations from user with id: %d", node.Dedicated)) + } + } + } + workload, err = a.workloadpipeline(workload, nil) if err != nil { // if failed to create pipeline, then @@ -111,8 +132,6 @@ func (a *API) create(r *http.Request) (interface{}, mw.Response) { return nil, mw.BadRequest(fmt.Errorf("invalid request wrong status '%s'", workload.GetNextAction().String())) } - db := mw.Database(r) - var filter phonebook.UserFilter filter = filter.WithID(schema.ID(workload.GetCustomerTid())) user, err := filter.Get(r.Context(), db)