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  }