diff --git a/cmd/main.go b/cmd/main.go index 2a9a6f1..2d9c89b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,12 @@ package main import ( + "context" "os" + "os/signal" "path/filepath" "runtime/debug" + "syscall" "github.com/urfave/cli" @@ -56,13 +59,22 @@ func action(cliCtx *cli.Context) error { } factory.NssfConfig = cfg - nssf, err := service.NewApp(cfg) + ctx, cancel := context.WithCancel(context.Background()) + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sigCh // Wait for interrupt signal to gracefully shutdown + cancel() // Notify each goroutine and wait them stopped + }() + + nssf, err := service.NewApp(ctx, cfg, tlsKeyLogPath) if err != nil { return err } NSSF = nssf - nssf.Start(tlsKeyLogPath) + nssf.Start() return nil } diff --git a/go.mod b/go.mod index 6623e22..b7e2fcb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 github.com/urfave/cli v1.22.5 + go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index bdefb50..1f01fc8 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/internal/context/context.go b/internal/context/context.go index 757e07e..9f7f624 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -22,6 +22,8 @@ import ( "github.com/free5gc/openapi/oauth" ) +const NRF_PORT = 29510 + var nssfContext = NSSFContext{} // Initialize NSSF context with default value @@ -40,7 +42,7 @@ func Init() { } nssfContext.NfService = initNfService(serviceName, "1.0.0") - nssfContext.NrfUri = fmt.Sprintf("%s://%s:%d", models.UriScheme_HTTPS, nssfContext.RegisterIPv4, 29510) + nssfContext.NrfUri = fmt.Sprintf("%s://%s:%d", models.UriScheme_HTTPS, nssfContext.RegisterIPv4, NRF_PORT) } type NFContext interface { @@ -71,6 +73,8 @@ func InitNssfContext() { nssfContext.Name = nssfConfig.Configuration.NssfName } + nssfContext.NfId = uuid.New().String() + nssfContext.Name = "NSSF" nssfContext.UriScheme = nssfConfig.Configuration.Sbi.Scheme nssfContext.RegisterIPv4 = nssfConfig.Configuration.Sbi.RegisterIPv4 nssfContext.SBIPort = nssfConfig.Configuration.Sbi.Port @@ -91,7 +95,7 @@ func InitNssfContext() { nssfContext.NrfUri = nssfConfig.Configuration.NrfUri } else { logger.InitLog.Warn("NRF Uri is empty! Using localhost as NRF IPv4 address.") - nssfContext.NrfUri = fmt.Sprintf("%s://%s:%d", nssfContext.UriScheme, "127.0.0.1", 29510) + nssfContext.NrfUri = fmt.Sprintf("%s://%s:%d", nssfContext.UriScheme, "127.0.0.1", NRF_PORT) } nssfContext.NrfCertPem = nssfConfig.Configuration.NrfCertPem nssfContext.SupportedPlmnList = nssfConfig.Configuration.SupportedPlmnList diff --git a/internal/sbi/api_nssaiavailability.go b/internal/sbi/api_nssaiavailability.go new file mode 100644 index 0000000..a037c28 --- /dev/null +++ b/internal/sbi/api_nssaiavailability.go @@ -0,0 +1,235 @@ +package sbi + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "github.com/free5gc/nssf/internal/logger" + "github.com/free5gc/nssf/internal/plugin" + "github.com/free5gc/nssf/internal/util" + "github.com/free5gc/openapi" + "github.com/free5gc/openapi/models" +) + +func (s *Server) getNssaiAvailabilityRoutes() []Route { + return []Route{ + { + "Health Check", + strings.ToUpper("Get"), + "/", + func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{"status": "Service Available"}) + }, + }, + + { + "NSSAIAvailabilityDelete", + strings.ToUpper("Delete"), + "/nssai-availability/:nfId", + s.NSSAIAvailabilityDelete, + }, + + { + "NSSAIAvailabilityPatch", + strings.ToUpper("Patch"), + "/nssai-availability/:nfId", + s.NSSAIAvailabilityPatch, + }, + + { + "NSSAIAvailabilityPut", + strings.ToUpper("Put"), + "/nssai-availability/:nfId", + s.NSSAIAvailabilityPut, + }, + + // Regular expressions for route matching should be unique in Gin package + // 'subscriptions' would conflict with existing wildcard ':nfId' + // Simply replace 'subscriptions' with ':nfId' and check if ':nfId' is 'subscriptions' in handler function + { + "NSSAIAvailabilityUnsubscribe", + strings.ToUpper("Delete"), + // "/nssai-availability/subscriptions/:subscriptionId", + "/nssai-availability/:nfId/:subscriptionId", + s.NSSAIAvailabilityUnsubscribeDelete, + }, + + { + "NSSAIAvailabilityPost", + strings.ToUpper("Post"), + "/nssai-availability/subscriptions", + s.NSSAIAvailabilityPost, + }, + } +} + +// NSSAIAvailabilityDelete - Deletes an already existing S-NSSAIs per TA +// provided by the NF service consumer (e.g AMF) +func (s *Server) NSSAIAvailabilityDelete(c *gin.Context) { + logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityDelete") + + nfId := c.Params.ByName("nfId") + + if nfId == "" { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + util.GinProblemJson(c, problemDetails) + return + } + + s.Processor().NssaiAvailabilityNfInstanceDelete(c, nfId) +} + +// NSSAIAvailabilityPatch - Updates an already existing S-NSSAIs per TA +// provided by the NF service consumer (e.g AMF) +func (s *Server) NSSAIAvailabilityPatch(c *gin.Context) { + logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityPatch") + + nfId := c.Params.ByName("nfId") + + if nfId == "" { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + util.GinProblemJson(c, problemDetails) + return + } + + var patchDocument plugin.PatchDocument + + requestBody, err := c.GetRawData() + if err != nil { + problemDetails := &models.ProblemDetails{ + Status: http.StatusInternalServerError, + Cause: "SYSTEM_FAILURE", + } + + util.GinProblemJson(c, problemDetails) + return + } + + if err = openapi.Deserialize(&patchDocument, requestBody, "application/json"); err != nil { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + logger.SBILog.Errorf("Error deserializing patch document: %+v", err) + util.GinProblemJson(c, problemDetails) + return + } + + // TODO: Request NfProfile of NfId from NRF + // Check if NfId is valid AMF and obtain AMF Set ID + // If NfId is invalid, return ProblemDetails with code 404 Not Found + // If NF consumer is not authorized to update NSSAI availability, return ProblemDetails with code 403 Forbidden + + s.Processor().NssaiAvailabilityNfInstancePatch(c, patchDocument, nfId) +} + +// NSSAIAvailabilityPut - Updates/replaces the NSSF +// with the S-NSSAIs the NF service consumer (e.g AMF) supports per TA +func (s *Server) NSSAIAvailabilityPut(c *gin.Context) { + logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityPut") + + nfId := c.Params.ByName("nfId") + + if nfId == "" { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + util.GinProblemJson(c, problemDetails) + return + } + + var nssaiAvailabilityInfo models.NssaiAvailabilityInfo + data, err := c.GetRawData() + if err != nil { + problemDetails := &models.ProblemDetails{ + Status: http.StatusInternalServerError, + Cause: "SYSTEM_FAILURE", + } + + util.GinProblemJson(c, problemDetails) + return + } + + if err = openapi.Deserialize(&nssaiAvailabilityInfo, data, "application/json"); err != nil { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + logger.SBILog.Errorf("Error deserializing NSSAI availability info: %+v", err) + util.GinProblemJson(c, problemDetails) + return + } + + s.Processor().NssaiAvailabilityNfInstanceUpdate(c, nssaiAvailabilityInfo, nfId) +} + +func (s *Server) NSSAIAvailabilityPost(c *gin.Context) { + var createData models.NssfEventSubscriptionCreateData + + requestBody, err := c.GetRawData() + if err != nil { + problemDetail := &models.ProblemDetails{ + Title: "System failure", + Status: http.StatusInternalServerError, + Detail: err.Error(), + Cause: "SYSTEM_FAILURE", + } + logger.NssaiavailLog.Errorf("Get Request Body error: %+v", err) + + util.GinProblemJson(c, problemDetail) + return + } + + err = openapi.Deserialize(&createData, requestBody, "application/json") + if err != nil { + problemDetail := "[Request Body] " + err.Error() + rsp := &models.ProblemDetails{ + Title: "Malformed request syntax", + Status: http.StatusBadRequest, + Detail: problemDetail, + } + logger.NssaiavailLog.Errorln(problemDetail) + + util.GinProblemJson(c, rsp) + return + } + + s.Processor().NssaiAvailabilitySubscriptionCreate(c, createData) +} + +func (s *Server) NSSAIAvailabilityUnsubscribeDelete(c *gin.Context) { + // Due to conflict of route matching, 'subscriptions' in the route is replaced with the existing wildcard ':nfId' + nfID := c.Param("nfId") + if nfID != "subscriptions" { + c.JSON(http.StatusNotFound, gin.H{}) + logger.NssaiavailLog.Infof("404 Not Found") + return + } + + subscriptionId := c.Params.ByName("subscriptionId") + if subscriptionId == "" { + problemDetails := &models.ProblemDetails{ + Status: http.StatusBadRequest, + Cause: "UNSPECIFIED", // TODO: Check if this is the correct cause + } + + util.GinProblemJson(c, problemDetails) + return + } + + s.Processor().NssaiAvailabilitySubscriptionUnsubscribe(c, subscriptionId) +} diff --git a/internal/sbi/api_nsselection.go b/internal/sbi/api_nsselection.go new file mode 100644 index 0000000..2d3fd51 --- /dev/null +++ b/internal/sbi/api_nsselection.go @@ -0,0 +1,37 @@ +package sbi + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "github.com/free5gc/nssf/internal/logger" +) + +func (s *Server) getNsSelectionRoutes() []Route { + return []Route{ + { + "Health Check", + strings.ToUpper("Get"), + "/", + func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{"status": "Service Available"}) + }, + }, + + { + "NSSelectionGet", + strings.ToUpper("Get"), + "/network-slice-information", + s.NetworkSliceInformationGet, + }, + } +} + +func (s *Server) NetworkSliceInformationGet(c *gin.Context) { + logger.NsselLog.Infof("Handle NSSelectionGet") + + query := c.Request.URL.Query() + s.Processor().NSSelectionSliceInformationGet(c, query) +} diff --git a/internal/sbi/consumer/consumer.go b/internal/sbi/consumer/consumer.go new file mode 100644 index 0000000..780cf24 --- /dev/null +++ b/internal/sbi/consumer/consumer.go @@ -0,0 +1,25 @@ +package consumer + +import ( + "github.com/free5gc/nssf/pkg/app" + "github.com/free5gc/openapi/Nnrf_NFManagement" +) + +type Consumer struct { + app.NssfApp + + *NrfService +} + +func NewConsumer(nssf app.NssfApp) *Consumer { + configuration := Nnrf_NFManagement.NewConfiguration() + configuration.SetBasePath(nssf.Context().NrfUri) + nrfService := &NrfService{ + nrfNfMgmtClient: Nnrf_NFManagement.NewAPIClient(configuration), + } + + return &Consumer{ + NssfApp: nssf, + NrfService: nrfService, + } +} diff --git a/internal/sbi/consumer/nf_management.go b/internal/sbi/consumer/nf_management.go deleted file mode 100644 index f6d4bdf..0000000 --- a/internal/sbi/consumer/nf_management.go +++ /dev/null @@ -1,126 +0,0 @@ -/* - * NSSF Consumer - * - * Network Function Management - */ - -package consumer - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - nssf_context "github.com/free5gc/nssf/internal/context" - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/openapi" - "github.com/free5gc/openapi/Nnrf_NFManagement" - "github.com/free5gc/openapi/models" -) - -func BuildNFProfile(context *nssf_context.NSSFContext) (profile models.NfProfile, err error) { - profile.NfInstanceId = context.NfId - profile.NfType = models.NfType_NSSF - profile.NfStatus = models.NfStatus_REGISTERED - profile.PlmnList = &context.SupportedPlmnList - profile.Ipv4Addresses = []string{context.RegisterIPv4} - var services []models.NfService - for _, nfService := range context.NfService { - services = append(services, nfService) - } - if len(services) > 0 { - profile.NfServices = &services - } - return -} - -func SendRegisterNFInstance(nrfUri, nfInstanceId string, profile models.NfProfile) ( - resourceNrfUri string, retrieveNfInstanceId string, err error, -) { - configuration := Nnrf_NFManagement.NewConfiguration() - configuration.SetBasePath(nrfUri) - apiClient := Nnrf_NFManagement.NewAPIClient(configuration) - - var res *http.Response - var nf models.NfProfile - for { - nf, res, err = apiClient.NFInstanceIDDocumentApi.RegisterNFInstance(context.TODO(), nfInstanceId, profile) - if err != nil || res == nil { - // TODO : add log - logger.ConsumerLog.Errorf("NSSF register to NRF Error[%s]", err.Error()) - time.Sleep(2 * time.Second) - continue - } - defer func() { - if resCloseErr := res.Body.Close(); resCloseErr != nil { - logger.ConsumerLog.Errorf("NFInstanceIDDocumentApi response body cannot close: %+v", resCloseErr) - } - }() - status := res.StatusCode - if status == http.StatusOK { - // NFUpdate - break - } else if status == http.StatusCreated { - // NFRegister - resourceUri := res.Header.Get("Location") - resourceNrfUri = resourceUri[:strings.Index(resourceUri, "/nnrf-nfm/")] - retrieveNfInstanceId = resourceUri[strings.LastIndex(resourceUri, "/")+1:] - - oauth2 := false - if nf.CustomInfo != nil { - v, ok := nf.CustomInfo["oauth2"].(bool) - if ok { - oauth2 = v - logger.MainLog.Infoln("OAuth2 setting receive from NRF:", oauth2) - } - } - nssf_context.GetSelf().OAuth2Required = oauth2 - if oauth2 && nssf_context.GetSelf().NrfCertPem == "" { - logger.CfgLog.Error("OAuth2 enable but no nrfCertPem provided in config.") - } - break - } else { - fmt.Println("NRF return wrong status code", status) - } - } - return resourceNrfUri, retrieveNfInstanceId, err -} - -func SendDeregisterNFInstance() (*models.ProblemDetails, error) { - logger.ConsumerLog.Infof("Send Deregister NFInstance") - - var err error - - ctx, pd, err := nssf_context.GetSelf().GetTokenCtx(models.ServiceName_NNRF_NFM, models.NfType_NRF) - if err != nil { - return pd, err - } - - nssfSelf := nssf_context.GetSelf() - // Set client and set url - configuration := Nnrf_NFManagement.NewConfiguration() - configuration.SetBasePath(nssfSelf.NrfUri) - client := Nnrf_NFManagement.NewAPIClient(configuration) - - var res *http.Response - - res, err = client.NFInstanceIDDocumentApi.DeregisterNFInstance(ctx, nssfSelf.NfId) - if err == nil { - return nil, err - } else if res != nil { - defer func() { - if resCloseErr := res.Body.Close(); resCloseErr != nil { - logger.ConsumerLog.Errorf("NFInstanceIDDocumentApi response body cannot close: %+v", resCloseErr) - } - }() - if res.Status != err.Error() { - return nil, err - } - problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails) - return &problem, err - } else { - return nil, openapi.ReportError("server no response") - } -} diff --git a/internal/sbi/consumer/nrf_service.go b/internal/sbi/consumer/nrf_service.go new file mode 100644 index 0000000..987be87 --- /dev/null +++ b/internal/sbi/consumer/nrf_service.go @@ -0,0 +1,138 @@ +/* + * NSSF Consumer + * + * Network Function Management + */ + +package consumer + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + nssf_context "github.com/free5gc/nssf/internal/context" + "github.com/free5gc/nssf/internal/logger" + "github.com/free5gc/openapi" + "github.com/free5gc/openapi/Nnrf_NFManagement" + "github.com/free5gc/openapi/models" +) + +type NrfService struct { + nrfNfMgmtClient *Nnrf_NFManagement.APIClient + // NOTE: No mutex needed. One connection at a time. +} + +func (ns *NrfService) buildNFProfile(context *nssf_context.NSSFContext) (profile models.NfProfile, err error) { + profile.NfInstanceId = context.NfId + profile.NfType = models.NfType_NSSF + profile.NfStatus = models.NfStatus_REGISTERED + profile.PlmnList = &context.SupportedPlmnList + profile.Ipv4Addresses = []string{context.RegisterIPv4} + var services []models.NfService + for _, nfService := range context.NfService { + services = append(services, nfService) + } + if len(services) > 0 { + profile.NfServices = &services + } + return +} + +func (ns *NrfService) SendRegisterNFInstance(ctx context.Context, nssfCtx *nssf_context.NSSFContext) ( + resourceNrfUri string, retrieveNfInstanceId string, err error, +) { + nfInstanceId := nssfCtx.NfId + profile, err := ns.buildNFProfile(nssfCtx) + if err != nil { + return "", "", fmt.Errorf("failed to build nrf profile: %s", err.Error()) + } + apiClient := ns.nrfNfMgmtClient + + var res *http.Response + var nf models.NfProfile + finish := false + for !finish { + select { + case <-ctx.Done(): + return "", "", fmt.Errorf("context done") + + default: + nf, res, err = apiClient.NFInstanceIDDocumentApi.RegisterNFInstance(ctx, nfInstanceId, profile) + if err != nil || res == nil { + // TODO : add log + logger.ConsumerLog.Errorf("NSSF register to NRF Error[%s]", err.Error()) + const retryInterval = 2 * time.Second + time.Sleep(retryInterval) + continue + } + defer func() { + if resCloseErr := res.Body.Close(); resCloseErr != nil { + logger.ConsumerLog.Errorf("NFInstanceIDDocumentApi response body cannot close: %+v", resCloseErr) + } + }() + status := res.StatusCode + if status == http.StatusOK { + // NFUpdate + finish = true + } else if status == http.StatusCreated { + // NFRegister + resourceUri := res.Header.Get("Location") + resourceNrfUri, _, _ = strings.Cut(resourceUri, "/nnrf-nfm/") + retrieveNfInstanceId = resourceUri[strings.LastIndex(resourceUri, "/")+1:] + + oauth2 := false + if nf.CustomInfo != nil { + v, ok := nf.CustomInfo["oauth2"].(bool) + if ok { + oauth2 = v + logger.MainLog.Infoln("OAuth2 setting receive from NRF:", oauth2) + } + } + nssf_context.GetSelf().OAuth2Required = oauth2 + if oauth2 && nssf_context.GetSelf().NrfCertPem == "" { + logger.CfgLog.Error("OAuth2 enable but no nrfCertPem provided in config.") + } + finish = true + } else { + fmt.Println("NRF return wrong status code", status) + } + } + } + return resourceNrfUri, retrieveNfInstanceId, err +} + +func (ns *NrfService) SendDeregisterNFInstance(nfInstanceId string) (*models.ProblemDetails, error) { + logger.ConsumerLog.Infof("Send Deregister NFInstance") + + var err error + + ctx, pd, err := nssf_context.GetSelf().GetTokenCtx(models.ServiceName_NNRF_NFM, models.NfType_NRF) + if err != nil { + return pd, err + } + + client := ns.nrfNfMgmtClient + + var res *http.Response + + res, err = client.NFInstanceIDDocumentApi.DeregisterNFInstance(ctx, nfInstanceId) + if err == nil { + return nil, err + } else if res != nil { + defer func() { + if resCloseErr := res.Body.Close(); resCloseErr != nil { + logger.ConsumerLog.Errorf("NFInstanceIDDocumentApi response body cannot close: %+v", resCloseErr) + } + }() + if res.Status != err.Error() { + return nil, err + } + problem := err.(openapi.GenericOpenAPIError).Model().(models.ProblemDetails) + return &problem, err + } else { + return nil, openapi.ReportError("server no response") + } +} diff --git a/internal/sbi/nssaiavailability/api_nf_instance_id_document.go b/internal/sbi/nssaiavailability/api_nf_instance_id_document.go deleted file mode 100644 index ac1f874..0000000 --- a/internal/sbi/nssaiavailability/api_nf_instance_id_document.go +++ /dev/null @@ -1,139 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nssaiavailability - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/plugin" - "github.com/free5gc/nssf/internal/sbi/producer" - "github.com/free5gc/openapi" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -func HTTPNSSAIAvailabilityDelete(c *gin.Context) { - req := httpwrapper.NewRequest(c.Request, nil) - req.Params["nfId"] = c.Params.ByName("nfId") - - rsp := producer.HandleNSSAIAvailabilityDelete(req) - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NssaiavailLog.Errorln(err) - problemDetails := models.ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} - -func HTTPNSSAIAvailabilityPatch(c *gin.Context) { - var nssaiAvailabilityUpdateInfo plugin.PatchDocument - - requestBody, err := c.GetRawData() - if err != nil { - problemDetail := models.ProblemDetails{ - Title: "System failure", - Status: http.StatusInternalServerError, - Detail: err.Error(), - Cause: "SYSTEM_FAILURE", - } - logger.NssaiavailLog.Errorf("Get Request Body error: %+v", err) - c.JSON(http.StatusInternalServerError, problemDetail) - return - } - - err = openapi.Deserialize(&nssaiAvailabilityUpdateInfo, requestBody, "application/json") - if err != nil { - problemDetail := "[Request Body] " + err.Error() - rsp := models.ProblemDetails{ - Title: "Malformed request syntax", - Status: http.StatusBadRequest, - Detail: problemDetail, - } - logger.NssaiavailLog.Errorln(problemDetail) - c.JSON(http.StatusBadRequest, rsp) - return - } - - req := httpwrapper.NewRequest(c.Request, nssaiAvailabilityUpdateInfo) - req.Params["nfId"] = c.Params.ByName("nfId") - - rsp := producer.HandleNSSAIAvailabilityPatch(req) - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NssaiavailLog.Errorln(err) - problemDetails := models.ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} - -func HTTPNSSAIAvailabilityPut(c *gin.Context) { - var nssaiAvailabilityInfo models.NssaiAvailabilityInfo - - requestBody, err := c.GetRawData() - if err != nil { - problemDetail := models.ProblemDetails{ - Title: "System failure", - Status: http.StatusInternalServerError, - Detail: err.Error(), - Cause: "SYSTEM_FAILURE", - } - logger.NssaiavailLog.Errorf("Get Request Body error: %+v", err) - c.JSON(http.StatusInternalServerError, problemDetail) - return - } - - err = openapi.Deserialize(&nssaiAvailabilityInfo, requestBody, "application/json") - if err != nil { - problemDetail := "[Request Body] " + err.Error() - rsp := models.ProblemDetails{ - Title: "Malformed request syntax", - Status: http.StatusBadRequest, - Detail: problemDetail, - } - logger.NssaiavailLog.Errorln(problemDetail) - c.JSON(http.StatusBadRequest, rsp) - return - } - - req := httpwrapper.NewRequest(c.Request, nssaiAvailabilityInfo) - req.Params["nfId"] = c.Params.ByName("nfId") - - rsp := producer.HandleNSSAIAvailabilityPut(req) - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NssaiavailLog.Errorln(err) - problemDetails := models.ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} diff --git a/internal/sbi/nssaiavailability/api_subscription_id_document.go b/internal/sbi/nssaiavailability/api_subscription_id_document.go deleted file mode 100644 index f0ffa94..0000000 --- a/internal/sbi/nssaiavailability/api_subscription_id_document.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nssaiavailability - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/sbi/producer" - "github.com/free5gc/openapi" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -func HTTPNSSAIAvailabilityUnsubscribe(c *gin.Context) { - // Due to conflict of route matching, 'subscriptions' in the route is replaced with the existing wildcard ':nfId' - nfID := c.Param("nfId") - if nfID != "subscriptions" { - c.JSON(http.StatusNotFound, gin.H{}) - logger.NssaiavailLog.Infof("404 Not Found") - return - } - - req := httpwrapper.NewRequest(c.Request, nil) - req.Params["subscriptionId"] = c.Params.ByName("subscriptionId") - - rsp := producer.HandleNSSAIAvailabilityUnsubscribe(req) - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NssaiavailLog.Errorln(err) - problemDetails := models.ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} diff --git a/internal/sbi/nssaiavailability/api_subscriptions_collection.go b/internal/sbi/nssaiavailability/api_subscriptions_collection.go deleted file mode 100644 index b724f68..0000000 --- a/internal/sbi/nssaiavailability/api_subscriptions_collection.go +++ /dev/null @@ -1,71 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nssaiavailability - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/sbi/producer" - "github.com/free5gc/openapi" - . "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -func HTTPNSSAIAvailabilityPost(c *gin.Context) { - var createData NssfEventSubscriptionCreateData - - requestBody, err := c.GetRawData() - if err != nil { - problemDetail := ProblemDetails{ - Title: "System failure", - Status: http.StatusInternalServerError, - Detail: err.Error(), - Cause: "SYSTEM_FAILURE", - } - logger.NssaiavailLog.Errorf("Get Request Body error: %+v", err) - c.JSON(http.StatusInternalServerError, problemDetail) - return - } - - err = openapi.Deserialize(&createData, requestBody, "application/json") - if err != nil { - problemDetail := "[Request Body] " + err.Error() - rsp := ProblemDetails{ - Title: "Malformed request syntax", - Status: http.StatusBadRequest, - Detail: problemDetail, - } - logger.NssaiavailLog.Errorln(problemDetail) - c.JSON(http.StatusBadRequest, rsp) - return - } - - req := httpwrapper.NewRequest(c.Request, createData) - - rsp := producer.HandleNSSAIAvailabilityPost(req) - - // TODO: Based on TS 29.531 5.3.2.3.1, add location header - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NssaiavailLog.Errorln(err) - problemDetails := ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} diff --git a/internal/sbi/nssaiavailability/routers.go b/internal/sbi/nssaiavailability/routers.go deleted file mode 100644 index 4cccca1..0000000 --- a/internal/sbi/nssaiavailability/routers.go +++ /dev/null @@ -1,124 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nssaiavailability - -import ( - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - nssf_context "github.com/free5gc/nssf/internal/context" - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/util" - "github.com/free5gc/nssf/pkg/factory" - "github.com/free5gc/openapi/models" - logger_util "github.com/free5gc/util/logger" -) - -// Route is the information for every URI. -type Route struct { - // Name is the name of this Route. - Name string - // Method is the string for the HTTP method. ex) GET, POST etc.. - Method string - // Pattern is the pattern of the URI. - Pattern string - // HandlerFunc is the handler function of this route. - HandlerFunc gin.HandlerFunc -} - -// Routes is the list of the generated Route. -type Routes []Route - -// NewRouter returns a new router. -func NewRouter() *gin.Engine { - router := logger_util.NewGinWithLogrus(logger.GinLog) - AddService(router) - return router -} - -func AddService(engine *gin.Engine) *gin.RouterGroup { - group := engine.Group(factory.NssfNssaiavailResUriPrefix) - - routerAuthorizationCheck := util.NewRouterAuthorizationCheck(models.ServiceName_NNSSF_NSSAIAVAILABILITY) - group.Use(func(c *gin.Context) { - routerAuthorizationCheck.Check(c, nssf_context.GetSelf()) - }) - - for _, route := range routes { - switch route.Method { - case "GET": - group.GET(route.Pattern, route.HandlerFunc) - case "POST": - group.POST(route.Pattern, route.HandlerFunc) - case "PUT": - group.PUT(route.Pattern, route.HandlerFunc) - case "DELETE": - group.DELETE(route.Pattern, route.HandlerFunc) - case "PATCH": - group.PATCH(route.Pattern, route.HandlerFunc) - } - } - return group -} - -// Index is the index handler. -func Index(c *gin.Context) { - c.String(http.StatusOK, "Hello World!") -} - -var routes = Routes{ - { - "Index", - "GET", - "/", - Index, - }, - - { - "NSSAIAvailabilityDelete", - strings.ToUpper("Delete"), - "/nssai-availability/:nfId", - HTTPNSSAIAvailabilityDelete, - }, - - { - "NSSAIAvailabilityPatch", - strings.ToUpper("Patch"), - "/nssai-availability/:nfId", - HTTPNSSAIAvailabilityPatch, - }, - - { - "NSSAIAvailabilityPut", - strings.ToUpper("Put"), - "/nssai-availability/:nfId", - HTTPNSSAIAvailabilityPut, - }, - - // Regular expressions for route matching should be unique in Gin package - // 'subscriptions' would conflict with existing wildcard ':nfId' - // Simply replace 'subscriptions' with ':nfId' and check if ':nfId' is 'subscriptions' in handler function - { - "NSSAIAvailabilityUnsubscribe", - strings.ToUpper("Delete"), - // "/nssai-availability/subscriptions/:subscriptionId", - "/nssai-availability/:nfId/:subscriptionId", - HTTPNSSAIAvailabilityUnsubscribe, - }, - - { - "NSSAIAvailabilityPost", - strings.ToUpper("Post"), - "/nssai-availability/subscriptions", - HTTPNSSAIAvailabilityPost, - }, -} diff --git a/internal/sbi/nsselection/api_network_slice_information_document.go b/internal/sbi/nsselection/api_network_slice_information_document.go deleted file mode 100644 index 7b60808..0000000 --- a/internal/sbi/nsselection/api_network_slice_information_document.go +++ /dev/null @@ -1,41 +0,0 @@ -/* - * NSSF NS Selection - * - * NSSF Network Slice Selection Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nsselection - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/sbi/producer" - "github.com/free5gc/openapi" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -func HTTPNetworkSliceInformationDocument(c *gin.Context) { - req := httpwrapper.NewRequest(c.Request, nil) - - rsp := producer.HandleNSSelectionGet(req) - - responseBody, err := openapi.Serialize(rsp.Body, "application/json") - if err != nil { - logger.NsselLog.Errorln(err) - problemDetails := models.ProblemDetails{ - Status: http.StatusInternalServerError, - Cause: "SYSTEM_FAILURE", - Detail: err.Error(), - } - c.JSON(http.StatusInternalServerError, problemDetails) - } else { - c.Data(rsp.Status, "application/json", responseBody) - } -} diff --git a/internal/sbi/nsselection/routers.go b/internal/sbi/nsselection/routers.go deleted file mode 100644 index f5f0b7c..0000000 --- a/internal/sbi/nsselection/routers.go +++ /dev/null @@ -1,92 +0,0 @@ -/* - * NSSF NS Selection - * - * NSSF Network Slice Selection Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package nsselection - -import ( - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - nssf_context "github.com/free5gc/nssf/internal/context" - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/util" - "github.com/free5gc/nssf/pkg/factory" - "github.com/free5gc/openapi/models" - logger_util "github.com/free5gc/util/logger" -) - -// Route is the information for every URI. -type Route struct { - // Name is the name of this Route. - Name string - // Method is the string for the HTTP method. ex) GET, POST etc.. - Method string - // Pattern is the pattern of the URI. - Pattern string - // HandlerFunc is the handler function of this route. - HandlerFunc gin.HandlerFunc -} - -// Routes is the list of the generated Route. -type Routes []Route - -// NewRouter returns a new router. -func NewRouter() *gin.Engine { - router := logger_util.NewGinWithLogrus(logger.GinLog) - AddService(router) - return router -} - -func AddService(engine *gin.Engine) *gin.RouterGroup { - group := engine.Group(factory.NssfNsselectResUriPrefix) - - routerAuthorizationCheck := util.NewRouterAuthorizationCheck(models.ServiceName_NNSSF_NSSELECTION) - group.Use(func(c *gin.Context) { - routerAuthorizationCheck.Check(c, nssf_context.GetSelf()) - }) - - for _, route := range routes { - switch route.Method { - case "GET": - group.GET(route.Pattern, route.HandlerFunc) - case "POST": - group.POST(route.Pattern, route.HandlerFunc) - case "PUT": - group.PUT(route.Pattern, route.HandlerFunc) - case "DELETE": - group.DELETE(route.Pattern, route.HandlerFunc) - case "PATCH": - group.PATCH(route.Pattern, route.HandlerFunc) - } - } - return group -} - -// Index is the index handler. -func Index(c *gin.Context) { - c.String(http.StatusOK, "Hello World!") -} - -var routes = Routes{ - { - "Index", - "GET", - "/", - Index, - }, - - { - "NSSelectionGet", - strings.ToUpper("Get"), - "/network-slice-information", - HTTPNetworkSliceInformationDocument, - }, -} diff --git a/internal/sbi/producer/nssaiavailability_store.go b/internal/sbi/processor/nssaiavailability_store.go similarity index 85% rename from internal/sbi/producer/nssaiavailability_store.go rename to internal/sbi/processor/nssaiavailability_store.go index 73aa784..2011e55 100644 --- a/internal/sbi/producer/nssaiavailability_store.go +++ b/internal/sbi/processor/nssaiavailability_store.go @@ -4,7 +4,7 @@ * NSSF NSSAI Availability Service */ -package producer +package processor import ( "bytes" @@ -14,6 +14,7 @@ import ( "reflect" jsonpatch "github.com/evanphx/json-patch" + "github.com/gin-gonic/gin" "github.com/free5gc/nssf/internal/logger" "github.com/free5gc/nssf/internal/plugin" @@ -22,29 +23,30 @@ import ( "github.com/free5gc/openapi/models" ) -// NSSAIAvailability DELETE method -func NSSAIAvailabilityDeleteProcedure(nfId string) *models.ProblemDetails { +func (p *Processor) NssaiAvailabilityNfInstanceDelete(c *gin.Context, nfId string) { var problemDetails *models.ProblemDetails for i, amfConfig := range factory.NssfConfig.Configuration.AmfList { if amfConfig.NfId == nfId { factory.NssfConfig.Configuration.AmfList = append( factory.NssfConfig.Configuration.AmfList[:i], factory.NssfConfig.Configuration.AmfList[i+1:]...) - return nil + + c.Status(http.StatusNoContent) + return } } - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.UNSUPPORTED_RESOURCE, Status: http.StatusNotFound, Detail: fmt.Sprintf("AMF ID '%s' does not exist", nfId), } - return problemDetails + util.GinProblemJson(c, problemDetails) } -// NSSAIAvailability PATCH method -func NSSAIAvailabilityPatchProcedure(nssaiAvailabilityUpdateInfo plugin.PatchDocument, nfId string) ( - *models.AuthorizedNssaiAvailabilityInfo, *models.ProblemDetails, +func (p *Processor) NssaiAvailabilityNfInstancePatch( + c *gin.Context, + nssaiAvailabilityUpdateInfo plugin.PatchDocument, nfId string, ) { var ( response *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{} @@ -83,12 +85,13 @@ func NSSAIAvailabilityPatchProcedure(nssaiAvailabilityUpdateInfo plugin.PatchDoc } factory.NssfConfig.RUnlock() if !hitAmf { - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.UNSUPPORTED_RESOURCE, Status: http.StatusNotFound, Detail: fmt.Sprintf("AMF ID '%s' does not exist", nfId), } - return nil, problemDetails + util.GinProblemJson(c, problemDetails) + return } // TODO: Check if returned HTTP status codes or problem details are proper when errors occur @@ -110,34 +113,37 @@ func NSSAIAvailabilityPatchProcedure(nssaiAvailabilityUpdateInfo plugin.PatchDoc patch, err := jsonpatch.DecodePatch(patchJSON) if err != nil { - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.MALFORMED_REQUEST, Status: http.StatusBadRequest, Detail: err.Error(), } - return nil, problemDetails + util.GinProblemJson(c, problemDetails) + return } modified, err := patch.Apply(original) if err != nil { - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.INVALID_REQUEST, Status: http.StatusConflict, Detail: err.Error(), } - return nil, problemDetails + util.GinProblemJson(c, problemDetails) + return } factory.NssfConfig.Lock() err = json.Unmarshal(modified, &factory.NssfConfig.Configuration.AmfList[amfIdx].SupportedNssaiAvailabilityData) factory.NssfConfig.Unlock() if err != nil { - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.INVALID_REQUEST, Status: http.StatusBadRequest, Detail: err.Error(), } - return nil, problemDetails + util.GinProblemJson(c, problemDetails) + return } // Return all authorized NSSAI availability information @@ -148,12 +154,13 @@ func NSSAIAvailabilityPatchProcedure(nssaiAvailabilityUpdateInfo plugin.PatchDoc // TODO: Return authorized NSSAI availability information of updated TAI only - return response, nil + c.JSON(http.StatusOK, response) } // NSSAIAvailability PUT method -func NSSAIAvailabilityPutProcedure(nssaiAvailabilityInfo models.NssaiAvailabilityInfo, nfId string) ( - *models.AuthorizedNssaiAvailabilityInfo, *models.ProblemDetails, +func (p *Processor) NssaiAvailabilityNfInstanceUpdate( + c *gin.Context, + nssaiAvailabilityInfo models.NssaiAvailabilityInfo, nfId string, ) { var ( response *models.AuthorizedNssaiAvailabilityInfo = &models.AuthorizedNssaiAvailabilityInfo{} @@ -168,7 +175,9 @@ func NSSAIAvailabilityPutProcedure(nssaiAvailabilityInfo models.NssaiAvailabilit Detail: "S-NSSAI in Requested NSSAI is not supported in PLMN", Cause: "SNSSAI_NOT_SUPPORTED", } - return nil, problemDetails + + util.GinProblemJson(c, problemDetails) + return } } @@ -217,5 +226,5 @@ func NSSAIAvailabilityPutProcedure(nssaiAvailabilityInfo models.NssaiAvailabilit } } - return response, problemDetails + c.JSON(http.StatusOK, response) } diff --git a/internal/sbi/processor/nssaiavailability_store_test.go b/internal/sbi/processor/nssaiavailability_store_test.go new file mode 100644 index 0000000..ca915fb --- /dev/null +++ b/internal/sbi/processor/nssaiavailability_store_test.go @@ -0,0 +1,90 @@ +package processor_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "go.uber.org/mock/gomock" + + "github.com/free5gc/nssf/internal/sbi/processor" + "github.com/free5gc/nssf/internal/util" + "github.com/free5gc/nssf/pkg/app" + "github.com/free5gc/nssf/pkg/factory" + "github.com/free5gc/openapi/models" +) + +func setup() { + // Set the default values for the factory.NssfConfig + factory.NssfConfig = &factory.Config{ + Configuration: &factory.Configuration{}, + } +} + +func TestMain(m *testing.M) { + // Run the tests + setup() + m.Run() +} + +func TestNfInstanceDelete(t *testing.T) { + mockNssfApp := app.NewMockNssfApp(gomock.NewController(t)) + processor := processor.NewProcessor(mockNssfApp) + httpRecorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(httpRecorder) + + // Create a sample AMF list + amfList := []factory.AmfConfig{ + { + NfId: "nf1", + }, + { + NfId: "nf2", + }, + { + NfId: "nf3", + }, + } + + // Set the sample AMF list in the factory.NssfConfig.Configuration + factory.NssfConfig.Configuration.AmfList = amfList + + // Test case 1: Delete an existing NF instance + nfIdToDelete := "nf2" + processor.NssaiAvailabilityNfInstanceDelete(c, nfIdToDelete) + if c.Writer.Status() != http.StatusNoContent { + t.Errorf("Expected status code %d, got: %d", http.StatusNoContent, httpRecorder.Code) + } + + // Verify that the NF instance is deleted from the AMF list + for _, amfConfig := range factory.NssfConfig.Configuration.AmfList { + if amfConfig.NfId == nfIdToDelete { + t.Errorf("Expected NF instance '%s' to be deleted, but it still exists", nfIdToDelete) + } + } + + // Test case 2: Delete a non-existing NF instance + nfIdToDelete = "nf4" + expectedDetail := fmt.Sprintf("AMF ID '%s' does not exist", nfIdToDelete) + processor.NssaiAvailabilityNfInstanceDelete(c, nfIdToDelete) + if httpRecorder.Code != http.StatusNotFound { + t.Errorf("Expected status code %d, got: %d", http.StatusNotFound, httpRecorder.Code) + } + + var problemDetails models.ProblemDetails + if err := json.Unmarshal(httpRecorder.Body.Bytes(), &problemDetails); err != nil { + t.Errorf("Error unmarshalling response body: %v", err) + } + if problemDetails.Title != util.UNSUPPORTED_RESOURCE { + t.Errorf("Expected problemDetails.Title to be '%s', got: '%s'", util.UNSUPPORTED_RESOURCE, problemDetails.Title) + } + if problemDetails.Status != http.StatusNotFound { + t.Errorf("Expected problemDetails.Status to be %d, got: %d", http.StatusNotFound, problemDetails.Status) + } + if problemDetails.Detail != expectedDetail { + t.Errorf("Expected problemDetails.Detail to be '%s', got: '%s'", expectedDetail, problemDetails.Detail) + } +} diff --git a/internal/sbi/producer/nssaiavailability_subscription.go b/internal/sbi/processor/nssaiavailability_subscription.go similarity index 82% rename from internal/sbi/producer/nssaiavailability_subscription.go rename to internal/sbi/processor/nssaiavailability_subscription.go index 769d801..b08e2d9 100644 --- a/internal/sbi/producer/nssaiavailability_subscription.go +++ b/internal/sbi/processor/nssaiavailability_subscription.go @@ -4,7 +4,7 @@ * NSSF NSSAI Availability Service */ -package producer +package processor import ( "fmt" @@ -13,6 +13,8 @@ import ( "strconv" "time" + "github.com/gin-gonic/gin" + "github.com/free5gc/nssf/internal/logger" "github.com/free5gc/nssf/internal/util" "github.com/free5gc/nssf/pkg/factory" @@ -34,7 +36,7 @@ func getUnusedSubscriptionID() (string, error) { if idx == math.MaxUint32 { return "", fmt.Errorf("No available subscription ID") } - idx = idx + 1 + idx++ } else { break } @@ -43,8 +45,9 @@ func getUnusedSubscriptionID() (string, error) { } // NSSAIAvailability subscription POST method -func NSSAIAvailabilityPostProcedure(createData models.NssfEventSubscriptionCreateData) ( - *models.NssfEventSubscriptionCreatedData, *models.ProblemDetails, +func (p *Processor) NssaiAvailabilitySubscriptionCreate( + c *gin.Context, + createData models.NssfEventSubscriptionCreateData, ) { var ( response *models.NssfEventSubscriptionCreatedData = &models.NssfEventSubscriptionCreatedData{} @@ -56,12 +59,14 @@ func NSSAIAvailabilityPostProcedure(createData models.NssfEventSubscriptionCreat if err != nil { logger.NssaiavailLog.Warnf(err.Error()) - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.UNSUPPORTED_RESOURCE, Status: http.StatusNotFound, Detail: err.Error(), } - return nil, problemDetails + + util.GinProblemJson(c, problemDetails) + return } subscription.SubscriptionId = tempID @@ -77,10 +82,10 @@ func NSSAIAvailabilityPostProcedure(createData models.NssfEventSubscriptionCreat } response.AuthorizedNssaiAvailabilityData = util.AuthorizeOfTaListFromConfig(subscription.SubscriptionData.TaiList) - return response, nil + c.JSON(http.StatusOK, response) } -func NSSAIAvailabilityUnsubscribeProcedure(subscriptionId string) *models.ProblemDetails { +func (p *Processor) NssaiAvailabilitySubscriptionUnsubscribe(c *gin.Context, subscriptionId string) { var problemDetails *models.ProblemDetails factory.NssfConfig.Lock() @@ -90,15 +95,17 @@ func NSSAIAvailabilityUnsubscribeProcedure(subscriptionId string) *models.Proble factory.NssfConfig.Subscriptions = append(factory.NssfConfig.Subscriptions[:i], factory.NssfConfig.Subscriptions[i+1:]...) - return nil + c.Status(http.StatusNoContent) + return } } // No specific subscription ID exists - *problemDetails = models.ProblemDetails{ + problemDetails = &models.ProblemDetails{ Title: util.UNSUPPORTED_RESOURCE, Status: http.StatusNotFound, Detail: fmt.Sprintf("Subscription ID '%s' is not available", subscriptionId), } - return problemDetails + + util.GinProblemJson(c, problemDetails) } diff --git a/internal/sbi/producer/nsselection_for_registration.go b/internal/sbi/processor/nsselection_network_slice_information.go similarity index 64% rename from internal/sbi/producer/nsselection_for_registration.go rename to internal/sbi/processor/nsselection_network_slice_information.go index 460492f..5ebb584 100644 --- a/internal/sbi/producer/nsselection_for_registration.go +++ b/internal/sbi/processor/nsselection_network_slice_information.go @@ -2,12 +2,22 @@ * NSSF NS Selection * * NSSF Network Slice Selection Service + * + * API version: 1.0.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) */ -package producer +package processor import ( + "encoding/json" + "fmt" + "math/rand" "net/http" + "net/url" + "strings" + + "github.com/gin-gonic/gin" "github.com/free5gc/nssf/internal/logger" "github.com/free5gc/nssf/internal/plugin" @@ -16,6 +26,153 @@ import ( "github.com/free5gc/openapi/models" ) +// Parse NSSelectionGet query parameter +func parseQueryParameter(query url.Values) (plugin.NsselectionQueryParameter, error) { + var ( + param plugin.NsselectionQueryParameter + err error + ) + + if query.Get("nf-type") != "" { + param.NfType = new(models.NfType) + *param.NfType = models.NfType(query.Get("nf-type")) + } + + param.NfId = query.Get("nf-id") + + if query.Get("slice-info-request-for-registration") != "" { + param.SliceInfoRequestForRegistration = new(models.SliceInfoForRegistration) + err = json.NewDecoder(strings.NewReader( + query.Get("slice-info-request-for-registration"))).Decode(param.SliceInfoRequestForRegistration) + if err != nil { + return param, err + } + } + + if query.Get("slice-info-request-for-pdu-session") != "" { + param.SliceInfoRequestForPduSession = new(models.SliceInfoForPduSession) + err = json.NewDecoder(strings.NewReader( + query.Get("slice-info-request-for-pdu-session"))).Decode(param.SliceInfoRequestForPduSession) + if err != nil { + return param, err + } + } + + if query.Get("home-plmn-id") != "" { + param.HomePlmnId = new(models.PlmnId) + err = json.NewDecoder(strings.NewReader(query.Get("home-plmn-id"))).Decode(param.HomePlmnId) + if err != nil { + return param, err + } + } + + if query.Get("tai") != "" { + param.Tai = new(models.Tai) + err = json.NewDecoder(strings.NewReader(query.Get("tai"))).Decode(param.Tai) + if err != nil { + return param, err + } + } + + if query.Get("supported-features") != "" { + param.SupportedFeatures = query.Get("supported-features") + } + + return param, err +} + +// Check if the NF service consumer is authorized +// TODO: Check if the NF service consumer is legal with local configuration, or possibly after querying NRF through +// `nf-id` e.g. Whether the V-NSSF is authorized +func checkNfServiceConsumer(nfType models.NfType) error { + if nfType != models.NfType_AMF && nfType != models.NfType_NSSF { + return fmt.Errorf("`nf-type`:'%s' is not authorized to retrieve the slice selection information", string(nfType)) + } + + return nil +} + +func (p *Processor) NSSelectionSliceInformationGet(c *gin.Context, query url.Values) { + var ( + status int + response *models.AuthorizedNetworkSliceInfo + problemDetails *models.ProblemDetails + ) + + // TODO: Record request times of the NF service consumer and response with ProblemDetails of 429 Too Many Requests + // if the consumer has sent too many requests in a configured amount of time + // TODO: Check URI length and response with ProblemDetails of 414 URI Too Long if URI is too long + + // Parse query parameter + param, err := parseQueryParameter(query) + if err != nil { + // status = http.StatusBadRequest + problemDetails = &models.ProblemDetails{ + Title: util.MALFORMED_REQUEST, + Status: http.StatusBadRequest, + Detail: "[Query Parameter] " + err.Error(), + } + util.GinProblemJson(c, problemDetails) + return + } + + // Check permission of NF service consumer + err = checkNfServiceConsumer(*param.NfType) + if err != nil { + // status = http.StatusForbidden + problemDetails = &models.ProblemDetails{ + Title: util.UNAUTHORIZED_CONSUMER, + Status: http.StatusForbidden, + Detail: err.Error(), + } + util.GinProblemJson(c, problemDetails) + return + } + + if param.Tai == nil && param.HomePlmnId == nil { + problemDetails = &models.ProblemDetails{ + Title: util.MANDATORY_IE_MISSING, + Status: http.StatusBadRequest, + Cause: "MANDATORY_IE_MISSING", + Detail: "Either `tai` or `home-plmn-id` should be provided", + InvalidParams: []models.InvalidParam{ + { + Param: "tai", + }, + { + Param: "home-plmn-id", + }, + }, + } + + util.GinProblemJson(c, problemDetails) + return + } + + if param.SliceInfoRequestForRegistration != nil { + // Network slice information is requested during the Registration procedure + status, response, problemDetails = nsselectionForRegistration(param) + } else { + // Network slice information is requested during the PDU session establishment procedure + status, response, problemDetails = nsselectionForPduSession(param) + } + + if problemDetails != nil { + util.GinProblemJson(c, problemDetails) + return + } + + if response == nil { + util.GinProblemJson(c, &models.ProblemDetails{ + Title: util.INTERNAL_ERROR, + Status: http.StatusInternalServerError, + }) + return + } + + c.JSON(status, response) +} + // Set Allowed NSSAI with Subscribed S-NSSAI(s) which are marked as default S-NSSAI(s) func useDefaultSubscribedSnssai( param plugin.NsselectionQueryParameter, authorizedNetworkSliceInfo *models.AuthorizedNetworkSliceInfo, @@ -162,10 +319,11 @@ func setConfiguredNssai( // Network slice selection for registration // The function is executed when the IE, `slice-info-request-for-registration`, is provided in query parameters -func nsselectionForRegistration(param plugin.NsselectionQueryParameter, - authorizedNetworkSliceInfo *models.AuthorizedNetworkSliceInfo, - problemDetails *models.ProblemDetails, -) int { +func nsselectionForRegistration(param plugin.NsselectionQueryParameter) ( + int, *models.AuthorizedNetworkSliceInfo, *models.ProblemDetails, +) { + authorizedNetworkSliceInfo := &models.AuthorizedNetworkSliceInfo{} + var status int if param.HomePlmnId != nil { // Check whether UE's Home PLMN is supported when UE is a roamer @@ -175,7 +333,7 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, param.SliceInfoRequestForRegistration.RequestedNssai...) status = http.StatusOK - return status + return status, authorizedNetworkSliceInfo, nil } } @@ -187,7 +345,7 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, param.SliceInfoRequestForRegistration.RequestedNssai...) status = http.StatusOK - return status + return status, authorizedNetworkSliceInfo, nil } } @@ -198,22 +356,22 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, // for S-NSSAIs in both `sNssaiForMapping` and `subscribedSnssai` if present if param.HomePlmnId == nil { - problemDetail := "[Query Parameter] `home-plmn-id` should be provided" + + detail := "[Query Parameter] `home-plmn-id` should be provided" + " when requesting VPLMN specific mapped S-NSSAI values" - *problemDetails = models.ProblemDetails{ + problemDetails := &models.ProblemDetails{ Title: util.INVALID_REQUEST, Status: http.StatusBadRequest, - Detail: problemDetail, + Detail: detail, InvalidParams: []models.InvalidParam{ { Param: "home-plmn-id", - Reason: problemDetail, + Reason: detail, }, }, } status = http.StatusBadRequest - return status + return status, nil, problemDetails } mappingOfSnssai := util.GetMappingOfPlmnFromConfig(*param.HomePlmnId) @@ -286,12 +444,12 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, } status = http.StatusOK - return status + return status, authorizedNetworkSliceInfo, nil } else { logger.NsselLog.Warnf("No S-NSSAI mapping of UE's HPLMN %+v in NSSF configuration", *param.HomePlmnId) status = http.StatusOK - return status + return status, authorizedNetworkSliceInfo, nil } } @@ -306,7 +464,7 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, // Return ProblemDetails indicating S-NSSAI is not supported // TODO: Based on TS 23.501 V15.2.0, if the Requested NSSAI includes an S-NSSAI that is not valid in the // Serving PLMN, the NSSF may derive the Configured NSSAI for Serving PLMN - *problemDetails = models.ProblemDetails{ + problemDetails := &models.ProblemDetails{ Title: util.UNSUPPORTED_RESOURCE, Status: http.StatusForbidden, Detail: "S-NSSAI in Requested NSSAI is not supported in PLMN", @@ -314,7 +472,7 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, } status = http.StatusForbidden - return status + return status, nil, problemDetails } // Check if any Requested S-NSSAIs is present in Subscribed S-NSSAIs @@ -435,5 +593,127 @@ func nsselectionForRegistration(param plugin.NsselectionQueryParameter, } status = http.StatusOK - return status + return status, authorizedNetworkSliceInfo, nil +} + +func selectNsiInformation(nsiInformationList []models.NsiInformation) models.NsiInformation { + // TODO: Algorithm to select Network Slice Instance + // Take roaming indication into consideration + + // Randomly select a Network Slice Instance + idx := rand.Intn(len(nsiInformationList)) + return nsiInformationList[idx] +} + +// Network slice selection for PDU session +// The function is executed when the IE, `slice-info-for-pdu-session`, is provided in query parameters +func nsselectionForPduSession(param plugin.NsselectionQueryParameter) ( + int, *models.AuthorizedNetworkSliceInfo, *models.ProblemDetails, +) { + var status int + authorizedNetworkSliceInfo := &models.AuthorizedNetworkSliceInfo{} + + if param.HomePlmnId != nil { + // Check whether UE's Home PLMN is supported when UE is a roamer + if !util.CheckSupportedHplmn(*param.HomePlmnId) { + authorizedNetworkSliceInfo.RejectedNssaiInPlmn = append( + authorizedNetworkSliceInfo.RejectedNssaiInPlmn, + *param.SliceInfoRequestForPduSession.SNssai) + + status = http.StatusOK + return status, authorizedNetworkSliceInfo, nil + } + } + + if param.Tai != nil { + // Check whether UE's current TA is supported when UE provides TAI + if !util.CheckSupportedTa(*param.Tai) { + authorizedNetworkSliceInfo.RejectedNssaiInTa = append( + authorizedNetworkSliceInfo.RejectedNssaiInTa, + *param.SliceInfoRequestForPduSession.SNssai) + + status = http.StatusOK + return status, authorizedNetworkSliceInfo, nil + } + } + + if param.Tai != nil && + !util.CheckSupportedSnssaiInPlmn(*param.SliceInfoRequestForPduSession.SNssai, *param.Tai.PlmnId) { + // Return ProblemDetails indicating S-NSSAI is not supported + // TODO: Based on TS 23.501 V15.2.0, if the Requested NSSAI includes an S-NSSAI that is not valid in the + // Serving PLMN, the NSSF may derive the Configured NSSAI for Serving PLMN + + problemDetails := &models.ProblemDetails{ + Title: util.UNSUPPORTED_RESOURCE, + Status: http.StatusForbidden, + Detail: "S-NSSAI in Requested NSSAI is not supported in PLMN", + Cause: "SNSSAI_NOT_SUPPORTED", + } + + status = http.StatusForbidden + return status, nil, problemDetails + } + + if param.HomePlmnId != nil { + if param.SliceInfoRequestForPduSession.RoamingIndication == models.RoamingIndication_NON_ROAMING { + detail := "`home-plmn-id` is provided, which contradicts `roamingIndication`:'NON_ROAMING'" + problemDetails := &models.ProblemDetails{ + Title: util.INVALID_REQUEST, + Status: http.StatusBadRequest, + Detail: detail, + InvalidParams: []models.InvalidParam{ + { + Param: "home-plmn-id", + Reason: detail, + }, + }, + } + + status = http.StatusBadRequest + return status, nil, problemDetails + } + } else { + if param.SliceInfoRequestForPduSession.RoamingIndication != models.RoamingIndication_NON_ROAMING { + detail := fmt.Sprintf("`home-plmn-id` is not provided, which contradicts `roamingIndication`:'%s'", + string(param.SliceInfoRequestForPduSession.RoamingIndication)) + problemDetails := &models.ProblemDetails{ + Title: util.INVALID_REQUEST, + Status: http.StatusBadRequest, + Detail: detail, + InvalidParams: []models.InvalidParam{ + { + Param: "home-plmn-id", + Reason: detail, + }, + }, + } + + status = http.StatusBadRequest + return status, nil, problemDetails + } + } + + if param.Tai != nil && !util.CheckSupportedSnssaiInTa(*param.SliceInfoRequestForPduSession.SNssai, *param.Tai) { + // Requested S-NSSAI does not supported in UE's current TA + // Add it to Rejected NSSAI in TA + authorizedNetworkSliceInfo.RejectedNssaiInTa = append( + authorizedNetworkSliceInfo.RejectedNssaiInTa, + *param.SliceInfoRequestForPduSession.SNssai) + status = http.StatusOK + return status, authorizedNetworkSliceInfo, nil + } + + nsiInformationList := util.GetNsiInformationListFromConfig(*param.SliceInfoRequestForPduSession.SNssai) + + if len(nsiInformationList) == 0 { + *authorizedNetworkSliceInfo = models.AuthorizedNetworkSliceInfo{} + } else { + nsiInformation := selectNsiInformation(nsiInformationList) + authorizedNetworkSliceInfo.NsiInformation = new(models.NsiInformation) + *authorizedNetworkSliceInfo.NsiInformation = nsiInformation + } + + logger.NsselLog.Infof("authorizedNetworkSliceInfo: %+v", authorizedNetworkSliceInfo) + + return http.StatusOK, authorizedNetworkSliceInfo, nil } diff --git a/internal/sbi/processor/processor.go b/internal/sbi/processor/processor.go new file mode 100644 index 0000000..00f435c --- /dev/null +++ b/internal/sbi/processor/processor.go @@ -0,0 +1,15 @@ +package processor + +import ( + "github.com/free5gc/nssf/pkg/app" +) + +type Processor struct { + app.NssfApp +} + +func NewProcessor(nssf app.NssfApp) *Processor { + return &Processor{ + NssfApp: nssf, + } +} diff --git a/internal/sbi/producer/network_slice_information_document.go b/internal/sbi/producer/network_slice_information_document.go deleted file mode 100644 index 8421226..0000000 --- a/internal/sbi/producer/network_slice_information_document.go +++ /dev/null @@ -1,162 +0,0 @@ -/* - * NSSF NS Selection - * - * NSSF Network Slice Selection Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package producer - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/plugin" - "github.com/free5gc/nssf/internal/util" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -// Parse NSSelectionGet query parameter -func parseQueryParameter(query url.Values) (plugin.NsselectionQueryParameter, error) { - var ( - param plugin.NsselectionQueryParameter - err error - ) - - if query.Get("nf-type") != "" { - param.NfType = new(models.NfType) - *param.NfType = models.NfType(query.Get("nf-type")) - } - - param.NfId = query.Get("nf-id") - - if query.Get("slice-info-request-for-registration") != "" { - param.SliceInfoRequestForRegistration = new(models.SliceInfoForRegistration) - err = json.NewDecoder(strings.NewReader( - query.Get("slice-info-request-for-registration"))).Decode(param.SliceInfoRequestForRegistration) - if err != nil { - return param, err - } - } - - if query.Get("slice-info-request-for-pdu-session") != "" { - param.SliceInfoRequestForPduSession = new(models.SliceInfoForPduSession) - err = json.NewDecoder(strings.NewReader( - query.Get("slice-info-request-for-pdu-session"))).Decode(param.SliceInfoRequestForPduSession) - if err != nil { - return param, err - } - } - - if query.Get("home-plmn-id") != "" { - param.HomePlmnId = new(models.PlmnId) - err = json.NewDecoder(strings.NewReader(query.Get("home-plmn-id"))).Decode(param.HomePlmnId) - if err != nil { - return param, err - } - } - - if query.Get("tai") != "" { - param.Tai = new(models.Tai) - err = json.NewDecoder(strings.NewReader(query.Get("tai"))).Decode(param.Tai) - if err != nil { - return param, err - } - } - - if query.Get("supported-features") != "" { - param.SupportedFeatures = query.Get("supported-features") - } - - return param, err -} - -// Check if the NF service consumer is authorized -// TODO: Check if the NF service consumer is legal with local configuration, or possibly after querying NRF through -// `nf-id` e.g. Whether the V-NSSF is authorized -func checkNfServiceConsumer(nfType models.NfType) error { - if nfType != models.NfType_AMF && nfType != models.NfType_NSSF { - return fmt.Errorf("`nf-type`:'%s' is not authorized to retrieve the slice selection information", string(nfType)) - } - - return nil -} - -// NSSelectionGet - Retrieve the Network Slice Selection Information -func HandleNSSelectionGet(request *httpwrapper.Request) *httpwrapper.Response { - logger.NsselLog.Infof("Handle NSSelectionGet") - - query := request.Query - - response, problemDetails := NSSelectionGetProcedure(query) - - if response != nil { - return httpwrapper.NewResponse(http.StatusOK, nil, response) - } else if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - problemDetails = &models.ProblemDetails{ - Status: http.StatusForbidden, - Cause: "UNSPECIFIED", - } - return httpwrapper.NewResponse(http.StatusForbidden, nil, problemDetails) -} - -func NSSelectionGetProcedure(query url.Values) (*models.AuthorizedNetworkSliceInfo, *models.ProblemDetails) { - var ( - status int - response *models.AuthorizedNetworkSliceInfo - problemDetails *models.ProblemDetails - ) - response = &models.AuthorizedNetworkSliceInfo{} - problemDetails = &models.ProblemDetails{} - - // TODO: Record request times of the NF service consumer and response with ProblemDetails of 429 Too Many Requests - // if the consumer has sent too many requests in a configured amount of time - // TODO: Check URI length and response with ProblemDetails of 414 URI Too Long if URI is too long - - // Parse query parameter - param, err := parseQueryParameter(query) - if err != nil { - // status = http.StatusBadRequest - problemDetails = &models.ProblemDetails{ - Title: util.MALFORMED_REQUEST, - Status: http.StatusBadRequest, - Detail: "[Query Parameter] " + err.Error(), - } - return nil, problemDetails - } - - // Check permission of NF service consumer - err = checkNfServiceConsumer(*param.NfType) - if err != nil { - // status = http.StatusForbidden - problemDetails = &models.ProblemDetails{ - Title: util.UNAUTHORIZED_CONSUMER, - Status: http.StatusForbidden, - Detail: err.Error(), - } - return nil, problemDetails - } - - if param.SliceInfoRequestForRegistration != nil { - // Network slice information is requested during the Registration procedure - status = nsselectionForRegistration(param, response, problemDetails) - } else { - // Network slice information is requested during the PDU session establishment procedure - status = nsselectionForPduSession(param, response, problemDetails) - } - - if status == http.StatusOK { - return response, problemDetails - } else { - return response, problemDetails - } -} diff --git a/internal/sbi/producer/nf_instance_id_document.go b/internal/sbi/producer/nf_instance_id_document.go deleted file mode 100644 index f79eca8..0000000 --- a/internal/sbi/producer/nf_instance_id_document.go +++ /dev/null @@ -1,83 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package producer - -import ( - "net/http" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/nssf/internal/plugin" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -// HandleNSSAIAvailabilityDelete - Deletes an already existing S-NSSAIs per TA -// provided by the NF service consumer (e.g AMF) -func HandleNSSAIAvailabilityDelete(request *httpwrapper.Request) *httpwrapper.Response { - logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityDelete") - - nfID := request.Params["nfId"] - - problemDetails := NSSAIAvailabilityDeleteProcedure(nfID) - - if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - return httpwrapper.NewResponse(http.StatusNoContent, nil, nil) -} - -// HandleNSSAIAvailabilityPatch - Updates an already existing S-NSSAIs per TA -// provided by the NF service consumer (e.g AMF) -func HandleNSSAIAvailabilityPatch(request *httpwrapper.Request) *httpwrapper.Response { - logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityPatch") - - nssaiAvailabilityUpdateInfo := request.Body.(plugin.PatchDocument) - nfID := request.Params["nfId"] - - // TODO: Request NfProfile of NfId from NRF - // Check if NfId is valid AMF and obtain AMF Set ID - // If NfId is invalid, return ProblemDetails with code 404 Not Found - // If NF consumer is not authorized to update NSSAI availability, return ProblemDetails with code 403 Forbidden - - response, problemDetails := NSSAIAvailabilityPatchProcedure(nssaiAvailabilityUpdateInfo, nfID) - - if response != nil { - return httpwrapper.NewResponse(http.StatusOK, nil, response) - } else if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - problemDetails = &models.ProblemDetails{ - Status: http.StatusForbidden, - Cause: "UNSPECIFIED", - } - return httpwrapper.NewResponse(http.StatusForbidden, nil, problemDetails) -} - -// HandleNSSAIAvailabilityPut - Updates/replaces the NSSF -// with the S-NSSAIs the NF service consumer (e.g AMF) supports per TA -func HandleNSSAIAvailabilityPut(request *httpwrapper.Request) *httpwrapper.Response { - logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityPut") - - nssaiAvailabilityInfo := request.Body.(models.NssaiAvailabilityInfo) - nfID := request.Params["nfId"] - - response, problemDetails := NSSAIAvailabilityPutProcedure(nssaiAvailabilityInfo, nfID) - - if response != nil { - return httpwrapper.NewResponse(http.StatusOK, nil, response) - } else if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - problemDetails = &models.ProblemDetails{ - Status: http.StatusForbidden, - Cause: "UNSPECIFIED", - } - return httpwrapper.NewResponse(http.StatusForbidden, nil, problemDetails) -} diff --git a/internal/sbi/producer/nsselection_for_pdu_session.go b/internal/sbi/producer/nsselection_for_pdu_session.go deleted file mode 100644 index b4dddff..0000000 --- a/internal/sbi/producer/nsselection_for_pdu_session.go +++ /dev/null @@ -1,135 +0,0 @@ -/* - * NSSF NS Selection - * - * NSSF Network Slice Selection Service - */ - -package producer - -import ( - "fmt" - "math/rand" - "net/http" - - "github.com/free5gc/nssf/internal/plugin" - "github.com/free5gc/nssf/internal/util" - "github.com/free5gc/openapi/models" -) - -func selectNsiInformation(nsiInformationList []models.NsiInformation) models.NsiInformation { - // TODO: Algorithm to select Network Slice Instance - // Take roaming indication into consideration - - // Randomly select a Network Slice Instance - idx := rand.Intn(len(nsiInformationList)) - return nsiInformationList[idx] -} - -// Network slice selection for PDU session -// The function is executed when the IE, `slice-info-for-pdu-session`, is provided in query parameters -func nsselectionForPduSession(param plugin.NsselectionQueryParameter, - authorizedNetworkSliceInfo *models.AuthorizedNetworkSliceInfo, - problemDetails *models.ProblemDetails, -) int { - var status int - if param.HomePlmnId != nil { - // Check whether UE's Home PLMN is supported when UE is a roamer - if !util.CheckSupportedHplmn(*param.HomePlmnId) { - authorizedNetworkSliceInfo.RejectedNssaiInPlmn = append( - authorizedNetworkSliceInfo.RejectedNssaiInPlmn, - *param.SliceInfoRequestForPduSession.SNssai) - - status = http.StatusOK - return status - } - } - - if param.Tai != nil { - // Check whether UE's current TA is supported when UE provides TAI - if !util.CheckSupportedTa(*param.Tai) { - authorizedNetworkSliceInfo.RejectedNssaiInTa = append( - authorizedNetworkSliceInfo.RejectedNssaiInTa, - *param.SliceInfoRequestForPduSession.SNssai) - - status = http.StatusOK - return status - } - } - - if param.Tai != nil && - !util.CheckSupportedSnssaiInPlmn(*param.SliceInfoRequestForPduSession.SNssai, *param.Tai.PlmnId) { - // Return ProblemDetails indicating S-NSSAI is not supported - // TODO: Based on TS 23.501 V15.2.0, if the Requested NSSAI includes an S-NSSAI that is not valid in the - // Serving PLMN, the NSSF may derive the Configured NSSAI for Serving PLMN - *problemDetails = models.ProblemDetails{ - Title: util.UNSUPPORTED_RESOURCE, - Status: http.StatusForbidden, - Detail: "S-NSSAI in Requested NSSAI is not supported in PLMN", - Cause: "SNSSAI_NOT_SUPPORTED", - } - - status = http.StatusForbidden - return status - } - - if param.HomePlmnId != nil { - if param.SliceInfoRequestForPduSession.RoamingIndication == models.RoamingIndication_NON_ROAMING { - problemDetail := "`home-plmn-id` is provided, which contradicts `roamingIndication`:'NON_ROAMING'" - *problemDetails = models.ProblemDetails{ - Title: util.INVALID_REQUEST, - Status: http.StatusBadRequest, - Detail: problemDetail, - InvalidParams: []models.InvalidParam{ - { - Param: "home-plmn-id", - Reason: problemDetail, - }, - }, - } - - status = http.StatusBadRequest - return status - } - } else { - if param.SliceInfoRequestForPduSession.RoamingIndication != models.RoamingIndication_NON_ROAMING { - problemDetail := fmt.Sprintf("`home-plmn-id` is not provided, which contradicts `roamingIndication`:'%s'", - string(param.SliceInfoRequestForPduSession.RoamingIndication)) - *problemDetails = models.ProblemDetails{ - Title: util.INVALID_REQUEST, - Status: http.StatusBadRequest, - Detail: problemDetail, - InvalidParams: []models.InvalidParam{ - { - Param: "home-plmn-id", - Reason: problemDetail, - }, - }, - } - - status = http.StatusBadRequest - return status - } - } - - if param.Tai != nil && !util.CheckSupportedSnssaiInTa(*param.SliceInfoRequestForPduSession.SNssai, *param.Tai) { - // Requested S-NSSAI does not supported in UE's current TA - // Add it to Rejected NSSAI in TA - authorizedNetworkSliceInfo.RejectedNssaiInTa = append( - authorizedNetworkSliceInfo.RejectedNssaiInTa, - *param.SliceInfoRequestForPduSession.SNssai) - status = http.StatusOK - return status - } - - nsiInformationList := util.GetNsiInformationListFromConfig(*param.SliceInfoRequestForPduSession.SNssai) - - if len(nsiInformationList) == 0 { - *authorizedNetworkSliceInfo = models.AuthorizedNetworkSliceInfo{} - } else { - nsiInformation := selectNsiInformation(nsiInformationList) - authorizedNetworkSliceInfo.NsiInformation = new(models.NsiInformation) - *authorizedNetworkSliceInfo.NsiInformation = nsiInformation - } - - return http.StatusOK -} diff --git a/internal/sbi/producer/subscription_id_document.go b/internal/sbi/producer/subscription_id_document.go deleted file mode 100644 index 54d98a6..0000000 --- a/internal/sbi/producer/subscription_id_document.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package producer - -import ( - "net/http" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -// HandleNSSAIAvailabilityUnsubscribe - Deletes an already existing NSSAI availability notification subscription -func HandleNSSAIAvailabilityUnsubscribe(request *httpwrapper.Request) *httpwrapper.Response { - logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityUnsubscribe") - - subscriptionID := request.Params["subscriptionId"] - - problemDetails := NSSAIAvailabilityUnsubscribeProcedure(subscriptionID) - - if problemDetails == nil { - return httpwrapper.NewResponse(http.StatusNoContent, nil, nil) - } else if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - problemDetails = &models.ProblemDetails{ - Status: http.StatusForbidden, - Cause: "UNSPECIFIED", - } - return httpwrapper.NewResponse(http.StatusForbidden, nil, problemDetails) -} diff --git a/internal/sbi/producer/subscriptions_collection.go b/internal/sbi/producer/subscriptions_collection.go deleted file mode 100644 index 048ae8d..0000000 --- a/internal/sbi/producer/subscriptions_collection.go +++ /dev/null @@ -1,41 +0,0 @@ -/* - * NSSF NSSAI Availability - * - * NSSF NSSAI Availability Service - * - * API version: 1.0.0 - * Generated by: OpenAPI Generator (https://openapi-generator.tech) - */ - -package producer - -import ( - "net/http" - - "github.com/free5gc/nssf/internal/logger" - "github.com/free5gc/openapi/models" - "github.com/free5gc/util/httpwrapper" -) - -// HandleNSSAIAvailabilityPost - Creates subscriptions for notification about updates to NSSAI availability information -func HandleNSSAIAvailabilityPost(request *httpwrapper.Request) *httpwrapper.Response { - logger.NssaiavailLog.Infof("Handle NSSAIAvailabilityPost") - - createData := request.Body.(models.NssfEventSubscriptionCreateData) - - // TODO: If NF consumer is not authorized to update NSSAI availability, return ProblemDetails with code 403 Forbidden - - response, problemDetails := NSSAIAvailabilityPostProcedure(createData) - - if response != nil { - // TODO: Based on TS 29.531 5.3.2.3.1, add location header - return httpwrapper.NewResponse(http.StatusCreated, nil, response) - } else if problemDetails != nil { - return httpwrapper.NewResponse(int(problemDetails.Status), nil, problemDetails) - } - problemDetails = &models.ProblemDetails{ - Status: http.StatusForbidden, - Cause: "UNSPECIFIED", - } - return httpwrapper.NewResponse(http.StatusForbidden, nil, problemDetails) -} diff --git a/internal/sbi/router.go b/internal/sbi/router.go new file mode 100644 index 0000000..0e6288c --- /dev/null +++ b/internal/sbi/router.go @@ -0,0 +1,38 @@ +package sbi + +import ( + "github.com/gin-gonic/gin" +) + +// Route is the information for every URI. +type Route struct { + // Name is the name of this Route. + Name string + // Method is the string for the HTTP method. ex) GET, POST etc.. + Method string + // Pattern is the pattern of the URI. + Pattern string + // HandlerFunc is the handler function of this route. + HandlerFunc gin.HandlerFunc +} + +type RouteGroup interface { + AddService(engine *gin.Engine) *gin.RouterGroup +} + +func AddService(group *gin.RouterGroup, routes []Route) { + for _, route := range routes { + switch route.Method { + case "GET": + group.GET(route.Pattern, route.HandlerFunc) + case "POST": + group.POST(route.Pattern, route.HandlerFunc) + case "PUT": + group.PUT(route.Pattern, route.HandlerFunc) + case "DELETE": + group.DELETE(route.Pattern, route.HandlerFunc) + case "PATCH": + group.PATCH(route.Pattern, route.HandlerFunc) + } + } +} diff --git a/internal/sbi/server.go b/internal/sbi/server.go new file mode 100644 index 0000000..88c67d3 --- /dev/null +++ b/internal/sbi/server.go @@ -0,0 +1,162 @@ +package sbi + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + + "github.com/free5gc/nssf/internal/logger" + "github.com/free5gc/nssf/internal/sbi/processor" + "github.com/free5gc/nssf/internal/util" + "github.com/free5gc/nssf/pkg/app" + "github.com/free5gc/nssf/pkg/factory" + "github.com/free5gc/openapi/models" + "github.com/free5gc/util/httpwrapper" + logger_util "github.com/free5gc/util/logger" +) + +type nssfApp interface { + app.NssfApp + + Processor() *processor.Processor +} + +type Server struct { + nssfApp + + httpServer *http.Server + router *gin.Engine + processor *processor.Processor +} + +func NewServer(nssf nssfApp, tlsKeyLogPath string) *Server { + s := &Server{ + nssfApp: nssf, + processor: nssf.Processor(), + } + + s.router = newRouter(s) + + server, err := bindRouter(nssf, s.router, tlsKeyLogPath) + s.httpServer = server + + if err != nil { + logger.SBILog.Errorf("bind Router Error: %+v", err) + panic("Server initialization failed") + } + + return s +} + +func (s *Server) Processor() *processor.Processor { + return s.processor +} + +func (s *Server) Run(wg *sync.WaitGroup) { + logger.SBILog.Info("Starting server...") + + wg.Add(1) + go func() { + defer wg.Done() + + err := s.serve() + if err != http.ErrServerClosed { + logger.SBILog.Panicf("HTTP server setup failed: %+v", err) + } + }() +} + +func (s *Server) Shutdown() { + s.shutdownHttpServer() +} + +func (s *Server) shutdownHttpServer() { + const shutdownTimeout time.Duration = 2 * time.Second + + if s.httpServer == nil { + return + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + + err := s.httpServer.Shutdown(shutdownCtx) + if err != nil { + logger.SBILog.Errorf("HTTP server shutdown failed: %+v", err) + } +} + +func bindRouter(nssf app.NssfApp, router *gin.Engine, tlsKeyLogPath string) (*http.Server, error) { + sbiConfig := nssf.Config().Configuration.Sbi + bindAddr := fmt.Sprintf("%s:%d", sbiConfig.BindingIPv4, sbiConfig.Port) + + return httpwrapper.NewHttp2Server(bindAddr, tlsKeyLogPath, router) +} + +func newRouter(s *Server) *gin.Engine { + router := logger_util.NewGinWithLogrus(logger.GinLog) + + for _, serviceName := range s.Config().Configuration.ServiceNameList { + switch serviceName { + case models.ServiceName_NNSSF_NSSAIAVAILABILITY: + nssaiAvailabilityGroup := router.Group(factory.NssfNssaiavailResUriPrefix) + nssaiAvailabilityGroup.Use(func(c *gin.Context) { + // oauth middleware + util.NewRouterAuthorizationCheck(serviceName).Check(c, s.Context()) + }) + nssaiAvailabilityRoutes := s.getNssaiAvailabilityRoutes() + AddService(nssaiAvailabilityGroup, nssaiAvailabilityRoutes) + + case models.ServiceName_NNSSF_NSSELECTION: + nsSelectionGroup := router.Group(factory.NssfNsselectResUriPrefix) + nsSelectionGroup.Use(func(c *gin.Context) { + // oauth middleware + util.NewRouterAuthorizationCheck(serviceName).Check(c, s.Context()) + }) + nsSelectionRoutes := s.getNsSelectionRoutes() + AddService(nsSelectionGroup, nsSelectionRoutes) + + default: + logger.SBILog.Warnf("Unsupported service name: %s", serviceName) + } + } + + return router +} + +func (s *Server) unsecureServe() error { + return s.httpServer.ListenAndServe() +} + +func (s *Server) secureServe() error { + sbiConfig := s.Config().Configuration.Sbi + + pemPath := sbiConfig.Tls.Pem + if pemPath == "" { + pemPath = factory.NssfDefaultCertPemPath + } + + keyPath := sbiConfig.Tls.Key + if keyPath == "" { + keyPath = factory.NssfDefaultPrivateKeyPath + } + + return s.httpServer.ListenAndServeTLS(pemPath, keyPath) +} + +func (s *Server) serve() error { + sbiConfig := s.Config().Configuration.Sbi + + switch sbiConfig.Scheme { + case "http": + return s.unsecureServe() + case "https": + return s.secureServe() + default: + return fmt.Errorf("invalid SBI scheme: %s", sbiConfig.Scheme) + } +} diff --git a/internal/util/problem_json.go b/internal/util/problem_json.go new file mode 100644 index 0000000..6efdc0c --- /dev/null +++ b/internal/util/problem_json.go @@ -0,0 +1,12 @@ +package util + +import ( + "github.com/gin-gonic/gin" + + "github.com/free5gc/openapi/models" +) + +func GinProblemJson(c *gin.Context, problemDetails *models.ProblemDetails) { + c.JSON(int(problemDetails.Status), problemDetails) + c.Writer.Header().Set("Content-Type", "application/problem+json") +} diff --git a/internal/util/router_auth_check_test.go b/internal/util/router_auth_check_test.go index 0d6ec07..e0ebc1e 100644 --- a/internal/util/router_auth_check_test.go +++ b/internal/util/router_auth_check_test.go @@ -1,4 +1,4 @@ -package util +package util_test import ( "net/http" @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pkg/errors" + "github.com/free5gc/nssf/internal/util" "github.com/free5gc/openapi/models" ) @@ -85,7 +86,7 @@ func TestRouterAuthorizationCheck_Check(t *testing.T) { var testService models.ServiceName = "testService" - rac := NewRouterAuthorizationCheck(testService) + rac := util.NewRouterAuthorizationCheck(testService) rac.Check(c, newMockNSSFContext()) if w.Code != tt.want.statusCode { t.Errorf("StatusCode should be %d, but got %d", tt.want.statusCode, w.Code) diff --git a/internal/util/util.go b/internal/util/util.go index 5ad2e7b..0f76e8f 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -17,7 +17,9 @@ import ( // Title in Problem Details for NSSF HTTP APIs const ( + INTERNAL_ERROR = "Internal server error" INVALID_REQUEST = "Invalid request message framing" + MANDATORY_IE_MISSING = "Mandatory IEs are missing" MALFORMED_REQUEST = "Malformed request syntax" UNAUTHORIZED_CONSUMER = "Unauthorized NF service consumer" UNSUPPORTED_RESOURCE = "Unsupported request resources" @@ -393,7 +395,8 @@ func AddAllowedSnssai(allowedSnssai models.AllowedSnssai, accessType models.Acce for i := range authorizedNetworkSliceInfo.AllowedNssaiList { if authorizedNetworkSliceInfo.AllowedNssaiList[i].AccessType == accessType { hitAllowedNssai = true - if len(authorizedNetworkSliceInfo.AllowedNssaiList[i].AllowedSnssaiList) == 8 { + const MAX_ALLOWED_SNSSAI = 8 + if len(authorizedNetworkSliceInfo.AllowedNssaiList[i].AllowedSnssaiList) == MAX_ALLOWED_SNSSAI { logger.UtilLog.Infof("Unable to add a new Allowed S-NSSAI since already eight S-NSSAIs in Allowed NSSAI") } else { authorizedNetworkSliceInfo.AllowedNssaiList[i].AllowedSnssaiList = append( diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..6e47aba --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,18 @@ +package app + +import ( + nssf_context "github.com/free5gc/nssf/internal/context" + "github.com/free5gc/nssf/pkg/factory" +) + +type NssfApp interface { + SetLogEnable(enable bool) + SetLogLevel(level string) + SetReportCaller(reportCaller bool) + + Start() + Terminate() + + Context() *nssf_context.NSSFContext + Config() *factory.Config +} diff --git a/pkg/app/mock.go b/pkg/app/mock.go new file mode 100644 index 0000000..e918625 --- /dev/null +++ b/pkg/app/mock.go @@ -0,0 +1,129 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: pkg/app/app.go +// +// Generated by this command: +// +// mockgen -source=pkg/app/app.go -package=app +// + +// Package app is a generated GoMock package. +package app + +import ( + reflect "reflect" + + context "github.com/free5gc/nssf/internal/context" + factory "github.com/free5gc/nssf/pkg/factory" + gomock "go.uber.org/mock/gomock" +) + +// MockNssfApp is a mock of NssfApp interface. +type MockNssfApp struct { + ctrl *gomock.Controller + recorder *MockNssfAppMockRecorder +} + +// MockNssfAppMockRecorder is the mock recorder for MockNssfApp. +type MockNssfAppMockRecorder struct { + mock *MockNssfApp +} + +// NewMockNssfApp creates a new mock instance. +func NewMockNssfApp(ctrl *gomock.Controller) *MockNssfApp { + mock := &MockNssfApp{ctrl: ctrl} + mock.recorder = &MockNssfAppMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNssfApp) EXPECT() *MockNssfAppMockRecorder { + return m.recorder +} + +// Config mocks base method. +func (m *MockNssfApp) Config() *factory.Config { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Config") + ret0, _ := ret[0].(*factory.Config) + return ret0 +} + +// Config indicates an expected call of Config. +func (mr *MockNssfAppMockRecorder) Config() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockNssfApp)(nil).Config)) +} + +// Context mocks base method. +func (m *MockNssfApp) Context() *context.NSSFContext { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Context") + ret0, _ := ret[0].(*context.NSSFContext) + return ret0 +} + +// Context indicates an expected call of Context. +func (mr *MockNssfAppMockRecorder) Context() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockNssfApp)(nil).Context)) +} + +// SetLogEnable mocks base method. +func (m *MockNssfApp) SetLogEnable(enable bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLogEnable", enable) +} + +// SetLogEnable indicates an expected call of SetLogEnable. +func (mr *MockNssfAppMockRecorder) SetLogEnable(enable any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogEnable", reflect.TypeOf((*MockNssfApp)(nil).SetLogEnable), enable) +} + +// SetLogLevel mocks base method. +func (m *MockNssfApp) SetLogLevel(level string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLogLevel", level) +} + +// SetLogLevel indicates an expected call of SetLogLevel. +func (mr *MockNssfAppMockRecorder) SetLogLevel(level any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogLevel", reflect.TypeOf((*MockNssfApp)(nil).SetLogLevel), level) +} + +// SetReportCaller mocks base method. +func (m *MockNssfApp) SetReportCaller(reportCaller bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetReportCaller", reportCaller) +} + +// SetReportCaller indicates an expected call of SetReportCaller. +func (mr *MockNssfAppMockRecorder) SetReportCaller(reportCaller any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReportCaller", reflect.TypeOf((*MockNssfApp)(nil).SetReportCaller), reportCaller) +} + +// Start mocks base method. +func (m *MockNssfApp) Start() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Start") +} + +// Start indicates an expected call of Start. +func (mr *MockNssfAppMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockNssfApp)(nil).Start)) +} + +// Terminate mocks base method. +func (m *MockNssfApp) Terminate() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Terminate") +} + +// Terminate indicates an expected call of Terminate. +func (mr *MockNssfAppMockRecorder) Terminate() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Terminate", reflect.TypeOf((*MockNssfApp)(nil).Terminate)) +} diff --git a/pkg/service/init.go b/pkg/service/init.go index 84aab10..67313df 100644 --- a/pkg/service/init.go +++ b/pkg/service/init.go @@ -5,41 +5,80 @@ package service import ( + "context" "fmt" "io" "os" - "os/signal" "runtime/debug" - "syscall" + "sync" "github.com/sirupsen/logrus" nssf_context "github.com/free5gc/nssf/internal/context" "github.com/free5gc/nssf/internal/logger" + "github.com/free5gc/nssf/internal/sbi" "github.com/free5gc/nssf/internal/sbi/consumer" - "github.com/free5gc/nssf/internal/sbi/nssaiavailability" - "github.com/free5gc/nssf/internal/sbi/nsselection" + "github.com/free5gc/nssf/internal/sbi/processor" + "github.com/free5gc/nssf/pkg/app" "github.com/free5gc/nssf/pkg/factory" - "github.com/free5gc/util/httpwrapper" - logger_util "github.com/free5gc/util/logger" ) type NssfApp struct { cfg *factory.Config nssfCtx *nssf_context.NSSFContext + + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + sbiServer *sbi.Server + processor *processor.Processor + consumer *consumer.Consumer } -func NewApp(cfg *factory.Config) (*NssfApp, error) { - nssf := &NssfApp{cfg: cfg} +var _ app.NssfApp = &NssfApp{} + +func NewApp(ctx context.Context, cfg *factory.Config, tlsKeyLogPath string) (*NssfApp, error) { + nssf_context.InitNssfContext() + + nssf := &NssfApp{ + cfg: cfg, + wg: sync.WaitGroup{}, + nssfCtx: nssf_context.GetSelf(), + } nssf.SetLogEnable(cfg.GetLogEnable()) nssf.SetLogLevel(cfg.GetLogLevel()) nssf.SetReportCaller(cfg.GetLogReportCaller()) - nssf_context.Init() - nssf.nssfCtx = nssf_context.GetSelf() + nssf.ctx, nssf.cancel = context.WithCancel(ctx) + + processor := processor.NewProcessor(nssf) + nssf.processor = processor + + consumer := consumer.NewConsumer(nssf) + nssf.consumer = consumer + + sbiServer := sbi.NewServer(nssf, tlsKeyLogPath) + nssf.sbiServer = sbiServer + return nssf, nil } +func (a *NssfApp) Config() *factory.Config { + return a.cfg +} + +func (a *NssfApp) Context() *nssf_context.NSSFContext { + return a.nssfCtx +} + +func (a *NssfApp) Processor() *processor.Processor { + return a.processor +} + +func (a *NssfApp) Consumer() *consumer.Consumer { + return a.consumer +} + func (a *NssfApp) SetLogEnable(enable bool) { logger.MainLog.Infof("Log enable is set to [%v]", enable) if enable && logger.Log.Out == os.Stderr { @@ -82,85 +121,67 @@ func (a *NssfApp) SetReportCaller(reportCaller bool) { logger.Log.SetReportCaller(reportCaller) } -func (a *NssfApp) Start(tlsKeyLogPath string) { - logger.InitLog.Infoln("Server started") - - router := logger_util.NewGinWithLogrus(logger.GinLog) - - nssaiavailability.AddService(router) - nsselection.AddService(router) +func (a *NssfApp) registerToNrf(ctx context.Context) error { + nssfContext := a.nssfCtx - pemPath := factory.NssfDefaultCertPemPath - keyPath := factory.NssfDefaultPrivateKeyPath - sbi := factory.NssfConfig.Configuration.Sbi - if sbi.Tls != nil { - pemPath = sbi.Tls.Pem - keyPath = sbi.Tls.Key + var err error + _, nssfContext.NfId, err = a.consumer.SendRegisterNFInstance(ctx, nssfContext) + if err != nil { + return fmt.Errorf("failed to register NSSF to NRF: %s", err.Error()) } - self := a.nssfCtx - nssf_context.InitNssfContext() - addr := fmt.Sprintf("%s:%d", self.BindingIPv4, self.SBIPort) + return nil +} - // Register to NRF - profile, err := consumer.BuildNFProfile(self) - if err != nil { - logger.InitLog.Error("Failed to build NSSF profile") +func (a *NssfApp) deregisterFromNrf() { + problemDetails, err := a.consumer.SendDeregisterNFInstance(a.nssfCtx.NfId) + if problemDetails != nil { + logger.InitLog.Errorf("Deregister NF instance Failed Problem[%+v]", problemDetails) + } else if err != nil { + logger.InitLog.Errorf("Deregister NF instance Error[%+v]", err) + } else { + logger.InitLog.Infof("Deregister from NRF successfully") } - _, self.NfId, err = consumer.SendRegisterNFInstance(self.NrfUri, self.NfId, profile) +} + +func (a *NssfApp) Start() { + err := a.registerToNrf(a.ctx) if err != nil { - logger.InitLog.Errorf("Failed to register NSSF to NRF: %s", err.Error()) + logger.MainLog.Errorf("register to NRF failed: %+v", err) + } else { + logger.MainLog.Infoln("register to NRF successfully") } - signalChannel := make(chan os.Signal, 1) - signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) - go func() { - defer func() { - if p := recover(); p != nil { - // Print stack for panic to log. Fatalf() will let program exit. - logger.InitLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) - } - }() - - <-signalChannel - a.Terminate() - os.Exit(0) + // Graceful deregister when panic + defer func() { + if p := recover(); p != nil { + a.deregisterFromNrf() + logger.InitLog.Fatalf("panic: %v\n%s", p, string(debug.Stack())) + } }() - server, err := httpwrapper.NewHttp2Server(addr, tlsKeyLogPath, router) + a.sbiServer.Run(&a.wg) - if server == nil { - logger.InitLog.Errorf("Initialize HTTP server failed: %+v", err) - return - } - - if err != nil { - logger.InitLog.Warnf("Initialize HTTP server: +%v", err) - } + go a.listenShutdown(a.ctx) + a.Wait() +} - serverScheme := factory.NssfConfig.Configuration.Sbi.Scheme - if serverScheme == "http" { - err = server.ListenAndServe() - } else if serverScheme == "https" { - err = server.ListenAndServeTLS(pemPath, keyPath) - } +func (a *NssfApp) listenShutdown(ctx context.Context) { + <-ctx.Done() + a.terminateProcedure() +} - if err != nil { - logger.InitLog.Fatalf("HTTP server setup failed: %+v", err) - } +func (a *NssfApp) Terminate() { + a.cancel() } -func (nssf *NssfApp) Terminate() { - logger.InitLog.Infof("Terminating NSSF...") - // deregister with NRF - problemDetails, err := consumer.SendDeregisterNFInstance() - if problemDetails != nil { - logger.InitLog.Errorf("Deregister NF instance Failed Problem[%+v]", problemDetails) - } else if err != nil { - logger.InitLog.Errorf("Deregister NF instance Error[%+v]", err) - } else { - logger.InitLog.Infof("Deregister from NRF successfully") - } +func (a *NssfApp) terminateProcedure() { + logger.MainLog.Infof("Terminating NSSF...") + a.deregisterFromNrf() + a.sbiServer.Shutdown() +} - logger.InitLog.Infof("NSSF terminated") +func (a *NssfApp) Wait() { + a.wg.Wait() + logger.MainLog.Infof("NSSF terminated") }