package coderd import ( "database/sql" "errors" "net/http" "slices" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/codersdk" ) // @Summary Upsert workspace agent port share // @ID upsert-workspace-agent-port-share // @Security CoderSessionToken // @Accept json // @Produce json // @Tags PortSharing // @Param workspace path string true "Workspace ID" format(uuid) // @Param request body codersdk.UpsertWorkspaceAgentPortShareRequest true "Upsert port sharing level request" // @Success 200 {object} codersdk.WorkspaceAgentPortShare // @Router /workspaces/{workspace}/port-share [post] func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspace := httpmw.WorkspaceParam(r) portSharer := *api.PortSharer.Load() var req codersdk.UpsertWorkspaceAgentPortShareRequest if !httpapi.Read(ctx, rw, r, &req) { return } if !req.ShareLevel.ValidPortShareLevel() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Port sharing level not allowed.", Validations: []codersdk.ValidationError{ { Field: "share_level", Detail: "Port sharing level not allowed.", }, }, }) return } if req.Port < 9 || req.Port > 65535 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Port must be between 9 and 65535.", Validations: []codersdk.ValidationError{ { Field: "port", Detail: "Port must be between 9 and 65535.", }, }, }) return } if !req.Protocol.ValidPortProtocol() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Port protocol not allowed.", }) return } template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID) if err != nil { httpapi.InternalServerError(rw, err) return } err = portSharer.AuthorizedPortSharingLevel(template, req.ShareLevel) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: err.Error(), }) return } agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID) if err != nil { httpapi.InternalServerError(rw, err) return } found := false for _, agent := range agents { if agent.Name == req.AgentName { found = true break } } if !found { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Agent not found.", }) return } psl, err := api.Database.UpsertWorkspaceAgentPortShare(ctx, database.UpsertWorkspaceAgentPortShareParams{ WorkspaceID: workspace.ID, AgentName: req.AgentName, Port: req.Port, ShareLevel: database.AppSharingLevel(req.ShareLevel), Protocol: database.PortShareProtocol(req.Protocol), }) if err != nil { httpapi.InternalServerError(rw, err) return } httpapi.Write(ctx, rw, http.StatusOK, convertPortShare(psl)) } // @Summary Get workspace agent port shares // @ID get-workspace-agent-port-shares // @Security CoderSessionToken // @Produce json // @Tags PortSharing // @Param workspace path string true "Workspace ID" format(uuid) // @Success 200 {object} codersdk.WorkspaceAgentPortShares // @Router /workspaces/{workspace}/port-share [get] func (api *API) workspaceAgentPortShares(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspace := httpmw.WorkspaceParam(r) shares, err := api.Database.ListWorkspaceAgentPortShares(ctx, workspace.ID) if err != nil { httpapi.InternalServerError(rw, err) return } httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentPortShares{ Shares: convertPortShares(shares), }) } // @Summary Get workspace agent port shares // @ID get-workspace-agent-port-shares // @Security CoderSessionToken // @Accept json // @Tags PortSharing // @Param workspace path string true "Workspace ID" format(uuid) // @Param request body codersdk.DeleteWorkspaceAgentPortShareRequest true "Delete port sharing level request" // @Success 200 // @Router /workspaces/{workspace}/port-share [delete] func (api *API) deleteWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspace := httpmw.WorkspaceParam(r) var req codersdk.DeleteWorkspaceAgentPortShareRequest if !httpapi.Read(ctx, rw, r, &req) { return } _, err := api.Database.GetWorkspaceAgentPortShare(ctx, database.GetWorkspaceAgentPortShareParams{ WorkspaceID: workspace.ID, AgentName: req.AgentName, Port: req.Port, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ Message: "Port share not found.", }) return } httpapi.InternalServerError(rw, err) return } err = api.Database.DeleteWorkspaceAgentPortShare(ctx, database.DeleteWorkspaceAgentPortShareParams{ WorkspaceID: workspace.ID, AgentName: req.AgentName, Port: req.Port, }) if err != nil { httpapi.InternalServerError(rw, err) return } rw.WriteHeader(http.StatusOK) } func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.WorkspaceAgentPortShare { converted := []codersdk.WorkspaceAgentPortShare{} for _, share := range shares { converted = append(converted, convertPortShare(share)) } slices.SortFunc(converted, func(i, j codersdk.WorkspaceAgentPortShare) int { return (int)(i.Port - j.Port) }) return converted } func convertPortShare(share database.WorkspaceAgentPortShare) codersdk.WorkspaceAgentPortShare { return codersdk.WorkspaceAgentPortShare{ WorkspaceID: share.WorkspaceID, AgentName: share.AgentName, Port: share.Port, ShareLevel: codersdk.WorkspaceAgentPortShareLevel(share.ShareLevel), Protocol: codersdk.WorkspaceAgentPortShareProtocol(share.Protocol), } }