NONRTRIC-1005: Support for regex capture groups 82/13182/8
authorDenisGNoonan <denis.noonan@est.tech>
Tue, 16 Jul 2024 15:42:54 +0000 (16:42 +0100)
committerDenisGNoonan <denis.noonan@est.tech>
Wed, 24 Jul 2024 17:29:13 +0000 (18:29 +0100)
Issue-ID: NONRTRIC-1005
Change-Id: I4f32d68649c840701a37429d1c956f9b45bcc603
Signed-off-by: DenisGNoonan <denis.noonan@est.tech>
servicemanager/.env.example
servicemanager/internal/envreader/envreader.go
servicemanager/internal/publishservice/publishservice_test.go
servicemanager/internal/publishserviceapi/typeupdate.go
servicemanager/main.go
servicemanager/mockkong/kong_mock.go

index 1d3c4ef..c5d5b32 100644 (file)
@@ -8,7 +8,7 @@ KONG_CONTROL_PLANE_PORT=<port number>
 KONG_DATA_PLANE_IPV4=<host string>
 KONG_DATA_PLANE_PORT=<port number>
 CAPIF_PROTOCOL=<http or https protocol scheme>
-CAPIF_IPV4=<host>
+CAPIF_IPV4=<host string>
 CAPIF_PORT=<port number>
 LOG_LEVEL=<Trace, Debug, Info, Warning, Error, Fatal or Panic>
 SERVICE_MANAGER_PORT=<port number>
index f08b1d7..315772e 100644 (file)
@@ -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 == "<string>" {
+               err = fmt.Errorf("error loading KONG_DOMAIN from .env file: %s", kongDomain)
+       } else if kongProtocol == "" || kongProtocol == "<http or https protocol scheme>" {
+               err = fmt.Errorf("error loading KONG_PROTOCOL from .env file: %s", kongProtocol)
+       } else if kongControlPlaneIPv4 == "" || kongControlPlaneIPv4 == "<host string>" {
+               err = fmt.Errorf("error loading KONG_CONTROL_PLANE_IPV4 from .env file: %s", kongControlPlaneIPv4)
+       } else if kongDataPlaneIPv4 == "" || kongDataPlaneIPv4 == "<host string>" {
+               err = fmt.Errorf("error loading KONG_DATA_PLANE_IPV4 from .env file: %s", kongDataPlaneIPv4)
+       } else if capifProtocol == "" || capifProtocol == "<http or https protocol scheme>" {
+               err = fmt.Errorf("error loading CAPIF_PROTOCOL from .env file: %s", capifProtocol)
+       } else if capifIPv4 == "" || capifIPv4 == "<host string>" || capifIPv4 == "<host>" {
+               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
                }
        }
index c42cba7..31d5293 100644 (file)
@@ -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/(?<helloworld-id>[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/(?<helloworld-id>[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/(?<helloworld-id>[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/(?<helloworld-id>[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,
                                                        },
                                                },
                                        },
index 3b46a0f..5ecc7e5 100644 (file)
@@ -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
 }
index b90782c..d0fbe4f 100644 (file)
@@ -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
        }
 
index 14acd57..c0a6dba 100644 (file)
@@ -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