From 7e5a8ed66358774d85ebbdad5ad941f352afe921 Mon Sep 17 00:00:00 2001 From: Javier Marcos <1271349+javuto@users.noreply.github.com> Date: Fri, 29 Aug 2025 23:28:24 +0200 Subject: [PATCH] Tagging node using osctrl-api --- cmd/api/handlers/nodes.go | 49 ++++++++++++++++++++++++++++++ cmd/api/main.go | 3 ++ cmd/cli/api-node.go | 21 ++++++++++++- cmd/cli/node.go | 2 +- osctrl-api.yaml | 63 +++++++++++++++++++++++++++++++++++++++ pkg/tags/tags.go | 4 +++ pkg/types/types.go | 7 +++++ 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/cmd/api/handlers/nodes.go b/cmd/api/handlers/nodes.go index e0d17a53..933094cd 100644 --- a/cmd/api/handlers/nodes.go +++ b/cmd/api/handlers/nodes.go @@ -218,6 +218,55 @@ func (h *HandlersApi) DeleteNodeHandler(w http.ResponseWriter, r *http.Request) utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiGenericResponse{Message: "node deleted"}) } +// TagNodeHandler - POST Handler to tag a node +func (h *HandlersApi) TagNodeHandler(w http.ResponseWriter, r *http.Request) { + // Debug HTTP if enabled + if h.DebugHTTPConfig.Enabled { + utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody) + } + // Extract environment + envVar := r.PathValue("env") + if envVar == "" { + apiErrorResponse(w, "error with environment", http.StatusBadRequest, nil) + return + } + // Get environment + env, err := h.Envs.GetByUUID(envVar) + if err != nil { + apiErrorResponse(w, "error getting environment", http.StatusInternalServerError, nil) + return + } + // Get context data and check access + ctx := r.Context().Value(ContextKey(contextAPI)).(ContextValue) + if !h.Users.CheckPermissions(ctx[ctxUser], users.AdminLevel, env.UUID) { + apiErrorResponse(w, "no access", http.StatusForbidden, fmt.Errorf("attempt to use API by user %s", ctx[ctxUser])) + return + } + var t types.ApiNodeTagRequest + // Parse request JSON body + if err := json.NewDecoder(r.Body).Decode(&t); err != nil { + apiErrorResponse(w, "error parsing POST body", http.StatusInternalServerError, err) + return + } + // Get node by UUID + n, err := h.Nodes.GetByUUIDEnv(t.UUID, env.ID) + if err != nil { + if err.Error() == "record not found" { + apiErrorResponse(w, "node not found", http.StatusNotFound, err) + } else { + apiErrorResponse(w, "error getting node", http.StatusInternalServerError, err) + } + return + } + if err := h.Tags.TagNode(t.Tag, n, ctx[ctxUser], false, t.Type); err != nil { + apiErrorResponse(w, "error tagging node", http.StatusInternalServerError, err) + return + } + // Serialize and serve JSON + log.Debug().Msgf("Tagged node %s with %s", n.UUID, t.Tag) + utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, types.ApiGenericResponse{Message: "node tagged"}) +} + // LookupNodeHandler - POST Handler to lookup a node by identifier func (h *HandlersApi) LookupNodeHandler(w http.ResponseWriter, r *http.Request) { // Debug HTTP if enabled diff --git a/cmd/api/main.go b/cmd/api/main.go index c049073a..a596fb90 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -244,6 +244,9 @@ func osctrlAPIService() { muxAPI.Handle( "POST "+_apiPath(apiNodesPath)+"/{env}/delete", handlerAuthCheck(http.HandlerFunc(handlersApi.DeleteNodeHandler), flagParams.ConfigValues.Auth, flagParams.JWTConfigValues.JWTSecret)) + muxAPI.Handle( + "POST "+_apiPath(apiNodesPath)+"/{env}/tag", + handlerAuthCheck(http.HandlerFunc(handlersApi.TagNodeHandler), flagParams.ConfigValues.Auth, flagParams.JWTConfigValues.JWTSecret)) muxAPI.Handle( "POST "+_apiPath(apiNodesPath)+"/lookup", handlerAuthCheck(http.HandlerFunc(handlersApi.LookupNodeHandler), flagParams.ConfigValues.Auth, flagParams.JWTConfigValues.JWTSecret)) diff --git a/cmd/cli/api-node.go b/cmd/cli/api-node.go index 29f183b8..e5f1d759 100644 --- a/cmd/cli/api-node.go +++ b/cmd/cli/api-node.go @@ -61,7 +61,26 @@ func (api *OsctrlAPI) DeleteNode(env, identifier string) error { } // TagNode to tag node in osctrl -func (api *OsctrlAPI) TagNode(env, identifier, tag string) error { +func (api *OsctrlAPI) TagNode(env, identifier, tag string, tagType uint) error { + t := types.ApiNodeTagRequest{ + UUID: identifier, + Tag: tag, + Type: tagType, + } + var r types.ApiGenericResponse + reqURL := path.Join(api.Configuration.URL, APIPath, APINodes, env, "tag") + jsonMessage, err := json.Marshal(t) + if err != nil { + return fmt.Errorf("error marshaling data - %w", err) + } + jsonParam := bytes.NewReader(jsonMessage) + rawN, err := api.PostGeneric(reqURL, jsonParam) + if err != nil { + return fmt.Errorf("error api request - %w - %s", err, string(rawN)) + } + if err := json.Unmarshal(rawN, &r); err != nil { + return fmt.Errorf("can not parse body - %w", err) + } return nil } diff --git a/cmd/cli/node.go b/cmd/cli/node.go index da206a0d..9eab7738 100644 --- a/cmd/cli/node.go +++ b/cmd/cli/node.go @@ -178,7 +178,7 @@ func tagNode(c *cli.Context) error { return fmt.Errorf("error tagging - %w", err) } } else if apiFlag { - if err := osctrlAPI.TagNode(env, uuid, tag); err != nil { + if err := osctrlAPI.TagNode(env, uuid, tag, tagTypeInt); err != nil { return fmt.Errorf("error tagging node - %w", err) } } diff --git a/osctrl-api.yaml b/osctrl-api.yaml index 948fcacd..93441aa0 100644 --- a/osctrl-api.yaml +++ b/osctrl-api.yaml @@ -304,6 +304,59 @@ paths: security: - Authorization: - admin + /nodes/{env}/tag: + post: + tags: + - nodes + summary: Tags node + description: Tags an existing node by identifier (UUID, hostname or localname) + operationId: TagNodeHandler + parameters: + - name: env + in: path + description: Name or UUID of the requested osctrl environment + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ApiNodeTagRequest" + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiGenericResponse" + 400: + description: bad request + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 403: + description: no access + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 404: + description: no nodes + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + 500: + description: error tagging node + content: + application/json: + schema: + $ref: "#/components/schemas/ApiErrorResponse" + security: + - Authorization: + - admin /nodes/lookup: post: tags: @@ -2081,6 +2134,16 @@ components: properties: uuid: type: string + ApiNodeTagRequest: + type: object + properties: + uuid: + type: string + tag: + type: string + type: + type: integer + format: int32 DistributedQuery: type: object properties: diff --git a/pkg/tags/tags.go b/pkg/tags/tags.go index 2d2a5fa7..8d03ad10 100644 --- a/pkg/tags/tags.go +++ b/pkg/tags/tags.go @@ -34,6 +34,10 @@ const ( ActionEdit string = "edit" // ActionRemove as action to remove a tag ActionRemove string = "remove" + // ActionTag as action to tag a node + ActionTag string = "tag" + // ActionUntag as action to untag a node + ActionUntag string = "untag" ) // AdminTag to hold all tags diff --git a/pkg/types/types.go b/pkg/types/types.go index 556ed5b4..011eb1c0 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -66,6 +66,13 @@ type ApiNodeGenericRequest struct { UUID string `json:"uuid"` } +// ApiNodeTagRequest to receive tag node requests +type ApiNodeTagRequest struct { + UUID string `json:"uuid"` + Tag string `json:"tag"` + Type uint `json:"type"` +} + // ApiLoginRequest to receive login requests type ApiLoginRequest struct { Username string `json:"username"`