From c99b6b4342fcf8ea70772dc57fcb04c5fdbe4ac8 Mon Sep 17 00:00:00 2001 From: DenisGNoonan Date: Tue, 16 Jul 2024 16:42:54 +0100 Subject: [PATCH] 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