github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/service.go (about) 1 /* 2 * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package endpoints 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "strconv" 25 "strings" 26 27 "github.com/gin-gonic/gin" 28 "github.com/mysteriumnetwork/go-rest/apierror" 29 30 "github.com/mysteriumnetwork/node/config" 31 "github.com/mysteriumnetwork/node/core/service" 32 "github.com/mysteriumnetwork/node/identity" 33 "github.com/mysteriumnetwork/node/services" 34 tequilapi_client "github.com/mysteriumnetwork/node/tequilapi/client" 35 "github.com/mysteriumnetwork/node/tequilapi/contract" 36 "github.com/mysteriumnetwork/node/tequilapi/utils" 37 "github.com/rs/zerolog/log" 38 ) 39 40 // ServiceEndpoint struct represents management of service resource and it's sub-resources 41 type ServiceEndpoint struct { 42 serviceManager ServiceManager 43 optionsParser map[string]services.ServiceOptionsParser 44 proposalRepository proposalRepository 45 tequilaApiClient *tequilapi_client.Client 46 } 47 48 var ( 49 // serviceTypeInvalid represents service type which is unknown to node 50 serviceTypeInvalid = "<unknown>" 51 // serviceOptionsInvalid represents service options which is unknown to node (i.e. invalid structure for given type) 52 serviceOptionsInvalid struct{} 53 ) 54 55 // NewServiceEndpoint creates and returns service endpoint 56 func NewServiceEndpoint(serviceManager ServiceManager, optionsParser map[string]services.ServiceOptionsParser, proposalRepository proposalRepository, tequilaApiClient *tequilapi_client.Client) *ServiceEndpoint { 57 return &ServiceEndpoint{ 58 serviceManager: serviceManager, 59 optionsParser: optionsParser, 60 proposalRepository: proposalRepository, 61 tequilaApiClient: tequilaApiClient, 62 } 63 } 64 65 // ServiceList provides a list of running services on the node. 66 // swagger:operation GET /services Service ServiceListResponse 67 // 68 // --- 69 // summary: List of services 70 // description: ServiceList provides a list of running services on the node. 71 // responses: 72 // 200: 73 // description: List of running services 74 // schema: 75 // "$ref": "#/definitions/ServiceListResponse" 76 // 500: 77 // description: Internal server error 78 // schema: 79 // "$ref": "#/definitions/APIError" 80 func (se *ServiceEndpoint) ServiceList(c *gin.Context) { 81 includeAll := false 82 includeAllStr := c.Request.URL.Query().Get("include_all") 83 if len(includeAllStr) > 0 { 84 var err error 85 includeAll, err = strconv.ParseBool(includeAllStr) 86 if err != nil { 87 c.Error(apierror.BadRequestField(fmt.Sprintf("Failed to parse request: %s", err.Error()), "include_all", contract.ErrCodeServiceList)) 88 return 89 } 90 } 91 92 instances := se.serviceManager.List(includeAll) 93 94 statusResponse, err := se.toServiceListResponse(instances) 95 if err != nil { 96 c.Error(apierror.Internal("Cannot list services: "+err.Error(), contract.ErrCodeServiceList)) 97 return 98 } 99 utils.WriteAsJSON(statusResponse, c.Writer) 100 } 101 102 // ServiceGet provides info for requested service on the node. 103 // swagger:operation GET /services/:id Service serviceGet 104 // 105 // --- 106 // summary: Information about service 107 // description: ServiceGet provides info for requested service on the node. 108 // responses: 109 // 200: 110 // description: Service detailed information 111 // schema: 112 // "$ref": "#/definitions/ServiceInfoDTO" 113 // 404: 114 // description: Service not found 115 // schema: 116 // "$ref": "#/definitions/APIError" 117 // 500: 118 // description: Internal server error 119 // schema: 120 // "$ref": "#/definitions/APIError" 121 func (se *ServiceEndpoint) ServiceGet(c *gin.Context) { 122 id := service.ID(c.Param("id")) 123 instance := se.serviceManager.Service(id) 124 if instance == nil { 125 c.Error(apierror.NotFound("Requested service not found")) 126 return 127 } 128 129 statusResponse, err := se.toServiceInfoResponse(id, instance) 130 if err != nil { 131 c.Error(apierror.Internal("Cannot generate response: "+err.Error(), contract.ErrCodeServiceGet)) 132 return 133 } 134 utils.WriteAsJSON(statusResponse, c.Writer) 135 } 136 137 // ServiceStart starts requested service on the node. 138 // swagger:operation POST /services Service serviceStart 139 // 140 // --- 141 // summary: Starts service 142 // description: Provider starts serving new service to consumers 143 // parameters: 144 // - in: body 145 // name: body 146 // description: Parameters in body (providerID) required for starting new service 147 // schema: 148 // $ref: "#/definitions/ServiceStartRequestDTO" 149 // responses: 150 // 201: 151 // description: Initiated service start 152 // schema: 153 // "$ref": "#/definitions/ServiceInfoDTO" 154 // 400: 155 // description: Failed to parse or request validation failed 156 // schema: 157 // "$ref": "#/definitions/APIError" 158 // 422: 159 // description: Unable to process the request at this point 160 // schema: 161 // "$ref": "#/definitions/APIError" 162 // 500: 163 // description: Internal server error 164 // schema: 165 // "$ref": "#/definitions/APIError" 166 func (se *ServiceEndpoint) ServiceStart(c *gin.Context) { 167 sr, err := se.toServiceRequest(c.Request) 168 if err != nil { 169 c.Error(apierror.ParseFailed()) 170 return 171 } 172 173 if err := validateServiceRequest(sr); err != nil { 174 c.Error(err) 175 return 176 } 177 178 if se.isAlreadyRunning(sr) { 179 c.Error(apierror.Unprocessable("Service already running", contract.ErrCodeServiceRunning)) 180 return 181 } 182 183 log.Info().Msgf("Service start options: %+v", sr) 184 id, err := se.serviceManager.Start( 185 identity.FromAddress(sr.ProviderID), 186 sr.Type, 187 sr.AccessPolicies.IDs, 188 sr.Options, 189 ) 190 if err == service.ErrorLocation { 191 c.Error(apierror.Unprocessable("Cannot detect location", contract.ErrCodeServiceLocation)) 192 return 193 } else if err != nil { 194 c.Error(apierror.Internal("Cannot start service: "+err.Error(), contract.ErrCodeServiceStart)) 195 return 196 } 197 198 instance := se.serviceManager.Service(id) 199 200 c.Status(http.StatusCreated) 201 statusResponse, err := se.toServiceInfoResponse(id, instance) 202 if err != nil { 203 c.Error(apierror.Internal("Cannot generate response: "+err.Error(), contract.ErrCodeServiceGet)) 204 return 205 } 206 207 if ignoreUserConfig, _ := strconv.ParseBool(c.Query("ignore_user_config")); !ignoreUserConfig { 208 se.updateActiveServicesInUserConfig() 209 } 210 211 utils.WriteAsJSON(statusResponse, c.Writer) 212 } 213 214 // ServiceStop stops service on the node. 215 // swagger:operation DELETE /services/:id Service serviceStop 216 // 217 // --- 218 // summary: Stops service 219 // description: Initiates service stop 220 // responses: 221 // 202: 222 // description: Service Stop initiated 223 // 404: 224 // description: No service exists 225 // schema: 226 // "$ref": "#/definitions/APIError" 227 // 500: 228 // description: Internal server error 229 // schema: 230 // "$ref": "#/definitions/APIError" 231 func (se *ServiceEndpoint) ServiceStop(c *gin.Context) { 232 id := service.ID(c.Param("id")) 233 instance := se.serviceManager.Service(id) 234 if instance == nil { 235 c.Error(apierror.NotFound("Service not found")) 236 return 237 } 238 239 if err := se.serviceManager.Stop(id); err != nil { 240 c.Error(apierror.Internal("Cannot stop service: "+err.Error(), contract.ErrCodeServiceStop)) 241 return 242 } 243 244 if ignoreUserConfig, _ := strconv.ParseBool(c.Query("ignore_user_config")); !ignoreUserConfig { 245 se.updateActiveServicesInUserConfig() 246 } 247 248 c.Status(http.StatusAccepted) 249 } 250 251 func (se *ServiceEndpoint) updateActiveServicesInUserConfig() { 252 runningInstances := se.serviceManager.List(false) 253 activeServices := make([]string, len(runningInstances)) 254 for i, service := range runningInstances { 255 activeServices[i] = service.Type 256 } 257 config := map[string]interface{}{ 258 config.FlagActiveServices.Name: strings.Join(activeServices, ","), 259 } 260 se.tequilaApiClient.SetConfig(config) 261 } 262 263 func (se *ServiceEndpoint) isAlreadyRunning(sr contract.ServiceStartRequest) bool { 264 for _, instance := range se.serviceManager.List(false) { 265 if instance.ProviderID.Address == sr.ProviderID && instance.Type == sr.Type { 266 return true 267 } 268 } 269 return false 270 } 271 272 // AddRoutesForService adds service routes to given router 273 func AddRoutesForService( 274 serviceManager ServiceManager, 275 optionsParser map[string]services.ServiceOptionsParser, 276 proposalRepository proposalRepository, 277 tequilaApiClient *tequilapi_client.Client, 278 ) func(*gin.Engine) error { 279 serviceEndpoint := NewServiceEndpoint(serviceManager, optionsParser, proposalRepository, tequilaApiClient) 280 281 return func(e *gin.Engine) error { 282 g := e.Group("/services") 283 { 284 g.GET("", serviceEndpoint.ServiceList) 285 g.POST("", serviceEndpoint.ServiceStart) 286 g.GET("/:id", serviceEndpoint.ServiceGet) 287 g.DELETE("/:id", serviceEndpoint.ServiceStop) 288 } 289 return nil 290 } 291 } 292 293 func (se *ServiceEndpoint) toServiceRequest(req *http.Request) (contract.ServiceStartRequest, error) { 294 var jsonData struct { 295 ProviderID string `json:"provider_id"` 296 Type string `json:"type"` 297 Options *json.RawMessage `json:"options"` 298 AccessPolicies *contract.ServiceAccessPolicies `json:"access_policies"` 299 } 300 decoder := json.NewDecoder(req.Body) 301 decoder.DisallowUnknownFields() 302 if err := decoder.Decode(&jsonData); err != nil { 303 return contract.ServiceStartRequest{}, err 304 } 305 306 serviceOpts, _ := services.GetStartOptions(jsonData.Type) 307 sr := contract.ServiceStartRequest{ 308 ProviderID: jsonData.ProviderID, 309 Type: se.toServiceType(jsonData.Type), 310 Options: se.toServiceOptions(jsonData.Type, jsonData.Options), 311 AccessPolicies: &contract.ServiceAccessPolicies{ 312 IDs: serviceOpts.AccessPolicyList, 313 }, 314 } 315 if jsonData.AccessPolicies != nil { 316 sr.AccessPolicies = jsonData.AccessPolicies 317 } 318 return sr, nil 319 } 320 321 func (se *ServiceEndpoint) toServiceType(value string) string { 322 if value == "" { 323 return "" 324 } 325 326 _, ok := se.optionsParser[value] 327 if !ok { 328 return serviceTypeInvalid 329 } 330 331 return value 332 } 333 334 func (se *ServiceEndpoint) toServiceOptions(serviceType string, value *json.RawMessage) service.Options { 335 optionsParser, ok := se.optionsParser[serviceType] 336 if !ok { 337 return nil 338 } 339 340 options, err := optionsParser(value) 341 if err != nil { 342 return serviceOptionsInvalid 343 } 344 345 return options 346 } 347 348 func (se *ServiceEndpoint) toServiceInfoResponse(id service.ID, instance *service.Instance) (contract.ServiceInfoDTO, error) { 349 priced, err := se.proposalRepository.EnrichProposalWithPrice(instance.Proposal) 350 if err != nil { 351 return contract.ServiceInfoDTO{}, err 352 } 353 354 var prop *contract.ProposalDTO 355 if len(id) > 0 { 356 tmp := contract.NewProposalDTO(priced) 357 prop = &tmp 358 } 359 360 return contract.ServiceInfoDTO{ 361 ID: string(id), 362 ProviderID: instance.ProviderID.Address, 363 Type: instance.Type, 364 Options: instance.Options, 365 Status: string(instance.State()), 366 Proposal: prop, 367 }, nil 368 } 369 370 func (se *ServiceEndpoint) toServiceListResponse(instances []*service.Instance) (contract.ServiceListResponse, error) { 371 res := make([]contract.ServiceInfoDTO, 0) 372 for _, instance := range instances { 373 mapped, err := se.toServiceInfoResponse(instance.ID, instance) 374 if err != nil { 375 return nil, err 376 } 377 res = append(res, mapped) 378 } 379 return res, nil 380 } 381 382 func validateServiceRequest(sr contract.ServiceStartRequest) *apierror.APIError { 383 v := apierror.NewValidator() 384 if len(sr.ProviderID) == 0 { 385 v.Required("provider_id") 386 } 387 if sr.Type == "" { 388 v.Required("type") 389 } else if sr.Type == serviceTypeInvalid { 390 v.Invalid("type", "Invalid service type") 391 } 392 if sr.Options == serviceOptionsInvalid { 393 v.Invalid("options", "Invalid options") 394 } 395 return v.Err() 396 } 397 398 // ServiceManager represents service manager that is used for services management. 399 type ServiceManager interface { 400 Start(providerID identity.Identity, serviceType string, policies []string, options service.Options) (service.ID, error) 401 Stop(id service.ID) error 402 Service(id service.ID) *service.Instance 403 Kill() error 404 List(includeAll bool) []*service.Instance 405 }