From 8f808e8abec6245c654e9e982ac3c92ea90d5dce Mon Sep 17 00:00:00 2001 From: JohnKeeney Date: Wed, 26 Jun 2024 14:10:58 +0100 Subject: [PATCH 01/10] Update release notes for Service Manager and CapifCore (J-Release) Issue-ID: NONRTRIC-972 Change-Id: I6f0ea5ca4226d80e95654bd2e6b5956c320efd7c Signed-off-by: JohnKeeney --- docs/release-notes.rst | 77 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index e4a93f9..3e47269 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -10,35 +10,40 @@ Release Notes This document provides the release notes for the Non-RT RIC Service Management & Exposure (SME). -Version history SME CAPIFcore -============================= +Version history SME Service Manager +=================================== +------------+----------+------------------+--------------------------------------+ | **Date** | **Ver.** | **Author** | **Comment** | | | | | | +------------+----------+------------------+--------------------------------------+ -| 2022-12-14 | 1.0.0 | Henrik Andersson | G Release | -| | | | Initial version of Capifcore | +| 2024-06-26 | 0.1.2 | | J Release (Service Manager) | +| | | | Initial version of Service Manager | | | | | | +------------+----------+------------------+--------------------------------------+ -| 2023-02-10 | 1.0.1 | Yennifer Chacon | G Maintenance | -| | | | Release | + +Version history SME CAPIFCore +============================= + +------------+----------+------------------+--------------------------------------+ -| 2023-06-16 | 1.1.0 | Yennifer Chacon | H Release | +| **Date** | **Ver.** | **Author** | **Comment** | | | | | | +------------+----------+------------------+--------------------------------------+ -| 2023-12-15 | 1.2.0 | John Keeney | I Release | +| 2022-12-14 | 1.0.0 | Henrik Andersson | G Release | +| | | | Initial version of CapifCore | | | | | | +------------+----------+------------------+--------------------------------------+ - -Version history SME Service Manager -=================================== - +| 2023-02-10 | 1.0.1 | Yennifer Chacon | G Maintenance Release (CapifCore) | +| | | | | +------------+----------+------------------+--------------------------------------+ -| **Date** | **Ver.** | **Author** | **Comment** | +| 2023-06-16 | 1.1.0 | Yennifer Chacon | H Release (CapifCore) | | | | | | +------------+----------+------------------+--------------------------------------+ -| TBA | | | Initial release of Service Manager | +| 2023-12-15 | 1.2.0 | John Keeney | I Release (CapifCore) | +| | | | | ++------------+----------+------------------+--------------------------------------+ +| 2024-06-26 | 1.3.1 | John Keeney | J Release (CapifCore) | +| | | | | +------------+----------+------------------+--------------------------------------+ Release Data @@ -100,7 +105,7 @@ H Release | **Release date** | 2023-06-16 | | | | +-----------------------------+---------------------------------------------------+ -| **Purpose of the delivery** | nonrtric-plt-capifcore:1.1.0 | +| **Purpose of the delivery** | o-ran-sc/nonrtric-plt-capifcore:1.1.0 | | | Add more CAPIF core functions and APIs | | | | +-----------------------------+---------------------------------------------------+ @@ -121,7 +126,47 @@ I Release | **Release date** | 2023-12-15 | | | | +-----------------------------+---------------------------------------------------+ -| **Purpose of the delivery** | nonrtric-plt-capifcore:1.2.0 | +| **Purpose of the delivery** | o-ran-sc/nonrtric-plt-capifcore:1.2.0 | | | Add more CAPIF core functions and APIs | | | | +-----------------------------+---------------------------------------------------+ + +J Release +--------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC Service Manager | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/plt/sme/ | +| | 36718abc0fb386770a182c2c01358e1ce3621c75 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | J release | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2024-06-26 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | o-ran-sc/nonrtric-plt-servicemanager:0.1.2 | +| | First release of NONRTRIC Service Manager | +| | | ++-----------------------------+---------------------------------------------------+ + ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC CAPIF Core | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/plt/sme/ | +| | 36718abc0fb386770a182c2c01358e1ce3621c75 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | J release | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2024-06-26 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | o-ran-sc/nonrtric-plt-capifcore:1.3.1 | +| | Small updates & improvements | +| | | ++-----------------------------+---------------------------------------------------+ -- 2.16.6 From f8417715ccf4456b071ee85dc788b7d9fbf56417 Mon Sep 17 00:00:00 2001 From: JohnKeeney Date: Thu, 27 Jun 2024 19:56:23 +0100 Subject: [PATCH 02/10] Roll versions after J-Relase (master branch) Issue-ID: NONRTRIC-972 Change-Id: Ie95dd9222d2798dca8428c618f310f41577d657d Signed-off-by: JohnKeeney --- capifcore/container-tag.yaml | 2 +- docs/overview.rst | 2 +- servicemanager/README.md | 4 ++-- servicemanager/container-tag.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/capifcore/container-tag.yaml b/capifcore/container-tag.yaml index f17eeca..9b9cc34 100644 --- a/capifcore/container-tag.yaml +++ b/capifcore/container-tag.yaml @@ -20,4 +20,4 @@ # By default this file is in the docker build directory, # but the location can configured in the JJB template. --- -tag: 1.3.1 +tag: 1.4.0 diff --git a/docs/overview.rst b/docs/overview.rst index c9b42aa..18fc2bb 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -118,7 +118,7 @@ Service Manager Deployment Postman ******* -A Postman collection has been included in this repo at sme/postman/ServiceManager.postman_collection.json. +A Postman collection has been included in this repo at sme/postman/ServiceManager.postman_collection.json ************* diff --git a/servicemanager/README.md b/servicemanager/README.md index 4be8951..5f04d2f 100644 --- a/servicemanager/README.md +++ b/servicemanager/README.md @@ -3,7 +3,7 @@ ========================LICENSE_START================================= O-RAN-SC %% -Copyright (C) 2024 OpenInfra Foundation Europe. All rights reserved. +Copyright (C) 2024 OpenInfra Foundation Europe. All rights reserved %% Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ export SERVICE_MANAGER_ENV=development ### CAPIFcore and Kong -We also need Kong and CAPIFcore to be running. Please see the examples in the `deploy` folder. You can also use https://gerrit.o-ran-sc.org/r/it/dep for deployment. Please see the notes at https://wiki.o-ran-sc.org/display/RICNR/%5BWIP%5D+Service+Manager. +We also need Kong and CAPIFcore to be running. Please see the examples in the `deploy` folder. You can also use https://gerrit.o-ran-sc.org/r/it/dep for deployment. Please see the notes at https://wiki.o-ran-sc.org/display/RICNR/Release+J%3A+Service+Manager ## Build diff --git a/servicemanager/container-tag.yaml b/servicemanager/container-tag.yaml index ced992f..fdabad8 100644 --- a/servicemanager/container-tag.yaml +++ b/servicemanager/container-tag.yaml @@ -19,4 +19,4 @@ # By default this file is in the docker build directory, # but the location can configured in the JJB template. --- -tag: 0.1.2 +tag: 0.2.0 -- 2.16.6 From 7da72dd8d4d777ff66b4fb9c85cadab28ab3f453 Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Thu, 4 Jul 2024 17:01:28 +0100 Subject: [PATCH 03/10] NONRTRIC-1021: Add API-name to Kong-route Issue-ID: NONRTRIC-1021 Change-Id: I4c6d7a9b7e6e49d9192536b7698e4778078203d5 Signed-off-by: DenisGNoonan --- .../internal/publishserviceapi/typeupdate.go | 33 +++++++++++++--------- servicemanager/main.go | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index d7246bd..3b46a0f 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -77,15 +77,16 @@ func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, ap client := resty.New() profiles := *sd.AefProfiles - for _, profile := range profiles { + for i, profile := range profiles { log.Debugf("createKongRoutes, AefId %s", profile.AefId) - for _, version := range profile.Versions { + for j, version := range profile.Versions { log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion) - for _, resource := range *version.Resources { - statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, apfId, profile.AefId, version.ApiVersion) + for k, resource := range *version.Resources { + statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, &resource, apfId, profile.AefId, version.ApiVersion) if (err != nil) || (statusCode != http.StatusCreated) { return statusCode, err } + (*profiles[i].Versions[j].Resources)[k] = resource } } } @@ -95,7 +96,7 @@ func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, ap func (sd *ServiceAPIDescription) createKongRoute( kongControlPlaneURL string, client *resty.Client, - resource Resource, + resource *Resource, apfId string, aefId string, apiVersion string ) (int, error) { @@ -114,9 +115,13 @@ func (sd *ServiceAPIDescription) createKongRoute( log.Debugf("createKongRoute, routeName %s", routeName) log.Debugf("createKongRoute, aefId %s", aefId) - uri := buildUriWithVersion(apiVersion, resource.Uri) + uri := buildUri(apiVersion, resource.Uri) log.Debugf("createKongRoute, uri %s", uri) + routeUri := buildUri(sd.ApiName, uri) + log.Debugf("createKongRoute, routeUri %s", routeUri) + resource.Uri = routeUri + statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, tags) if (err != nil) || (statusCode != http.StatusCreated) { return statusCode, err @@ -127,7 +132,7 @@ func (sd *ServiceAPIDescription) createKongRoute( // Define the route information for Kong kongRouteInfo := map[string]interface{}{ "name": routeName, - "paths": []string{uri}, + "paths": []string{routeUri}, "methods": resource.Operations, "tags": tags, "strip_path": true, @@ -159,15 +164,15 @@ func (sd *ServiceAPIDescription) createKongRoute( return resp.StatusCode(), nil } -func buildUriWithVersion(apiVersion string, uri string) string { - if apiVersion != "" { - if apiVersion[0] != '/' { - apiVersion = "/" + apiVersion +func buildUri(prependUri string, uri string) string { + if prependUri != "" { + if prependUri[0] != '/' { + prependUri = "/" + prependUri } - if apiVersion[len(apiVersion)-1] != '/' && uri[0] != '/' { - apiVersion = apiVersion + "/" + if prependUri[len(prependUri)-1] != '/' && uri[0] != '/' { + prependUri = prependUri + "/" } - uri = apiVersion + uri + uri = prependUri + uri } return uri } diff --git a/servicemanager/main.go b/servicemanager/main.go index 5b949aa..b90782c 100644 --- a/servicemanager/main.go +++ b/servicemanager/main.go @@ -155,7 +155,7 @@ func keepServerAlive() { } func hello(c echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") + return c.String(http.StatusOK, "Hello World, from Service Manager!") } func getSwagger(c echo.Context) error { -- 2.16.6 From c99b6b4342fcf8ea70772dc57fcb04c5fdbe4ac8 Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Tue, 16 Jul 2024 16:42:54 +0100 Subject: [PATCH 04/10] NONRTRIC-1005: Support for regex capture groups Issue-ID: NONRTRIC-1005 Change-Id: I4f32d68649c840701a37429d1c956f9b45bcc603 Signed-off-by: DenisGNoonan --- servicemanager/.env.example | 2 +- servicemanager/internal/envreader/envreader.go | 87 ++++++- .../internal/publishservice/publishservice_test.go | 259 +++++++++++++++++++-- .../internal/publishserviceapi/typeupdate.go | 204 ++++++++++++++-- servicemanager/main.go | 11 +- servicemanager/mockkong/kong_mock.go | 113 +++++++++ 6 files changed, 623 insertions(+), 53 deletions(-) diff --git a/servicemanager/.env.example b/servicemanager/.env.example index 1d3c4ef..c5d5b32 100644 --- a/servicemanager/.env.example +++ b/servicemanager/.env.example @@ -8,7 +8,7 @@ KONG_CONTROL_PLANE_PORT= KONG_DATA_PLANE_IPV4= KONG_DATA_PLANE_PORT= CAPIF_PROTOCOL= -CAPIF_IPV4= +CAPIF_IPV4= CAPIF_PORT= LOG_LEVEL= SERVICE_MANAGER_PORT= diff --git a/servicemanager/internal/envreader/envreader.go b/servicemanager/internal/envreader/envreader.go index f08b1d7..315772e 100644 --- a/servicemanager/internal/envreader/envreader.go +++ b/servicemanager/internal/envreader/envreader.go @@ -21,6 +21,8 @@ package envreader import ( + "fmt" + "net/url" "os" "path/filepath" "runtime" @@ -75,6 +77,14 @@ func (r *RealConfigReader) ReadDotEnv() (map[string]string, map[string]int, erro logConfig(myEnv, envFile) myPorts, err := createMapPorts(myEnv) + if err == nil { + err = validateEnv(myEnv) + } + + if err == nil { + err = validateUrls(myEnv, myPorts) + } + return myEnv, myPorts, err } @@ -110,31 +120,98 @@ func logConfig(myEnv map[string]string, envFile string) { log.Infof("TEST_SERVICE_PORT %s", myEnv["TEST_SERVICE_PORT"]) } +func validateUrls(myEnv map[string]string, myPorts map[string]int) error { + capifProtocol := myEnv["CAPIF_PROTOCOL"] + capifIPv4 := myEnv["CAPIF_IPV4"] + capifPort := myPorts["CAPIF_PORT"] + capifcoreUrl := fmt.Sprintf("%s://%s:%d", capifProtocol, capifIPv4, capifPort) + + kongProtocol := myEnv["KONG_PROTOCOL"] + kongControlPlaneIPv4 := myEnv["KONG_CONTROL_PLANE_IPV4"] + kongControlPlanePort := myPorts["KONG_CONTROL_PLANE_PORT"] + kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongControlPlaneIPv4, kongControlPlanePort) + + kongDataPlaneIPv4 := myEnv["KONG_DATA_PLANE_IPV4"] + kongDataPlanePort := myPorts["KONG_DATA_PLANE_PORT"] + kongDataPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongDataPlaneIPv4, kongDataPlanePort) + + log.Infof("Capifcore URL %s", capifcoreUrl) + log.Infof("Kong Control Plane URL %s", kongControlPlaneURL) + log.Infof("Kong Data Plane URL %s", kongDataPlaneURL) + + // Very basic checks + _, err := url.ParseRequestURI(capifcoreUrl) + if err != nil { + err = fmt.Errorf("error parsing Capifcore URL: %s", err) + return err + } + _, err = url.ParseRequestURI(kongControlPlaneURL) + if err != nil { + err = fmt.Errorf("error parsing Kong Control Plane URL: %s", err) + return err + } + _, err = url.ParseRequestURI(kongDataPlaneURL) + if err != nil { + err = fmt.Errorf("error parsing Kong Data Plane URL: %s", err) + return err + } + + return nil +} + +func validateEnv(myEnv map[string]string) error { + var err error = nil + + kongDomain := myEnv["KONG_DOMAIN"] + kongProtocol := myEnv["KONG_PROTOCOL"] + kongControlPlaneIPv4 := myEnv["KONG_CONTROL_PLANE_IPV4"] + kongDataPlaneIPv4 := myEnv["KONG_DATA_PLANE_IPV4"] + capifProtocol := myEnv["CAPIF_PROTOCOL"] + capifIPv4 := myEnv["CAPIF_IPV4"] + + if kongDomain == "" || kongDomain == "" { + err = fmt.Errorf("error loading KONG_DOMAIN from .env file: %s", kongDomain) + } else if kongProtocol == "" || kongProtocol == "" { + err = fmt.Errorf("error loading KONG_PROTOCOL from .env file: %s", kongProtocol) + } else if kongControlPlaneIPv4 == "" || kongControlPlaneIPv4 == "" { + err = fmt.Errorf("error loading KONG_CONTROL_PLANE_IPV4 from .env file: %s", kongControlPlaneIPv4) + } else if kongDataPlaneIPv4 == "" || kongDataPlaneIPv4 == "" { + err = fmt.Errorf("error loading KONG_DATA_PLANE_IPV4 from .env file: %s", kongDataPlaneIPv4) + } else if capifProtocol == "" || capifProtocol == "" { + err = fmt.Errorf("error loading CAPIF_PROTOCOL from .env file: %s", capifProtocol) + } else if capifIPv4 == "" || capifIPv4 == "" || capifIPv4 == "" { + err = fmt.Errorf("error loading CAPIF_IPV4 from .env file: %s", capifIPv4) + } + // TEST_SERVICE_IPV4 is used only by the unit tests and are validated in the unit tests. + + return err +} + func createMapPorts(myEnv map[string]string) (map[string]int, error) { myPorts := make(map[string]int) var err error myPorts["KONG_DATA_PLANE_PORT"], err = strconv.Atoi(myEnv["KONG_DATA_PLANE_PORT"]) if err != nil { - log.Fatalf("error loading KONG_DATA_PLANE_PORT from .env file: %s", err) + err = fmt.Errorf("error loading KONG_DATA_PLANE_PORT from .env file: %s", err) return nil, err } myPorts["KONG_CONTROL_PLANE_PORT"], err = strconv.Atoi(myEnv["KONG_CONTROL_PLANE_PORT"]) if err != nil { - log.Fatalf("error loading KONG_CONTROL_PLANE_PORT from .env file: %s", err) + err = fmt.Errorf("error loading KONG_CONTROL_PLANE_PORT from .env file: %s", err) return nil, err } myPorts["CAPIF_PORT"], err = strconv.Atoi(myEnv["CAPIF_PORT"]) if err != nil { - log.Fatalf("error loading CAPIF_PORT from .env file: %s", err) + err = fmt.Errorf("error loading CAPIF_PORT from .env file: %s", err) return nil, err } myPorts["SERVICE_MANAGER_PORT"], err = strconv.Atoi(myEnv["SERVICE_MANAGER_PORT"]) if err != nil { - log.Fatalf("error loading SERVICE_MANAGER_PORT from .env file: %s", err) + err = fmt.Errorf("error loading SERVICE_MANAGER_PORT from .env file: %s", err) return nil, err } @@ -142,7 +219,7 @@ func createMapPorts(myEnv map[string]string) (map[string]int, error) { if myEnv["TEST_SERVICE_PORT"] != "" { myPorts["TEST_SERVICE_PORT"], err = strconv.Atoi(myEnv["TEST_SERVICE_PORT"]) if err != nil { - log.Fatalf("error loading TEST_SERVICE_PORT from .env file: %s", err) + err = fmt.Errorf("error loading TEST_SERVICE_PORT from .env file: %s", err) return nil, err } } diff --git a/servicemanager/internal/publishservice/publishservice_test.go b/servicemanager/internal/publishservice/publishservice_test.go index c42cba7..31d5293 100644 --- a/servicemanager/internal/publishservice/publishservice_test.go +++ b/servicemanager/internal/publishservice/publishservice_test.go @@ -202,6 +202,24 @@ func capifCleanUp() { result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager) assert.Equal(t, http.StatusNoContent, result.Code()) + apiName = "helloworld-v1" + apiId = "api_id_" + apiName + + result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager) + assert.Equal(t, http.StatusNoContent, result.Code()) + + apiName = "helloworld-v1-id" + apiId = "api_id_" + apiName + + result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager) + assert.Equal(t, http.StatusNoContent, result.Code()) + + apiName = "helloworld-no-version" + apiId = "api_id_" + apiName + + result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+apiId).Go(t, eServiceManager) + assert.Equal(t, http.StatusNoContent, result.Code()) + // Delete the provider domainID := "domain_id_Kong" result = testutil.NewRequest().Delete("/api-provider-management/v1/registrations/"+domainID).Go(t, eServiceManager) @@ -262,7 +280,11 @@ func TestPostUnpublishedServiceWithUnregisteredPublisher(t *testing.T) { assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing") apiName := "apiName" - newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort) + apiVersion := "v1" + resourceName := "helloworld" + uri := "/helloworld" + + newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri) // Attempt to publish a service for provider result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) @@ -288,7 +310,7 @@ func TestRegisterValidProvider(t *testing.T) { assert.NoError(t, err, "error unmarshaling response") } -func TestPublishUnpublishService(t *testing.T) { +func TestPublishUnpublishServiceMissingInterface(t *testing.T) { apfId := "APF_id_rApp_Kong_as_APF" apiName := "apiName" @@ -331,11 +353,37 @@ func TestPublishUnpublishService(t *testing.T) { assert.NoError(t, err, "error unmarshaling response") assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription") +} + + +func TestPublishUnpublishWithoutVersionId(t *testing.T) { + apfId := "APF_id_rApp_Kong_as_APF" + + myEnv, myPorts, err := mockConfigReader.ReadDotEnv() + assert.Nil(t, err, "error reading env file") + + testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"]) + testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"]) + + assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing") + assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing") + + apiVersion := "v1" + resourceName := "helloworld" + uri := "/helloworld" + apiName := "helloworld-v1" + + aefId := "AEF_id_rApp_Kong_as_AEF" + namespace := "namespace" + repoName := "repoName" + chartName := "chartName" + releaseName := "releaseName" + description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName) - newServiceDescription = getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort) + newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri) // Publish a service for provider - result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) + result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) assert.Equal(t, http.StatusCreated, result.Code()) if result.Code() != http.StatusCreated { @@ -374,30 +422,184 @@ func TestPublishUnpublishService(t *testing.T) { assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4) assert.Equal(t, kongDataPlanePort, resultServicePort) - // Publish the same service again should result in Forbidden - newServiceDescription.ApiId = &newApiId - result = testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) - assert.Equal(t, http.StatusForbidden, result.Code()) + // Check Versions structure + version := aefProfile.Versions[0] + assert.Equal(t, "v1", version.ApiVersion) - err = result.UnmarshalBodyToObject(&resultError) + resource := (*version.Resources)[0] + communicationType := publishapi.CommunicationType("REQUEST_RESPONSE") + assert.Equal(t, communicationType, resource.CommType) + + assert.Equal(t, 1, len(*resource.Operations)) + var operation publishapi.Operation = "GET" + assert.Equal(t, operation, (*resource.Operations)[0]) + assert.Equal(t, "helloworld", resource.ResourceName) + assert.Equal(t, "/helloworld-v1/helloworld", resource.Uri) +} + +func TestPublishUnpublishVersionId(t *testing.T) { + apfId := "APF_id_rApp_Kong_as_APF" + + myEnv, myPorts, err := mockConfigReader.ReadDotEnv() + assert.Nil(t, err, "error reading env file") + + testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"]) + testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"]) + + assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing") + assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing") + + apiVersion := "v1" + resourceName := "helloworld-id" + uri := "~/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)" + apiName := "helloworld-v1-id" + + aefId := "AEF_id_rApp_Kong_as_AEF" + namespace := "namespace" + repoName := "repoName" + chartName := "chartName" + releaseName := "releaseName" + description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName) + + newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri) + + // Publish a service for provider + result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) + assert.Equal(t, http.StatusCreated, result.Code()) + + if result.Code() != http.StatusCreated { + log.Fatalf("failed to publish the service with HTTP result code %d", result.Code()) + return + } + + var resultService publishapi.ServiceAPIDescription + err = result.UnmarshalJsonToObject(&resultService) assert.NoError(t, err, "error unmarshaling response") - assert.Contains(t, *resultError.Cause, "already published") - assert.Equal(t, http.StatusForbidden, *resultError.Status) + newApiId := "api_id_" + apiName + assert.Equal(t, newApiId, *resultService.ApiId) - // Delete the service - result = testutil.NewRequest().Delete("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager) - assert.Equal(t, http.StatusNoContent, result.Code()) + assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation)) - // Check no services published - result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis").Go(t, eServiceManager) + // Check that the service is published for the provider + result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager) assert.Equal(t, http.StatusOK, result.Code()) - // Parse JSON from the response body - err = result.UnmarshalJsonToObject(&resultServices) + err = result.UnmarshalJsonToObject(&resultService) assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, newApiId, *resultService.ApiId) + + aefProfile := (*resultService.AefProfiles)[0] + interfaceDescription := (*aefProfile.InterfaceDescriptions)[0] + + resultServiceIpv4 := *interfaceDescription.Ipv4Addr + resultServicePort := *interfaceDescription.Port + + kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"]) + kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"]) + + assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing") + assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing") + + assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4) + assert.Equal(t, kongDataPlanePort, resultServicePort) + + // Check Versions structure + version := aefProfile.Versions[0] + assert.Equal(t, "v1", version.ApiVersion) + + resource := (*version.Resources)[0] + communicationType := publishapi.CommunicationType("REQUEST_RESPONSE") + assert.Equal(t, communicationType, resource.CommType) + + assert.Equal(t, 1, len(*resource.Operations)) + var operation publishapi.Operation = "GET" + assert.Equal(t, operation, (*resource.Operations)[0]) + + assert.Equal(t, "helloworld-id", resource.ResourceName) + assert.Equal(t, "~/helloworld-v1-id/helloworld/v1/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) +} + +func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) { + apfId := "APF_id_rApp_Kong_as_APF" + + myEnv, myPorts, err := mockConfigReader.ReadDotEnv() + assert.Nil(t, err, "error reading env file") + + testServiceIpv4 := common29122.Ipv4Addr(myEnv["TEST_SERVICE_IPV4"]) + testServicePort := common29122.Port(myPorts["TEST_SERVICE_PORT"]) + + assert.NotEmpty(t, testServiceIpv4, "TEST_SERVICE_IPV4 is required in .env file for unit testing") + assert.NotZero(t, testServicePort, "TEST_SERVICE_PORT is required in .env file for unit testing") + + apiVersion := "" + resourceName := "helloworld-no-version" + uri := "~/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)" + apiName := "helloworld-no-version" + + aefId := "AEF_id_rApp_Kong_as_AEF" + namespace := "namespace" + repoName := "repoName" + chartName := "chartName" + releaseName := "releaseName" + description := fmt.Sprintf("Description,%s,%s,%s,%s", namespace, repoName, chartName, releaseName) + + newServiceDescription := getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort, apiVersion, resourceName, uri) + + // Publish a service for provider + result := testutil.NewRequest().Post("/published-apis/v1/"+apfId+"/service-apis").WithJsonBody(newServiceDescription).Go(t, eServiceManager) + assert.Equal(t, http.StatusCreated, result.Code()) + + if result.Code() != http.StatusCreated { + log.Fatalf("failed to publish the service with HTTP result code %d", result.Code()) + return + } + + var resultService publishapi.ServiceAPIDescription + err = result.UnmarshalJsonToObject(&resultService) + assert.NoError(t, err, "error unmarshaling response") + newApiId := "api_id_" + apiName + assert.Equal(t, newApiId, *resultService.ApiId) + + assert.Equal(t, "http://example.com/published-apis/v1/"+apfId+"/service-apis/"+*resultService.ApiId, result.Recorder.Header().Get(echo.HeaderLocation)) + + // Check that the service is published for the provider + result = testutil.NewRequest().Get("/published-apis/v1/"+apfId+"/service-apis/"+newApiId).Go(t, eServiceManager) + assert.Equal(t, http.StatusOK, result.Code()) + + err = result.UnmarshalJsonToObject(&resultService) + assert.NoError(t, err, "error unmarshaling response") + assert.Equal(t, newApiId, *resultService.ApiId) + + aefProfile := (*resultService.AefProfiles)[0] + interfaceDescription := (*aefProfile.InterfaceDescriptions)[0] + + resultServiceIpv4 := *interfaceDescription.Ipv4Addr + resultServicePort := *interfaceDescription.Port + + kongDataPlaneIPv4 := common29122.Ipv4Addr(myEnv["KONG_DATA_PLANE_IPV4"]) + kongDataPlanePort := common29122.Port(myPorts["KONG_DATA_PLANE_PORT"]) + + assert.NotEmpty(t, kongDataPlaneIPv4, "KONG_DATA_PLANE_IPV4 is required in .env file for unit testing") + assert.NotZero(t, kongDataPlanePort, "KONG_DATA_PLANE_PORT is required in .env file for unit testing") + + assert.Equal(t, kongDataPlaneIPv4, resultServiceIpv4) + assert.Equal(t, kongDataPlanePort, resultServicePort) + + // Check Versions structure + version := aefProfile.Versions[0] + assert.Equal(t, "", version.ApiVersion) + + resource := (*version.Resources)[0] + communicationType := publishapi.CommunicationType("REQUEST_RESPONSE") + assert.Equal(t, communicationType, resource.CommType) + + assert.Equal(t, 1, len(*resource.Operations)) + var operation publishapi.Operation = "GET" + assert.Equal(t, operation, (*resource.Operations)[0]) + + assert.Equal(t, "helloworld-no-version", resource.ResourceName) + assert.Equal(t, "~/helloworld-no-version/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) - // Check if the parsed array is empty - assert.Zero(t, len(resultServices)) capifCleanUp() } @@ -449,7 +651,16 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] return err } -func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port) publishapi.ServiceAPIDescription { +func getServiceAPIDescription( + aefId string, + apiName string, + description string, + testServiceIpv4 common29122.Ipv4Addr, + testServicePort common29122.Port, + apiVersion string, + resourceName string, + uri string) publishapi.ServiceAPIDescription { + domainName := "Kong" var protocol publishapi.Protocol = "HTTP_1_1" @@ -470,15 +681,15 @@ func getServiceAPIDescription(aefId, apiName, description string, testServiceIpv Protocol: &protocol, Versions: []publishapi.Version{ { - ApiVersion: "v1", + ApiVersion: apiVersion, Resources: &[]publishapi.Resource{ { CommType: "REQUEST_RESPONSE", Operations: &[]publishapi.Operation{ "GET", }, - ResourceName: "helloworld", - Uri: "/helloworld", + ResourceName: resourceName, + Uri: uri, }, }, }, diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index 3b46a0f..5ecc7e5 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -25,6 +25,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" resty "github.com/go-resty/resty/v2" @@ -115,33 +116,63 @@ func (sd *ServiceAPIDescription) createKongRoute( log.Debugf("createKongRoute, routeName %s", routeName) log.Debugf("createKongRoute, aefId %s", aefId) - uri := buildUri(apiVersion, resource.Uri) + uri := insertVersion(apiVersion, resource.Uri) log.Debugf("createKongRoute, uri %s", uri) - routeUri := buildUri(sd.ApiName, uri) + // Create a url.Values map to hold the form data + data := url.Values{} + serviceUri := uri + + foundRegEx := false + if strings.HasPrefix(uri, "~") { + log.Debug("createKongRoute, found regex prefix") + foundRegEx = true + data.Set("strip_path", "false") + serviceUri = "/" + } else { + log.Debug("createKongRoute, no regex prefix found") + data.Set("strip_path", "true") + } + + log.Debugf("createKongRoute, serviceUri %s", serviceUri) + log.Debugf("createKongRoute, strip_path %s", data.Get("strip_path")) + + routeUri := prependUri(sd.ApiName, uri) log.Debugf("createKongRoute, routeUri %s", routeUri) resource.Uri = routeUri - statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, tags) - if (err != nil) || (statusCode != http.StatusCreated) { + statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, serviceUri, tags) + if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { + // We carry on if we tried to create a duplicate service. We depend on Kong route matching. return statusCode, err } - kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes" + data.Set("name", routeName) - // Define the route information for Kong - kongRouteInfo := map[string]interface{}{ - "name": routeName, - "paths": []string{routeUri}, - "methods": resource.Operations, - "tags": tags, - "strip_path": true, - } + routeUriPaths := []string{routeUri} + for _, path := range routeUriPaths { + log.Debugf("createKongRoute, path %s", path) + data.Add("paths", path) + } + + for _, tag := range tags { + log.Debugf("createKongRoute, tag %s", tag) + data.Add("tags", tag) + } + + for _, op := range *resource.Operations { + log.Debugf("createKongRoute, op %s", string(op)) + data.Add("methods", string(op)) + } + + // Encode the data to application/x-www-form-urlencoded format + encodedData := data.Encode() // Make the POST request to create the Kong service + kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes" resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(kongRouteInfo). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetBody(strings.NewReader(encodedData)). Post(kongRoutesURL) // Check for errors in the request @@ -153,6 +184,12 @@ func (sd *ServiceAPIDescription) createKongRoute( // Check the response status code if resp.StatusCode() == http.StatusCreated { log.Infof("kong route %s created successfully", routeName) + if (foundRegEx) { + statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, uri) + if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { + return statusCode, err + } + } } else { log.Debugf("kongRoutesURL %s", kongRoutesURL) err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode()) @@ -164,15 +201,146 @@ func (sd *ServiceAPIDescription) createKongRoute( return resp.StatusCode(), nil } -func buildUri(prependUri string, uri string) string { +func (sd *ServiceAPIDescription) createRequestTransformer( + kongControlPlaneURL string, + client *resty.Client, + routeName string, + routePattern string) (int, error) { + + log.Trace("entering createRequestTransformer") + + // Make the POST request to create the Kong Request Transformer + kongRequestTransformerURL := kongControlPlaneURL + "/routes/" + routeName + "/plugins" + + transformPattern, _ := deriveTransformPattern(routePattern) + + // Create the form data + formData := url.Values{ + "name": {"request-transformer"}, + "config.replace.uri": {transformPattern}, + } + encodedData := formData.Encode() + + // Create a new HTTP POST request + resp, err := client.R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetBody(strings.NewReader(encodedData)). + Post(kongRequestTransformerURL) + + // Check for errors in the request + if err != nil { + log.Debugf("createRequestTransformer POST Error: %v", err) + return resp.StatusCode(), err + } + + // Check the response status code + if resp.StatusCode() == http.StatusCreated { + log.Infof("kong request transformer created successfully for route %s", routeName) + } else { + log.Debugf("kongRequestTransformerURL %s", kongRequestTransformerURL) + err = fmt.Errorf("error creating Kong request transformer. Status code: %d", resp.StatusCode()) + log.Error(err.Error()) + log.Errorf("response body: %s", resp.Body()) + return resp.StatusCode(), err + } + + return resp.StatusCode(), nil +} + +// Function to derive the transform pattern from the route pattern +func deriveTransformPattern(routePattern string) (string, error) { + log.Trace("entering deriveTransformPattern") + + log.Debugf("deriveTransformPattern routePattern %s", routePattern) + + routePattern = strings.TrimPrefix(routePattern, "~") + log.Debugf("deriveTransformPattern, TrimPrefix trimmed routePattern %s", routePattern) + + // Append a slash to handle an edge case for matching a trailing capture group. + appendedSlash := false + if routePattern[len(routePattern)-1] != '/' { + routePattern = routePattern + "/" + appendedSlash = true + log.Debugf("deriveTransformPattern, append / routePattern %s", routePattern) + } + + // Regular expression to match named capture groups + re := regexp.MustCompile(`/\(\?<([^>]+)>([^\/]+)/`) + // Find all matches in the route pattern + matches := re.FindAllStringSubmatch(routePattern, -1) + + transformPattern := routePattern + for _, match := range matches { + // match[0] is the full match, match[1] is the capture group name, match[2] is the pattern + placeholder := fmt.Sprintf("/$(uri_captures[\"%s\"])/", match[1]) + // Replace the capture group with the corresponding placeholder + transformPattern = strings.Replace(transformPattern, match[0], placeholder, 1) + } + log.Debugf("deriveTransformPattern transformPattern %s", transformPattern) + + if appendedSlash { + transformPattern = strings.TrimSuffix(transformPattern, "/") + log.Debugf("deriveTransformPattern, remove / transformPattern %s", transformPattern) + } + + return transformPattern, nil +} + +func insertVersion(version string, route string) string { + versionedRoute := route + + if version != "" { + sep := "/" + n := 3 + + foundRegEx := false + if strings.HasPrefix(route, "~") { + log.Debug("insertVersion, found regex prefix") + foundRegEx = true + route = strings.TrimPrefix(route, "~") + } + + log.Debugf("insertVersion route %s", route) + split := strings.SplitAfterN(route, sep, n) + log.Debugf("insertVersion split %q", split) + + versionedRoute = split[0] + if len(split) == 2 { + versionedRoute = split[0] + split[1] + } else if len(split) > 2 { + versionedRoute = split[0] + split[1] + version + sep + split[2] + } + + if foundRegEx { + versionedRoute = "~" + versionedRoute + } + } + log.Debugf("insertVersion versionedRoute %s", versionedRoute) + + return versionedRoute +} + +func prependUri(prependUri string, uri string) string { if prependUri != "" { + trimmedUri := uri + foundRegEx := false + if strings.HasPrefix(uri, "~") { + log.Debug("prependUri, found regex prefix") + foundRegEx = true + trimmedUri = strings.TrimPrefix(uri, "~") + log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri) + } + if prependUri[0] != '/' { prependUri = "/" + prependUri } - if prependUri[len(prependUri)-1] != '/' && uri[0] != '/' { + if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' { prependUri = prependUri + "/" } - uri = prependUri + uri + uri = prependUri + trimmedUri + if foundRegEx { + uri = "~" + uri + } } return uri } diff --git a/servicemanager/main.go b/servicemanager/main.go index b90782c..d0fbe4f 100644 --- a/servicemanager/main.go +++ b/servicemanager/main.go @@ -48,8 +48,9 @@ import ( func main() { realConfigReader := &envreader.RealConfigReader{} myEnv, myPorts, err := realConfigReader.ReadDotEnv() + if err != nil { - log.Fatal("error loading environment file") + log.Fatalf("error loading environment file, %v", err) return } @@ -87,7 +88,7 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] // Register ProviderManagement providerManagerSwagger, err := providermanagementapi.GetSwagger() if err != nil { - log.Fatalf("error loading ProviderManagement swagger spec\n: %s", err) + log.Fatalf("error loading ProviderManagement swagger spec\n: %v", err) return err } providerManagerSwagger.Servers = nil @@ -99,7 +100,7 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] // Register PublishService publishServiceSwagger, err := publishserviceapi.GetSwagger() if err != nil { - log.Fatalf("error loading PublishService swagger spec\n: %s", err) + log.Fatalf("error loading PublishService swagger spec\n: %v", err) return err } publishServiceSwagger.Servers = nil @@ -116,7 +117,7 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] // Register InvokerManagement invokerManagerSwagger, err := invokermanagementapi.GetSwagger() if err != nil { - log.Fatalf("error loading InvokerManagement swagger spec\n: %s", err) + log.Fatalf("error loading InvokerManagement swagger spec\n: %v", err) return err } invokerManagerSwagger.Servers = nil @@ -128,7 +129,7 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] // Register DiscoverService discoverServiceSwagger, err := discoverserviceapi.GetSwagger() if err != nil { - log.Fatalf("error loading DiscoverService swagger spec\n: %s", err) + log.Fatalf("error loading DiscoverService swagger spec\n: %v", err) return err } diff --git a/servicemanager/mockkong/kong_mock.go b/servicemanager/mockkong/kong_mock.go index 14acd57..c0a6dba 100644 --- a/servicemanager/mockkong/kong_mock.go +++ b/servicemanager/mockkong/kong_mock.go @@ -85,6 +85,78 @@ func RegisterHandlers(e *echo.Echo) { return c.String(http.StatusCreated, string(body)) }) + e.POST("/services/api_id_apiName_helloworld-id/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName_helloworld-no-version/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-id_helloworld-id/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1_helloworld/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-no-version_helloworld-no-version/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_apiName_helloworld-id/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_apiName_helloworld-no-version/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-v1-id_helloworld-id/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-no-version_helloworld-no-version/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + e.GET("/services", func(c echo.Context) error { return c.String(http.StatusOK, "{}") }) @@ -132,4 +204,45 @@ func RegisterHandlers(e *echo.Echo) { e.DELETE("/services/api_id_apiName2_app", func(c echo.Context) error { return c.NoContent(http.StatusNoContent) }) + + e.DELETE("/routes/api_id_apiName_helloworld-id", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/routes/api_id_apiName_helloworld-no-version", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/routes/api_id_helloworld-v1_helloworld", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/services/api_id_apiName_helloworld-id", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/services/api_id_apiName_helloworld-no-version", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/services/api_id_helloworld-v1_helloworld", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("routes/api_id_helloworld-no-version_helloworld-no-version", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("services/api_id_helloworld-no-version_helloworld-no-version", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/routes/api_id_helloworld-v1-id_helloworld-id", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + + e.DELETE("/services/api_id_helloworld-v1-id_helloworld-id", func(c echo.Context) error { + return c.NoContent(http.StatusNoContent) + }) + } \ No newline at end of file -- 2.16.6 From 6bb6446fd84a7cf225a20cf64b0adc7e8085f81b Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Tue, 6 Aug 2024 16:50:42 +0100 Subject: [PATCH 05/10] NONRTRIC-1005: Add multiple Interface descriptions Change-Id: I2a0e208a05d04cb4acc5bc50932925a80cecf3f5 Signed-off-by: DenisGNoonan --- docs/release-notes.rst | 7 +- .../discoverservice/discoverservice_test.go | 13 +- .../internal/publishservice/publishservice.go | 14 +- .../internal/publishservice/publishservice_test.go | 8 +- .../internal/publishserviceapi/typeupdate.go | 578 +++++++++++++-------- servicemanager/mockkong/kong_mock.go | 162 +++++- 6 files changed, 553 insertions(+), 229 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 3e47269..b8b51d0 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -17,10 +17,15 @@ Version history SME Service Manager | **Date** | **Ver.** | **Author** | **Comment** | | | | | | +------------+----------+------------------+--------------------------------------+ -| 2024-06-26 | 0.1.2 | | J Release (Service Manager) | +| 2024-06-26 | 0.1.2 | Denis G Noonan | J Release (Service Manager) | | | | | Initial version of Service Manager | | | | | | +------------+----------+------------------+--------------------------------------+ +| TBA | TBA | Denis G Noonan | Service Manager includes support for | +| | | | dynamic URIs, multiple interface | +| | | | descriptions, and improved schema | +| | | | validation. | ++------------+----------+------------------+--------------------------------------+ Version history SME CAPIFCore ============================= diff --git a/servicemanager/internal/discoverservice/discoverservice_test.go b/servicemanager/internal/discoverservice/discoverservice_test.go index ab6ae85..3b6fa81 100644 --- a/servicemanager/internal/discoverservice/discoverservice_test.go +++ b/servicemanager/internal/discoverservice/discoverservice_test.go @@ -731,16 +731,14 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] func getServiceAPIDescription(aefId string, apiName string, apiCategory string, apiVersion string, protocol *publishapi.Protocol, dataFormat *publishapi.DataFormat, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port, commType publishapi.CommunicationType) publishapi.ServiceAPIDescription { domainName := "Kong" otherDomainName := "otherDomain" - var otherProtocol publishapi.Protocol = "HTTP_2" + var DataFormatOther publishapi.DataFormat = "OTHER" categoryPointer := &apiCategory if apiCategory == "" { categoryPointer = nil } - var DataFormatOther publishapi.DataFormat = "OTHER" - return publishapi.ServiceAPIDescription{ AefProfiles: &[]publishapi.AefProfile{ { @@ -775,6 +773,15 @@ func getServiceAPIDescription(aefId string, apiName string, apiCategory string, }, { AefId: aefId, // "otherAefId" + InterfaceDescriptions: &[]publishapi.InterfaceDescription{ + { + Ipv4Addr: &testServiceIpv4, + Port: &testServicePort, + SecurityMethods: &[]publishapi.SecurityMethod{ + "PSK", + }, + }, + }, DomainName: &otherDomainName, Protocol: &otherProtocol, DataFormat: &DataFormatOther, diff --git a/servicemanager/internal/publishservice/publishservice.go b/servicemanager/internal/publishservice/publishservice.go index 4031827..d042cd0 100644 --- a/servicemanager/internal/publishservice/publishservice.go +++ b/servicemanager/internal/publishservice/publishservice.go @@ -103,10 +103,18 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e ps.KongDataPlaneIPv4, ps.KongDataPlanePort, apfId) - if (err != nil) || (statusCode != http.StatusCreated) { - // We can return with http.StatusForbidden if there is a http.StatusConflict detected by Kong + + log.Trace("After RegisterKong") + + if err != nil { msg := err.Error() - log.Errorf("error on RegisterKong %s", msg) + log.Errorf("PostApfIdServiceApis, error on RegisterKong %s", msg) + return sendCoreError(ctx, statusCode, msg) + } + if statusCode != http.StatusCreated { + // We can return with http.StatusForbidden if there is a http.StatusConflict detected by Kong + msg := "error detected by Kong" + log.Errorf(msg) return sendCoreError(ctx, statusCode, msg) } diff --git a/servicemanager/internal/publishservice/publishservice_test.go b/servicemanager/internal/publishservice/publishservice_test.go index 31d5293..eb33c38 100644 --- a/servicemanager/internal/publishservice/publishservice_test.go +++ b/servicemanager/internal/publishservice/publishservice_test.go @@ -352,7 +352,7 @@ func TestPublishUnpublishServiceMissingInterface(t *testing.T) { err = result.UnmarshalJsonToObject(&resultError) assert.NoError(t, err, "error unmarshaling response") - assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription") + assert.Contains(t, *resultError.Cause, "cannot read InterfaceDescriptions") } @@ -434,7 +434,7 @@ func TestPublishUnpublishWithoutVersionId(t *testing.T) { var operation publishapi.Operation = "GET" assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld", resource.ResourceName) - assert.Equal(t, "/helloworld-v1/helloworld", resource.Uri) + assert.Equal(t, "/helloworld-v1/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld", resource.Uri) } func TestPublishUnpublishVersionId(t *testing.T) { @@ -516,7 +516,7 @@ func TestPublishUnpublishVersionId(t *testing.T) { assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld-id", resource.ResourceName) - assert.Equal(t, "~/helloworld-v1-id/helloworld/v1/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) + assert.Equal(t, "~/helloworld-v1-id/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld/v1/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) } func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) { @@ -598,7 +598,7 @@ func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) { assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld-no-version", resource.ResourceName) - assert.Equal(t, "~/helloworld-no-version/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) + assert.Equal(t, "~/helloworld-no-version/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) capifCleanUp() } diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index 5ecc7e5..9480104 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -26,9 +26,11 @@ import ( "net/http" "net/url" "regexp" + "strconv" "strings" resty "github.com/go-resty/resty/v2" + "github.com/google/uuid" log "github.com/sirupsen/logrus" common29122 "oransc.org/nonrtric/servicemanager/internal/common29122" @@ -57,7 +59,7 @@ func (sd *ServiceAPIDescription) RegisterKong( ) kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongControlPlaneIPv4, kongControlPlanePort) - statusCode, err = sd.createKongRoutes(kongControlPlaneURL, apfId) + statusCode, err = sd.createKongInterfaceDescriptions(kongControlPlaneURL, apfId) if (err != nil) || (statusCode != http.StatusCreated) { return statusCode, err } @@ -68,107 +70,411 @@ func (sd *ServiceAPIDescription) RegisterKong( return statusCode, nil } -func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, apfId string) (int, error) { - log.Trace("entering createKongRoutes") +func (sd *ServiceAPIDescription) createKongInterfaceDescriptions(kongControlPlaneURL string, apfId string) (int, error) { + log.Trace("entering createKongInterfaceDescriptions") + var ( statusCode int err error ) - client := resty.New() + outputUris := []string{} + + if sd == nil { + err = errors.New("cannot read ServiceAPIDescription") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + if (sd.AefProfiles == nil) || (len(*sd.AefProfiles) < 1) { + err = errors.New("cannot read AefProfiles") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } profiles := *sd.AefProfiles + for _, profile := range profiles { + log.Debugf("createKongInterfaceDescriptions, AefId %s", profile.AefId) + + if (profile.Versions == nil) || (len(profile.Versions) < 1) { + err := errors.New("cannot read Versions") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, version := range profile.Versions { + log.Debugf("createKongInterfaceDescriptions, apiVersion \"%s\"", version.ApiVersion) + + if (profile.InterfaceDescriptions == nil) || (len(*profile.InterfaceDescriptions) < 1) { + err := errors.New("cannot read InterfaceDescriptions") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, interfaceDescription := range *profile.InterfaceDescriptions { + log.Debugf("createKongInterfaceDescriptions, Ipv4Addr %s", *interfaceDescription.Ipv4Addr) + log.Debugf("createKongInterfaceDescriptions, Port %d", *interfaceDescription.Port) + if uint(*interfaceDescription.Port) > 65535 { + err := errors.New("invalid Port") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + if interfaceDescription.SecurityMethods == nil { + err := errors.New("cannot read SecurityMethods") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, securityMethod := range *interfaceDescription.SecurityMethods { + log.Debugf("createKongInterfaceDescriptions, SecurityMethod %s", securityMethod) + + if (securityMethod != SecurityMethodOAUTH) && (securityMethod != SecurityMethodPKI) && (securityMethod != SecurityMethodPSK) { + msg := fmt.Sprintf("invalid SecurityMethod %s", securityMethod) + err := errors.New(msg) + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + } + + if (version.Resources == nil) || (len(*version.Resources) < 1) { + err := errors.New("cannot read Resources") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, resource := range *version.Resources { + var kongRouteUri string + kongRouteUri, statusCode, err = sd.createKongServiceRoutePrecheck(kongControlPlaneURL, client, interfaceDescription, resource, apfId, profile.AefId, version.ApiVersion) + if (err != nil) || (statusCode != http.StatusCreated) { + return statusCode, err + } + log.Debugf("createKongInterfaceDescriptions, kongRouteUri %s", kongRouteUri) + outputUris = append(outputUris, kongRouteUri) + log.Tracef("createKongInterfaceDescriptions, len(outputUris) %d", len(outputUris)) + log.Tracef("createKongInterfaceDescriptions, outputUris %v", outputUris) + } + } + } + } + + // Our list of returned resources has the new resource with the hash code and version number + m := 0 for i, profile := range profiles { - log.Debugf("createKongRoutes, AefId %s", profile.AefId) for j, version := range profile.Versions { - log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion) - for k, resource := range *version.Resources { - statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, &resource, apfId, profile.AefId, version.ApiVersion) - if (err != nil) || (statusCode != http.StatusCreated) { - return statusCode, err + var newResources []Resource + for range *profile.InterfaceDescriptions { + log.Tracef("createKongInterfaceDescriptions, range over *profile.InterfaceDescriptions") + for _, resource := range *version.Resources { + log.Tracef("createKongInterfaceDescriptions, m %d outputUris[m] %s", m, outputUris[m]) + resource.Uri = outputUris[m] + m = m + 1 + // Build a new list of resources with updated uris + newResources = append(newResources, resource) + log.Tracef("createKongInterfaceDescriptions, newResources %v", newResources) } - (*profiles[i].Versions[j].Resources)[k] = resource } + // Swap over to the new list of uris + *profiles[i].Versions[j].Resources = newResources + log.Tracef("createKongInterfaceDescriptions, assigned *profiles[i].Versions[j].Resources %v", *profiles[i].Versions[j].Resources) } } + log.Tracef("exiting createKongInterfaceDescriptions statusCode %d", statusCode) + return statusCode, nil } -func (sd *ServiceAPIDescription) createKongRoute( +func (sd *ServiceAPIDescription) createKongServiceRoutePrecheck( kongControlPlaneURL string, client *resty.Client, - resource *Resource, + interfaceDescription InterfaceDescription, + resource Resource, apfId string, aefId string, - apiVersion string ) (int, error) { - log.Trace("entering createKongRoute") + apiVersion string ) (string, int, error) { + log.Trace("entering createKongServiceRoutePrecheck") + log.Debugf("createKongServiceRoutePrecheck, aefId %s", aefId) - resourceName := resource.ResourceName - apiId := *sd.ApiId + if (resource.Operations == nil) || (len(*resource.Operations) < 1) { + err := errors.New("cannot read Resource.Operations") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName) - log.Debugf("createKongRoute, tags %s", tags) + log.Debugf("createKongServiceRoutePrecheck, resource.Uri %s", resource.Uri) + if resource.Uri == "" { + err := errors.New("cannot read Resource.Uri") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - serviceName := apiId + "_" + resourceName - routeName := serviceName + log.Debugf("createKongServiceRoutePrecheck, ResourceName %v", resource.ResourceName) + + if resource.ResourceName == "" { + err := errors.New("cannot read Resource.ResourceName") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - log.Debugf("createKongRoute, serviceName %s", serviceName) - log.Debugf("createKongRoute, routeName %s", routeName) - log.Debugf("createKongRoute, aefId %s", aefId) + if (resource.CommType != CommunicationTypeREQUESTRESPONSE) && (resource.CommType != CommunicationTypeSUBSCRIBENOTIFY) { + err := errors.New("invalid Resource.CommType") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } uri := insertVersion(apiVersion, resource.Uri) - log.Debugf("createKongRoute, uri %s", uri) + log.Debugf("createKongServiceRoutePrecheck, uri %s", uri) - // Create a url.Values map to hold the form data - data := url.Values{} - serviceUri := uri + kongRouteUri, statusCode, err := sd.createKongServiceRoute(kongControlPlaneURL, client, interfaceDescription, uri, apfId, aefId, apiVersion, resource) + if (err != nil) || ((statusCode != http.StatusCreated) ) { + // We carry on if we tried to create a duplicate service. We depend on Kong route matching. + return kongRouteUri, statusCode, err + } + + return kongRouteUri, statusCode, err +} + +func insertVersion(version string, route string) string { + versionedRoute := route + + if version != "" { + sep := "/" + n := 3 + + foundRegEx := false + if strings.HasPrefix(route, "~") { + log.Debug("insertVersion, found regex prefix") + foundRegEx = true + route = strings.TrimPrefix(route, "~") + } + + log.Debugf("insertVersion route %s", route) + split := strings.SplitAfterN(route, sep, n) + log.Debugf("insertVersion split %q", split) + + versionedRoute = split[0] + if len(split) == 2 { + versionedRoute = split[0] + split[1] + } else if len(split) > 2 { + versionedRoute = split[0] + split[1] + version + sep + split[2] + } + + if foundRegEx { + versionedRoute = "~" + versionedRoute + } + } + log.Debugf("insertVersion versionedRoute %s", versionedRoute) + + return versionedRoute +} +func (sd *ServiceAPIDescription) createKongServiceRoute( + kongControlPlaneURL string, + client *resty.Client, + interfaceDescription InterfaceDescription, + uri string, + apfId string, + aefId string, + apiVersion string, + resource Resource) (string, int, error) { + log.Tracef("entering createKongServiceRoute") + + var ( + statusCode int + err error + ) + + kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL) + if err != nil { + return "", http.StatusInternalServerError, err + } + log.Debugf("createKongServiceRoute, kongControlPlaneURL %s", kongControlPlaneURL) + log.Debugf("createKongServiceRoute, kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme) + + kongServiceUri := uri foundRegEx := false if strings.HasPrefix(uri, "~") { - log.Debug("createKongRoute, found regex prefix") + log.Debug("createKongServiceRoute, found regex prefix") foundRegEx = true - data.Set("strip_path", "false") - serviceUri = "/" + + // For our Kong Service path, we omit the leading ~ and take the path up to the regex, not including the '(' + kongServiceUri = uri[1:] + index := strings.Index(kongServiceUri, "(?") + if (index != -1 ) { + kongServiceUri = kongServiceUri[:index] + } else { + log.Errorf("createKongServiceRoute, regex characters '(?' not found in the regex %s", kongServiceUri) + return "", http.StatusBadRequest, err + } } else { - log.Debug("createKongRoute, no regex prefix found") - data.Set("strip_path", "true") + log.Debug("createKongServiceRoute, no regex prefix found") } - log.Debugf("createKongRoute, serviceUri %s", serviceUri) - log.Debugf("createKongRoute, strip_path %s", data.Get("strip_path")) + log.Debugf("createKongServiceRoute, kongServiceUri %s", kongServiceUri) - routeUri := prependUri(sd.ApiName, uri) - log.Debugf("createKongRoute, routeUri %s", routeUri) - resource.Uri = routeUri + ipv4Addr := *interfaceDescription.Ipv4Addr + port := *interfaceDescription.Port - statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, serviceUri, tags) - if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { - // We carry on if we tried to create a duplicate service. We depend on Kong route matching. - return statusCode, err + portAsInt := int(port) + interfaceDescriptionSeed := string(ipv4Addr) + strconv.Itoa(portAsInt) + interfaceDescUuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(interfaceDescriptionSeed)) + uriPrefix := "port-" + strconv.Itoa(portAsInt) + "-hash-" + interfaceDescUuid.String() + + resourceName := resource.ResourceName + + apiId := *sd.ApiId + kongServiceName := apiId + "-" + resourceName + kongServiceNamePrefix := kongServiceName + "-" + uriPrefix + + log.Debugf("createKongServiceRoute, kongServiceName %s", kongServiceName) + log.Debugf("createKongServiceRoute, kongServiceNamePrefix %s", kongServiceNamePrefix) + + tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName) + log.Debugf("createKongServiceRoute, tags %s", tags) + + kongServiceInfo := map[string]interface{}{ + "host": ipv4Addr, + "name": kongServiceNamePrefix, + "port": port, + "protocol": kongControlPlaneURLParsed.Scheme, + "path": kongServiceUri, + "tags": tags, + } + + // Kong admin API endpoint for creating a service + kongServicesURL := kongControlPlaneURL + "/services" + + // Make the POST request to create the Kong service + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(kongServiceInfo). + Post(kongServicesURL) + + // Check for errors in the request + if err != nil { + log.Errorf("createKongServiceRoute, Request Error: %v", err) + return "", http.StatusInternalServerError, err + } + + // Check the response status code + statusCode = resp.StatusCode() + if statusCode == http.StatusCreated { + log.Infof("kong service %s created successfully", kongServiceNamePrefix) + } else if resp.StatusCode() == http.StatusConflict { + log.Errorf("kong service already exists. Status code: %d", resp.StatusCode()) + err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service + statusCode = http.StatusForbidden // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API + } else { + err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode()) + } + if err != nil { + log.Errorf(err.Error()) + log.Errorf("response body: %s", resp.Body()) + return "", statusCode, err + } + + // Create matching route + routeName := kongServiceNamePrefix + kongRouteUri := uri + + kongRouteUri = prependUri(uriPrefix, kongRouteUri) + log.Debugf("createKongServiceRoute, kongRouteUri with uriPrefix %s", kongRouteUri) + + kongRouteUri = prependUri(sd.ApiName, kongRouteUri) + log.Debugf("createKongServiceRoute, kongRouteUri with apiName %s", kongRouteUri) + + kongRouteUri, statusCode, err = sd.createRouteForService(kongControlPlaneURL, client, resource, routeName, kongRouteUri, uri, tags, foundRegEx) + if err != nil { + log.Errorf(err.Error()) + return kongRouteUri, statusCode, err + } + + return kongRouteUri, statusCode, err +} + +func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string { + tagsMap := map[string]string{ + "apfId": apfId, + "aefId": aefId, + "apiId": apiId, + "apiVersion": apiVersion, + "resourceName": resourceName, + } + + // Convert the map to a slice of strings + var tagsSlice []string + for key, value := range tagsMap { + str := fmt.Sprintf("%s: %s", key, value) + tagsSlice = append(tagsSlice, str) + } + + return tagsSlice +} + +func prependUri(prependUri string, uri string) string { + if prependUri != "" { + trimmedUri := uri + foundRegEx := false + if strings.HasPrefix(uri, "~") { + log.Debug("prependUri, found regex prefix") + foundRegEx = true + trimmedUri = strings.TrimPrefix(uri, "~") + log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri) + } + + if prependUri[0] != '/' { + prependUri = "/" + prependUri + } + if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' { + prependUri = prependUri + "/" + } + uri = prependUri + trimmedUri + if foundRegEx { + uri = "~" + uri + } } + return uri +} + +func (sd *ServiceAPIDescription) createRouteForService( + kongControlPlaneURL string, + client *resty.Client, + resource Resource, + routeName string, + kongRouteUri string, + uri string, + tags []string, + foundRegEx bool) (string, int, error) { + + log.Debugf("createRouteForService, kongRouteUri %s", kongRouteUri) + // Create a url.Values map to hold the form data + data := url.Values{} + data.Set("strip_path", "true") + log.Debugf("createRouteForService, strip_path %s", data.Get("strip_path")) data.Set("name", routeName) - routeUriPaths := []string{routeUri} + routeUriPaths := []string{kongRouteUri} for _, path := range routeUriPaths { - log.Debugf("createKongRoute, path %s", path) + log.Debugf("createRouteForService, path %s", path) data.Add("paths", path) - } + } for _, tag := range tags { - log.Debugf("createKongRoute, tag %s", tag) + log.Debugf("createRouteForService, tag %s", tag) data.Add("tags", tag) - } + } for _, op := range *resource.Operations { - log.Debugf("createKongRoute, op %s", string(op)) + log.Debugf("createRouteForService, op %s", string(op)) data.Add("methods", string(op)) - } + } // Encode the data to application/x-www-form-urlencoded format encodedData := data.Encode() // Make the POST request to create the Kong service + serviceName := routeName kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes" resp, err := client.R(). SetHeader("Content-Type", "application/x-www-form-urlencoded"). @@ -177,8 +483,8 @@ func (sd *ServiceAPIDescription) createKongRoute( // Check for errors in the request if err != nil { - log.Debugf("createKongRoute POST Error: %v", err) - return resp.StatusCode(), err + log.Debugf("createRouteForService POST Error: %v", err) + return kongRouteUri, resp.StatusCode(), err } // Check the response status code @@ -187,7 +493,7 @@ func (sd *ServiceAPIDescription) createKongRoute( if (foundRegEx) { statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, uri) if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { - return statusCode, err + return kongRouteUri, statusCode, err } } } else { @@ -195,10 +501,10 @@ func (sd *ServiceAPIDescription) createKongRoute( err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode()) log.Error(err.Error()) log.Errorf("response body: %s", resp.Body()) - return resp.StatusCode(), err + return kongRouteUri, resp.StatusCode(), err } - return resp.StatusCode(), nil + return kongRouteUri, resp.StatusCode(), nil } func (sd *ServiceAPIDescription) createRequestTransformer( @@ -235,7 +541,7 @@ func (sd *ServiceAPIDescription) createRequestTransformer( // Check the response status code if resp.StatusCode() == http.StatusCreated { - log.Infof("kong request transformer created successfully for route %s", routeName) + log.Infof("kong request transformer for route %s created successfully", routeName) } else { log.Debugf("kongRequestTransformerURL %s", kongRequestTransformerURL) err = fmt.Errorf("error creating Kong request transformer. Status code: %d", resp.StatusCode()) @@ -286,168 +592,6 @@ func deriveTransformPattern(routePattern string) (string, error) { return transformPattern, nil } -func insertVersion(version string, route string) string { - versionedRoute := route - - if version != "" { - sep := "/" - n := 3 - - foundRegEx := false - if strings.HasPrefix(route, "~") { - log.Debug("insertVersion, found regex prefix") - foundRegEx = true - route = strings.TrimPrefix(route, "~") - } - - log.Debugf("insertVersion route %s", route) - split := strings.SplitAfterN(route, sep, n) - log.Debugf("insertVersion split %q", split) - - versionedRoute = split[0] - if len(split) == 2 { - versionedRoute = split[0] + split[1] - } else if len(split) > 2 { - versionedRoute = split[0] + split[1] + version + sep + split[2] - } - - if foundRegEx { - versionedRoute = "~" + versionedRoute - } - } - log.Debugf("insertVersion versionedRoute %s", versionedRoute) - - return versionedRoute -} - -func prependUri(prependUri string, uri string) string { - if prependUri != "" { - trimmedUri := uri - foundRegEx := false - if strings.HasPrefix(uri, "~") { - log.Debug("prependUri, found regex prefix") - foundRegEx = true - trimmedUri = strings.TrimPrefix(uri, "~") - log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri) - } - - if prependUri[0] != '/' { - prependUri = "/" + prependUri - } - if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' { - prependUri = prependUri + "/" - } - uri = prependUri + trimmedUri - if foundRegEx { - uri = "~" + uri - } - } - return uri -} - -func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string { - tagsMap := map[string]string{ - "apfId": apfId, - "aefId": aefId, - "apiId": apiId, - "apiVersion": apiVersion, - "resourceName": resourceName, - } - - // Convert the map to a slice of strings - var tagsSlice []string - for key, value := range tagsMap { - str := fmt.Sprintf("%s: %s", key, value) - tagsSlice = append(tagsSlice, str) - } - - return tagsSlice -} - -func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, tags []string) (int, error) { - log.Tracef("entering createKongService") - log.Tracef("createKongService, kongServiceName %s", kongServiceName) - - // Define the service information for Kong - firstAEFProfileIpv4Addr, firstAEFProfilePort, err := sd.findFirstAEFProfile() - if err != nil { - return http.StatusBadRequest, err - } - - kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL) - if err != nil { - return http.StatusInternalServerError, err - } - log.Debugf("kongControlPlaneURL %s", kongControlPlaneURL) - log.Debugf("kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme) - - kongServiceInfo := map[string]interface{}{ - "host": firstAEFProfileIpv4Addr, - "name": kongServiceName, - "port": firstAEFProfilePort, - "protocol": kongControlPlaneURLParsed.Scheme, - "path": kongServiceUri, - "tags": tags, - } - - // Kong admin API endpoint for creating a service - kongServicesURL := kongControlPlaneURL + "/services" - - // Create a new Resty client - client := resty.New() - - // Make the POST request to create the Kong service - resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(kongServiceInfo). - Post(kongServicesURL) - - // Check for errors in the request - if err != nil { - log.Errorf("create Kong Service Request Error: %v", err) - return http.StatusInternalServerError, err - } - - // Check the response status code - statusCode := resp.StatusCode() - if statusCode == http.StatusCreated { - log.Infof("kong service %s created successfully", kongServiceName) - } else if resp.StatusCode() == http.StatusConflict { - log.Errorf("kong service already exists. Status code: %d", resp.StatusCode()) - err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service - statusCode = http.StatusForbidden // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API - } else { - err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode()) - } - if err != nil { - log.Errorf(err.Error()) - log.Errorf("response body: %s", resp.Body()) - } - - return statusCode, err -} - -func (sd *ServiceAPIDescription) findFirstAEFProfile() (common29122.Ipv4Addr, common29122.Port, error) { - log.Tracef("entering findFirstAEFProfile") - var aefProfile AefProfile - if *sd.AefProfiles != nil { - aefProfile = (*sd.AefProfiles)[0] - } - if (*sd.AefProfiles == nil) || (aefProfile.InterfaceDescriptions == nil) { - err := errors.New("cannot read interfaceDescription") - log.Errorf(err.Error()) - return "", common29122.Port(0), err - } - - interfaceDescription := (*aefProfile.InterfaceDescriptions)[0] - firstIpv4Addr := *interfaceDescription.Ipv4Addr - firstPort := *interfaceDescription.Port - - log.Debugf("findFirstAEFProfile firstIpv4Addr %s firstPort %d", firstIpv4Addr, firstPort) - - return firstIpv4Addr, firstPort, nil -} - // Update our exposures to point to Kong by replacing in incoming interface description with Kong interface descriptions. func (sd *ServiceAPIDescription) updateInterfaceDescription(kongDataPlaneIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongDomain string) { log.Trace("updating InterfaceDescriptions") diff --git a/servicemanager/mockkong/kong_mock.go b/servicemanager/mockkong/kong_mock.go index c0a6dba..8191c03 100644 --- a/servicemanager/mockkong/kong_mock.go +++ b/servicemanager/mockkong/kong_mock.go @@ -85,6 +85,166 @@ func RegisterHandlers(e *echo.Echo) { return c.String(http.StatusCreated, string(body)) }) + e.POST("/services/api_id_apiName_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-id_helloworld-id-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-v1-id_helloworld-id-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-no-version_helloworld-no-version-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-no-version_helloworld-no-version-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1_app-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2_app-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-id-helloworld-id-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-v1-id-helloworld-id-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-no-version-helloworld-no-version-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-no-version-helloworld-no-version-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1-app-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2-app-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + e.POST("/services/api_id_apiName_helloworld-id/routes", func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { @@ -245,4 +405,4 @@ func RegisterHandlers(e *echo.Echo) { return c.NoContent(http.StatusNoContent) }) -} \ No newline at end of file +} -- 2.16.6 From 9192172a44bdabbf95f73171435212d72a3144f7 Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Mon, 19 Aug 2024 16:35:50 +0100 Subject: [PATCH 06/10] NONRTRIC-1005: ServiceManager README for dynamic URIs Change-Id: Ib050c5452fa5b39b6382306316de217891fa2da0 Signed-off-by: DenisGNoonan --- servicemanager/README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/servicemanager/README.md b/servicemanager/README.md index 5f04d2f..c322bae 100644 --- a/servicemanager/README.md +++ b/servicemanager/README.md @@ -145,4 +145,63 @@ The additional env file needs to exist in the sme/servicemanager folder so that ## Postman -A Postman collection has been included in this repo at sme/postman/ServiceManager.postman_collection.json. \ No newline at end of file +A Postman collection has been included in this repo at sme/postman/ServiceManager.postman_collection.json. + +## Interface Descriptions + +To distinguish between multiple interface descriptions, Service Manager prepends the port number and a hash code to the URL path. + +## Static and Dynamic Routes + +We can specify either static or dynamic routes. Static routing defines a route when there is a single route for traffic to reach a destination. Dynamic routing allows us to specify path parameters. In this config file, we specify path parameters using regular expressions. + +Kong uses the regex definition from the [Rust programming language](https://docs.rs/regex/latest/regex/) to specify the regular expression (regex) that describes the path parameters, [Kong regex](https://docs.konghq.com/gateway/latest/key-concepts/routes/#regular-expressions). + +An example of a static path is as follows. This is the straightforward case. + +```http + /rapps +``` + +An example of a dynamic path is + +```http + ~/rapps/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*) +``` + +Our dynamic path starts with a ~ character. In this example, we have a path parameter that is described by a regex capture group called rappId. The regex describes a word made of mixed-case alphanumeric characters optionally followed by one or more sets of a dash or underscore together with another word. + +When the Service Manager client calls a dynamic API, we call the URL without the '~'. Kong substitutes the path parameter according to the rules specified in the regex. Therefore, we can call the above example by using + +```http + /rapps/my-rApp-id +``` + +as the URL where my-rApp-id is the rApp id of in the rApp Manager. The name my-rApp-id has to match the regex shown above. + +It is required to name the capture group in this YAML config file. The capture group name is used by Service Manager when creating a Kong Request Transformer plugin. We can specify multiple capture groups in a URL if there are multiple path parameters in the API path. + +We create a Kong Request Transformer plugin with .data[].config.replace, as in the following example curl with abridged response. + +```sh +curl -X GET http://oran-nonrtric-kong-admin.nonrtric.svc.cluster.local:8001/plugins +``` + +```json +{ + "body": [], + "uri": "/rapps/$(uri_captures[\"rappId\"])", + "headers": [], + "querystring": [] +} +``` + +In our example, this allows Kong to match /rapps/my-rApp-id. + +The Service Manager uses the following regex to search and replace the YAML file regexes. + +```regex +/\(\?<([^>]+)>([^\/]+)/ +``` + +Please note that the example path, /rapps/my-rApp-id, is not terminated by a '/'. Service Manager adds a '/' for internal matching. This made the regex easier to develop. Service Manager will match on /rapps/my-rApp-id/ for this case. -- 2.16.6 From 508332e99569c9b6be12a34d60de3058a0717363 Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Thu, 22 Aug 2024 17:10:42 +0100 Subject: [PATCH 07/10] NONRTRIC-1005: ServiceManager variable names Issue-ID: NONRTRIC-1005 Change-Id: Ie252eefe7dafbecad9a539315fab38a6946c164a Signed-off-by: DenisGNoonan --- .../internal/publishserviceapi/typeupdate.go | 118 +++++++++++++++------ 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index 9480104..72a987d 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -144,13 +144,13 @@ func (sd *ServiceAPIDescription) createKongInterfaceDescriptions(kongControlPlan } for _, resource := range *version.Resources { - var kongRouteUri string - kongRouteUri, statusCode, err = sd.createKongServiceRoutePrecheck(kongControlPlaneURL, client, interfaceDescription, resource, apfId, profile.AefId, version.ApiVersion) + var specUri string + specUri, statusCode, err = sd.createKongServiceRoutePrecheck(kongControlPlaneURL, client, interfaceDescription, resource, apfId, profile.AefId, version.ApiVersion) if (err != nil) || (statusCode != http.StatusCreated) { return statusCode, err } - log.Debugf("createKongInterfaceDescriptions, kongRouteUri %s", kongRouteUri) - outputUris = append(outputUris, kongRouteUri) + log.Debugf("createKongInterfaceDescriptions, specUri %s", specUri) + outputUris = append(outputUris, specUri) log.Tracef("createKongInterfaceDescriptions, len(outputUris) %d", len(outputUris)) log.Tracef("createKongInterfaceDescriptions, outputUris %v", outputUris) } @@ -222,16 +222,16 @@ func (sd *ServiceAPIDescription) createKongServiceRoutePrecheck( return "", http.StatusBadRequest, err } - uri := insertVersion(apiVersion, resource.Uri) - log.Debugf("createKongServiceRoutePrecheck, uri %s", uri) + specUri := resource.Uri + kongRegexUri, _ := deriveKongPattern(resource.Uri) - kongRouteUri, statusCode, err := sd.createKongServiceRoute(kongControlPlaneURL, client, interfaceDescription, uri, apfId, aefId, apiVersion, resource) + specUri, statusCode, err := sd.createKongServiceRoute(kongControlPlaneURL, client, interfaceDescription, kongRegexUri, specUri, apfId, aefId, apiVersion, resource) if (err != nil) || ((statusCode != http.StatusCreated) ) { // We carry on if we tried to create a duplicate service. We depend on Kong route matching. - return kongRouteUri, statusCode, err + return specUri, statusCode, err } - return kongRouteUri, statusCode, err + return specUri, statusCode, err } func insertVersion(version string, route string) string { @@ -272,7 +272,8 @@ func (sd *ServiceAPIDescription) createKongServiceRoute( kongControlPlaneURL string, client *resty.Client, interfaceDescription InterfaceDescription, - uri string, + kongRegexUri string, + specUri string, apfId string, aefId string, apiVersion string, @@ -291,14 +292,21 @@ func (sd *ServiceAPIDescription) createKongServiceRoute( log.Debugf("createKongServiceRoute, kongControlPlaneURL %s", kongControlPlaneURL) log.Debugf("createKongServiceRoute, kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme) - kongServiceUri := uri - foundRegEx := false - if strings.HasPrefix(uri, "~") { + log.Debugf("createKongServiceRoute, kongRegexUri %s", kongRegexUri) + log.Debugf("createKongServiceRoute, specUri %s", specUri) + + kongRegexUri = insertVersion(apiVersion, kongRegexUri) + kongServiceUri := kongRegexUri + log.Debugf("createKongServiceRoute, kongServiceUri after insertVersion, %s", kongServiceUri) + + specUri = insertVersion(apiVersion, specUri) + log.Debugf("createKongServiceRoute, specUri after insertVersion, %s", specUri) + + if strings.HasPrefix(kongServiceUri, "~") { log.Debug("createKongServiceRoute, found regex prefix") - foundRegEx = true // For our Kong Service path, we omit the leading ~ and take the path up to the regex, not including the '(' - kongServiceUri = uri[1:] + kongServiceUri = kongServiceUri[1:] index := strings.Index(kongServiceUri, "(?") if (index != -1 ) { kongServiceUri = kongServiceUri[:index] @@ -309,8 +317,7 @@ func (sd *ServiceAPIDescription) createKongServiceRoute( } else { log.Debug("createKongServiceRoute, no regex prefix found") } - - log.Debugf("createKongServiceRoute, kongServiceUri %s", kongServiceUri) + log.Debugf("createKongServiceRoute, kongServiceUri, path up to regex %s", kongServiceUri) ipv4Addr := *interfaceDescription.Ipv4Addr port := *interfaceDescription.Port @@ -375,21 +382,26 @@ func (sd *ServiceAPIDescription) createKongServiceRoute( // Create matching route routeName := kongServiceNamePrefix - kongRouteUri := uri - kongRouteUri = prependUri(uriPrefix, kongRouteUri) + kongRouteUri := prependUri(uriPrefix, kongRegexUri) log.Debugf("createKongServiceRoute, kongRouteUri with uriPrefix %s", kongRouteUri) kongRouteUri = prependUri(sd.ApiName, kongRouteUri) log.Debugf("createKongServiceRoute, kongRouteUri with apiName %s", kongRouteUri) - kongRouteUri, statusCode, err = sd.createRouteForService(kongControlPlaneURL, client, resource, routeName, kongRouteUri, uri, tags, foundRegEx) + specUri = prependUri(uriPrefix, specUri) + log.Debugf("createKongServiceRoute, specUri with uriPrefix %s", specUri) + + specUri = prependUri(sd.ApiName, specUri) + log.Debugf("createKongServiceRoute, specUri with apiName %s", specUri) + + statusCode, err = sd.createRouteForService(kongControlPlaneURL, client, resource, routeName, kongRouteUri, kongRegexUri, tags) if err != nil { log.Errorf(err.Error()) return kongRouteUri, statusCode, err } - return kongRouteUri, statusCode, err + return specUri, statusCode, err } func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string { @@ -442,9 +454,8 @@ func (sd *ServiceAPIDescription) createRouteForService( resource Resource, routeName string, kongRouteUri string, - uri string, - tags []string, - foundRegEx bool) (string, int, error) { + kongRegexUri string, + tags []string) (int, error) { log.Debugf("createRouteForService, kongRouteUri %s", kongRouteUri) @@ -484,27 +495,35 @@ func (sd *ServiceAPIDescription) createRouteForService( // Check for errors in the request if err != nil { log.Debugf("createRouteForService POST Error: %v", err) - return kongRouteUri, resp.StatusCode(), err + return resp.StatusCode(), err } // Check the response status code if resp.StatusCode() == http.StatusCreated { log.Infof("kong route %s created successfully", routeName) - if (foundRegEx) { - statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, uri) + + index := strings.Index(kongRegexUri, "(?") + if index != -1 { + log.Debugf("createRouteForService, found regex in %s", kongRegexUri) + requestTransformerUri := strings.TrimPrefix(kongRegexUri, "~") + log.Debugf("createRouteForService, requestTransformerUri %s", requestTransformerUri) + + statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, requestTransformerUri) if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { - return kongRouteUri, statusCode, err + return statusCode, err } + } else { + log.Debug("createRouteForService, no variable name found") } } else { log.Debugf("kongRoutesURL %s", kongRoutesURL) err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode()) log.Error(err.Error()) log.Errorf("response body: %s", resp.Body()) - return kongRouteUri, resp.StatusCode(), err + return resp.StatusCode(), err } - return kongRouteUri, resp.StatusCode(), nil + return resp.StatusCode(), nil } func (sd *ServiceAPIDescription) createRequestTransformer( @@ -553,15 +572,46 @@ func (sd *ServiceAPIDescription) createRequestTransformer( return resp.StatusCode(), nil } +// Function to derive the transform pattern from the route pattern +func deriveKongPattern(routePattern string) (string, error) { + log.Trace("entering deriveKongPattern") + log.Debugf("deriveKongPattern routePattern %s", routePattern) + + // Regular expression to match variable names + re := regexp.MustCompile(`\{([a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)\}`) + log.Debugf("deriveKongPattern MustCompile %v", re) + + // Find all matches in the route pattern + matches := re.FindAllStringSubmatch(routePattern, -1) + log.Debugf("deriveKongPattern FindAllStringSubmatch %v", re) + + transformPattern := routePattern + for _, match := range matches { + // match[0] is the full match with braces + // match[1] is the uri variable name + log.Debugf("deriveKongPattern match %v", match) + log.Debugf("deriveKongPattern match[0] %v", match[0]) + log.Debugf("deriveKongPattern match[1] %v", match[1]) + placeholder := fmt.Sprintf("(?<%s>[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", match[1]) + // Replace the variable with the Kong regex placeholder + transformPattern = strings.Replace(transformPattern, match[0], placeholder, 1) + } + log.Debugf("deriveKongPattern transformPattern %s", transformPattern) + + if len(matches) != 0 { + transformPattern = "~" + transformPattern + log.Debugf("deriveKongPattern transformPattern with prefix %s", transformPattern) + } + + return transformPattern, nil +} + + // Function to derive the transform pattern from the route pattern func deriveTransformPattern(routePattern string) (string, error) { log.Trace("entering deriveTransformPattern") - log.Debugf("deriveTransformPattern routePattern %s", routePattern) - routePattern = strings.TrimPrefix(routePattern, "~") - log.Debugf("deriveTransformPattern, TrimPrefix trimmed routePattern %s", routePattern) - // Append a slash to handle an edge case for matching a trailing capture group. appendedSlash := false if routePattern[len(routePattern)-1] != '/' { -- 2.16.6 From 015c3d23c73b38feaff7e6d3c9c45cd4db1596fe Mon Sep 17 00:00:00 2001 From: "aravind.est" Date: Thu, 19 Sep 2024 19:26:14 +0100 Subject: [PATCH 08/10] Fix the request transformer regex Request transformer regex fixed to match the continuous path parameters. Issue-ID: NONRTRIC-1031 Change-Id: I6b7f0f2ffcfead5fc6f9541077cc262323813a5f Signed-off-by: aravind.est --- servicemanager/internal/publishserviceapi/typeupdate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index 72a987d..ac84c0d 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -621,14 +621,14 @@ func deriveTransformPattern(routePattern string) (string, error) { } // Regular expression to match named capture groups - re := regexp.MustCompile(`/\(\?<([^>]+)>([^\/]+)/`) + re := regexp.MustCompile(`\(\?<([^>]+)>([^\/]+)`) // Find all matches in the route pattern matches := re.FindAllStringSubmatch(routePattern, -1) transformPattern := routePattern for _, match := range matches { // match[0] is the full match, match[1] is the capture group name, match[2] is the pattern - placeholder := fmt.Sprintf("/$(uri_captures[\"%s\"])/", match[1]) + placeholder := fmt.Sprintf("$(uri_captures[\"%s\"])", match[1]) // Replace the capture group with the corresponding placeholder transformPattern = strings.Replace(transformPattern, match[0], placeholder, 1) } -- 2.16.6 From 7ed019ee77ac56c4933a385eb93ab71d71fa1a7d Mon Sep 17 00:00:00 2001 From: "aravind.est" Date: Tue, 24 Sep 2024 14:28:41 +0100 Subject: [PATCH 09/10] NONRTRIC SME Fix sonar build issues this fixes the sonar prescan script failure in jenkins and github Issue-ID: NONRTRIC-1033 Change-Id: I1f0995077746af1e986924f60126085aa11001f8 Signed-off-by: aravind.est --- capifcore/build-capifcore-ubuntu.sh | 5 +++-- servicemanager/build-servicemanager-ubuntu.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/capifcore/build-capifcore-ubuntu.sh b/capifcore/build-capifcore-ubuntu.sh index 9bac724..934341f 100755 --- a/capifcore/build-capifcore-ubuntu.sh +++ b/capifcore/build-capifcore-ubuntu.sh @@ -1,7 +1,8 @@ #!/bin/bash ############################################################################## # -# Copyright (C) 2022: Nordix Foundation +# Copyright (C) 2022-2023: Nordix Foundation +# Copyright (C) 2023-2024: OpenInfra Foundation Europe # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -28,7 +29,7 @@ go version cd capifcore/ # install the go coverage tool helper -go install github.com/ory/go-acc +go install github.com/ory/go-acc@v0.2.8 go get github.com/stretchr/testify/mock@v1.7.1 diff --git a/servicemanager/build-servicemanager-ubuntu.sh b/servicemanager/build-servicemanager-ubuntu.sh index 963e3aa..f4a00d8 100755 --- a/servicemanager/build-servicemanager-ubuntu.sh +++ b/servicemanager/build-servicemanager-ubuntu.sh @@ -27,7 +27,7 @@ export GO111MODULE=on go version # Get the go coverage tool helper -go install github.com/ory/go-acc +go install github.com/ory/go-acc@v0.2.8 go get github.com/stretchr/testify/mock@v1.7.1 go-acc ./... --ignore mockkong,common,discoverserviceapi,invokermanagementapi,publishserviceapi,providermanagementapi -- 2.16.6 From 60a278f14b26889559388363b83b359ddb09dd30 Mon Sep 17 00:00:00 2001 From: "aravind.est" Date: Fri, 27 Sep 2024 11:22:16 +0100 Subject: [PATCH 10/10] NONRTRIC SME fix servicemanager sonar Servicemanager sonar job missing the go.mod and it is fixed. Issue-ID: NONRTRIC-1033 Change-Id: I86b3b74c1a34a9b2b7cc263ab545b939012350f1 Signed-off-by: aravind.est --- servicemanager/build-servicemanager-ubuntu.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/servicemanager/build-servicemanager-ubuntu.sh b/servicemanager/build-servicemanager-ubuntu.sh index f4a00d8..bcc6481 100755 --- a/servicemanager/build-servicemanager-ubuntu.sh +++ b/servicemanager/build-servicemanager-ubuntu.sh @@ -25,6 +25,7 @@ echo "--> build-servicemanager-ubuntu.sh" export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin export GO111MODULE=on go version +cd servicemanager/ # Get the go coverage tool helper go install github.com/ory/go-acc@v0.2.8 -- 2.16.6