NONRTRIC-1005: Support for regex capture groups
Issue-ID: NONRTRIC-1005
Change-Id: I4f32d68649c840701a37429d1c956f9b45bcc603
Signed-off-by: DenisGNoonan <denis.noonan@est.tech>
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_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>
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
assert.NoError(t, err, "error unmarshaling response")
assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription")
+}
- newServiceDescription = getServiceAPIDescription(aefId, apiName, description, testServiceIpv4, testServicePort)
+
+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, 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 @@
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)
- // Check if the parsed array is empty
- assert.Zero(t, len(resultServices))
+ 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)
+
capifCleanUp()
}
@@ -449,7 +651,16 @@
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 @@
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 @@
"fmt"
"net/http"
"net/url"
+ "regexp"
"strings"
resty "github.com/go-resty/resty/v2"
@@ -115,33 +116,63 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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