From: DenisGNoonan Date: Tue, 6 Aug 2024 15:50:42 +0000 (+0100) Subject: NONRTRIC-1005: Add multiple Interface descriptions X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=6bb6446fd84a7cf225a20cf64b0adc7e8085f81b;p=nonrtric%2Fplt%2Fsme.git NONRTRIC-1005: Add multiple Interface descriptions Change-Id: I2a0e208a05d04cb4acc5bc50932925a80cecf3f5 Signed-off-by: DenisGNoonan --- diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 3e47269..b8b51d0 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -17,10 +17,15 @@ Version history SME Service Manager | **Date** | **Ver.** | **Author** | **Comment** | | | | | | +------------+----------+------------------+--------------------------------------+ -| 2024-06-26 | 0.1.2 | | J Release (Service Manager) | +| 2024-06-26 | 0.1.2 | Denis G Noonan | J Release (Service Manager) | | | | | Initial version of Service Manager | | | | | | +------------+----------+------------------+--------------------------------------+ +| TBA | TBA | Denis G Noonan | Service Manager includes support for | +| | | | dynamic URIs, multiple interface | +| | | | descriptions, and improved schema | +| | | | validation. | ++------------+----------+------------------+--------------------------------------+ Version history SME CAPIFCore ============================= diff --git a/servicemanager/internal/discoverservice/discoverservice_test.go b/servicemanager/internal/discoverservice/discoverservice_test.go index ab6ae85..3b6fa81 100644 --- a/servicemanager/internal/discoverservice/discoverservice_test.go +++ b/servicemanager/internal/discoverservice/discoverservice_test.go @@ -731,16 +731,14 @@ func registerHandlers(e *echo.Echo, myEnv map[string]string, myPorts map[string] func getServiceAPIDescription(aefId string, apiName string, apiCategory string, apiVersion string, protocol *publishapi.Protocol, dataFormat *publishapi.DataFormat, description string, testServiceIpv4 common29122.Ipv4Addr, testServicePort common29122.Port, commType publishapi.CommunicationType) publishapi.ServiceAPIDescription { domainName := "Kong" otherDomainName := "otherDomain" - var otherProtocol publishapi.Protocol = "HTTP_2" + var DataFormatOther publishapi.DataFormat = "OTHER" categoryPointer := &apiCategory if apiCategory == "" { categoryPointer = nil } - var DataFormatOther publishapi.DataFormat = "OTHER" - return publishapi.ServiceAPIDescription{ AefProfiles: &[]publishapi.AefProfile{ { @@ -775,6 +773,15 @@ func getServiceAPIDescription(aefId string, apiName string, apiCategory string, }, { AefId: aefId, // "otherAefId" + InterfaceDescriptions: &[]publishapi.InterfaceDescription{ + { + Ipv4Addr: &testServiceIpv4, + Port: &testServicePort, + SecurityMethods: &[]publishapi.SecurityMethod{ + "PSK", + }, + }, + }, DomainName: &otherDomainName, Protocol: &otherProtocol, DataFormat: &DataFormatOther, diff --git a/servicemanager/internal/publishservice/publishservice.go b/servicemanager/internal/publishservice/publishservice.go index 4031827..d042cd0 100644 --- a/servicemanager/internal/publishservice/publishservice.go +++ b/servicemanager/internal/publishservice/publishservice.go @@ -103,10 +103,18 @@ func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) e ps.KongDataPlaneIPv4, ps.KongDataPlanePort, apfId) - if (err != nil) || (statusCode != http.StatusCreated) { - // We can return with http.StatusForbidden if there is a http.StatusConflict detected by Kong + + log.Trace("After RegisterKong") + + if err != nil { msg := err.Error() - log.Errorf("error on RegisterKong %s", msg) + log.Errorf("PostApfIdServiceApis, error on RegisterKong %s", msg) + return sendCoreError(ctx, statusCode, msg) + } + if statusCode != http.StatusCreated { + // We can return with http.StatusForbidden if there is a http.StatusConflict detected by Kong + msg := "error detected by Kong" + log.Errorf(msg) return sendCoreError(ctx, statusCode, msg) } diff --git a/servicemanager/internal/publishservice/publishservice_test.go b/servicemanager/internal/publishservice/publishservice_test.go index 31d5293..eb33c38 100644 --- a/servicemanager/internal/publishservice/publishservice_test.go +++ b/servicemanager/internal/publishservice/publishservice_test.go @@ -352,7 +352,7 @@ func TestPublishUnpublishServiceMissingInterface(t *testing.T) { err = result.UnmarshalJsonToObject(&resultError) assert.NoError(t, err, "error unmarshaling response") - assert.Contains(t, *resultError.Cause, "cannot read interfaceDescription") + assert.Contains(t, *resultError.Cause, "cannot read InterfaceDescriptions") } @@ -434,7 +434,7 @@ func TestPublishUnpublishWithoutVersionId(t *testing.T) { var operation publishapi.Operation = "GET" assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld", resource.ResourceName) - assert.Equal(t, "/helloworld-v1/helloworld", resource.Uri) + assert.Equal(t, "/helloworld-v1/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld", resource.Uri) } func TestPublishUnpublishVersionId(t *testing.T) { @@ -516,7 +516,7 @@ func TestPublishUnpublishVersionId(t *testing.T) { assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld-id", resource.ResourceName) - assert.Equal(t, "~/helloworld-v1-id/helloworld/v1/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) + assert.Equal(t, "~/helloworld-v1-id/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld/v1/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) } func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) { @@ -598,7 +598,7 @@ func TestPublishUnpublishServiceNoVersionWithId(t *testing.T) { assert.Equal(t, operation, (*resource.Operations)[0]) assert.Equal(t, "helloworld-no-version", resource.ResourceName) - assert.Equal(t, "~/helloworld-no-version/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) + assert.Equal(t, "~/helloworld-no-version/port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/helloworld/(?[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*)", resource.Uri) capifCleanUp() } diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go index 5ecc7e5..9480104 100644 --- a/servicemanager/internal/publishserviceapi/typeupdate.go +++ b/servicemanager/internal/publishserviceapi/typeupdate.go @@ -26,9 +26,11 @@ import ( "net/http" "net/url" "regexp" + "strconv" "strings" resty "github.com/go-resty/resty/v2" + "github.com/google/uuid" log "github.com/sirupsen/logrus" common29122 "oransc.org/nonrtric/servicemanager/internal/common29122" @@ -57,7 +59,7 @@ func (sd *ServiceAPIDescription) RegisterKong( ) kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongControlPlaneIPv4, kongControlPlanePort) - statusCode, err = sd.createKongRoutes(kongControlPlaneURL, apfId) + statusCode, err = sd.createKongInterfaceDescriptions(kongControlPlaneURL, apfId) if (err != nil) || (statusCode != http.StatusCreated) { return statusCode, err } @@ -68,107 +70,411 @@ func (sd *ServiceAPIDescription) RegisterKong( return statusCode, nil } -func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, apfId string) (int, error) { - log.Trace("entering createKongRoutes") +func (sd *ServiceAPIDescription) createKongInterfaceDescriptions(kongControlPlaneURL string, apfId string) (int, error) { + log.Trace("entering createKongInterfaceDescriptions") + var ( statusCode int err error ) - client := resty.New() + outputUris := []string{} + + if sd == nil { + err = errors.New("cannot read ServiceAPIDescription") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + if (sd.AefProfiles == nil) || (len(*sd.AefProfiles) < 1) { + err = errors.New("cannot read AefProfiles") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } profiles := *sd.AefProfiles + for _, profile := range profiles { + log.Debugf("createKongInterfaceDescriptions, AefId %s", profile.AefId) + + if (profile.Versions == nil) || (len(profile.Versions) < 1) { + err := errors.New("cannot read Versions") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, version := range profile.Versions { + log.Debugf("createKongInterfaceDescriptions, apiVersion \"%s\"", version.ApiVersion) + + if (profile.InterfaceDescriptions == nil) || (len(*profile.InterfaceDescriptions) < 1) { + err := errors.New("cannot read InterfaceDescriptions") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, interfaceDescription := range *profile.InterfaceDescriptions { + log.Debugf("createKongInterfaceDescriptions, Ipv4Addr %s", *interfaceDescription.Ipv4Addr) + log.Debugf("createKongInterfaceDescriptions, Port %d", *interfaceDescription.Port) + if uint(*interfaceDescription.Port) > 65535 { + err := errors.New("invalid Port") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + if interfaceDescription.SecurityMethods == nil { + err := errors.New("cannot read SecurityMethods") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, securityMethod := range *interfaceDescription.SecurityMethods { + log.Debugf("createKongInterfaceDescriptions, SecurityMethod %s", securityMethod) + + if (securityMethod != SecurityMethodOAUTH) && (securityMethod != SecurityMethodPKI) && (securityMethod != SecurityMethodPSK) { + msg := fmt.Sprintf("invalid SecurityMethod %s", securityMethod) + err := errors.New(msg) + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + } + + if (version.Resources == nil) || (len(*version.Resources) < 1) { + err := errors.New("cannot read Resources") + log.Errorf(err.Error()) + return http.StatusBadRequest, err + } + + for _, resource := range *version.Resources { + var kongRouteUri string + kongRouteUri, statusCode, err = sd.createKongServiceRoutePrecheck(kongControlPlaneURL, client, interfaceDescription, resource, apfId, profile.AefId, version.ApiVersion) + if (err != nil) || (statusCode != http.StatusCreated) { + return statusCode, err + } + log.Debugf("createKongInterfaceDescriptions, kongRouteUri %s", kongRouteUri) + outputUris = append(outputUris, kongRouteUri) + log.Tracef("createKongInterfaceDescriptions, len(outputUris) %d", len(outputUris)) + log.Tracef("createKongInterfaceDescriptions, outputUris %v", outputUris) + } + } + } + } + + // Our list of returned resources has the new resource with the hash code and version number + m := 0 for i, profile := range profiles { - log.Debugf("createKongRoutes, AefId %s", profile.AefId) for j, version := range profile.Versions { - log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion) - for k, resource := range *version.Resources { - statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, &resource, apfId, profile.AefId, version.ApiVersion) - if (err != nil) || (statusCode != http.StatusCreated) { - return statusCode, err + var newResources []Resource + for range *profile.InterfaceDescriptions { + log.Tracef("createKongInterfaceDescriptions, range over *profile.InterfaceDescriptions") + for _, resource := range *version.Resources { + log.Tracef("createKongInterfaceDescriptions, m %d outputUris[m] %s", m, outputUris[m]) + resource.Uri = outputUris[m] + m = m + 1 + // Build a new list of resources with updated uris + newResources = append(newResources, resource) + log.Tracef("createKongInterfaceDescriptions, newResources %v", newResources) } - (*profiles[i].Versions[j].Resources)[k] = resource } + // Swap over to the new list of uris + *profiles[i].Versions[j].Resources = newResources + log.Tracef("createKongInterfaceDescriptions, assigned *profiles[i].Versions[j].Resources %v", *profiles[i].Versions[j].Resources) } } + log.Tracef("exiting createKongInterfaceDescriptions statusCode %d", statusCode) + return statusCode, nil } -func (sd *ServiceAPIDescription) createKongRoute( +func (sd *ServiceAPIDescription) createKongServiceRoutePrecheck( kongControlPlaneURL string, client *resty.Client, - resource *Resource, + interfaceDescription InterfaceDescription, + resource Resource, apfId string, aefId string, - apiVersion string ) (int, error) { - log.Trace("entering createKongRoute") + apiVersion string ) (string, int, error) { + log.Trace("entering createKongServiceRoutePrecheck") + log.Debugf("createKongServiceRoutePrecheck, aefId %s", aefId) - resourceName := resource.ResourceName - apiId := *sd.ApiId + if (resource.Operations == nil) || (len(*resource.Operations) < 1) { + err := errors.New("cannot read Resource.Operations") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName) - log.Debugf("createKongRoute, tags %s", tags) + log.Debugf("createKongServiceRoutePrecheck, resource.Uri %s", resource.Uri) + if resource.Uri == "" { + err := errors.New("cannot read Resource.Uri") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - serviceName := apiId + "_" + resourceName - routeName := serviceName + log.Debugf("createKongServiceRoutePrecheck, ResourceName %v", resource.ResourceName) + + if resource.ResourceName == "" { + err := errors.New("cannot read Resource.ResourceName") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } - log.Debugf("createKongRoute, serviceName %s", serviceName) - log.Debugf("createKongRoute, routeName %s", routeName) - log.Debugf("createKongRoute, aefId %s", aefId) + if (resource.CommType != CommunicationTypeREQUESTRESPONSE) && (resource.CommType != CommunicationTypeSUBSCRIBENOTIFY) { + err := errors.New("invalid Resource.CommType") + log.Errorf(err.Error()) + return "", http.StatusBadRequest, err + } uri := insertVersion(apiVersion, resource.Uri) - log.Debugf("createKongRoute, uri %s", uri) + log.Debugf("createKongServiceRoutePrecheck, uri %s", uri) - // Create a url.Values map to hold the form data - data := url.Values{} - serviceUri := uri + kongRouteUri, statusCode, err := sd.createKongServiceRoute(kongControlPlaneURL, client, interfaceDescription, uri, apfId, aefId, apiVersion, resource) + if (err != nil) || ((statusCode != http.StatusCreated) ) { + // We carry on if we tried to create a duplicate service. We depend on Kong route matching. + return kongRouteUri, statusCode, err + } + + return kongRouteUri, statusCode, err +} + +func insertVersion(version string, route string) string { + versionedRoute := route + + if version != "" { + sep := "/" + n := 3 + + foundRegEx := false + if strings.HasPrefix(route, "~") { + log.Debug("insertVersion, found regex prefix") + foundRegEx = true + route = strings.TrimPrefix(route, "~") + } + + log.Debugf("insertVersion route %s", route) + split := strings.SplitAfterN(route, sep, n) + log.Debugf("insertVersion split %q", split) + + versionedRoute = split[0] + if len(split) == 2 { + versionedRoute = split[0] + split[1] + } else if len(split) > 2 { + versionedRoute = split[0] + split[1] + version + sep + split[2] + } + + if foundRegEx { + versionedRoute = "~" + versionedRoute + } + } + log.Debugf("insertVersion versionedRoute %s", versionedRoute) + + return versionedRoute +} +func (sd *ServiceAPIDescription) createKongServiceRoute( + kongControlPlaneURL string, + client *resty.Client, + interfaceDescription InterfaceDescription, + uri string, + apfId string, + aefId string, + apiVersion string, + resource Resource) (string, int, error) { + log.Tracef("entering createKongServiceRoute") + + var ( + statusCode int + err error + ) + + kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL) + if err != nil { + return "", http.StatusInternalServerError, err + } + log.Debugf("createKongServiceRoute, kongControlPlaneURL %s", kongControlPlaneURL) + log.Debugf("createKongServiceRoute, kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme) + + kongServiceUri := uri foundRegEx := false if strings.HasPrefix(uri, "~") { - log.Debug("createKongRoute, found regex prefix") + log.Debug("createKongServiceRoute, found regex prefix") foundRegEx = true - data.Set("strip_path", "false") - serviceUri = "/" + + // For our Kong Service path, we omit the leading ~ and take the path up to the regex, not including the '(' + kongServiceUri = uri[1:] + index := strings.Index(kongServiceUri, "(?") + if (index != -1 ) { + kongServiceUri = kongServiceUri[:index] + } else { + log.Errorf("createKongServiceRoute, regex characters '(?' not found in the regex %s", kongServiceUri) + return "", http.StatusBadRequest, err + } } else { - log.Debug("createKongRoute, no regex prefix found") - data.Set("strip_path", "true") + log.Debug("createKongServiceRoute, no regex prefix found") } - log.Debugf("createKongRoute, serviceUri %s", serviceUri) - log.Debugf("createKongRoute, strip_path %s", data.Get("strip_path")) + log.Debugf("createKongServiceRoute, kongServiceUri %s", kongServiceUri) - routeUri := prependUri(sd.ApiName, uri) - log.Debugf("createKongRoute, routeUri %s", routeUri) - resource.Uri = routeUri + ipv4Addr := *interfaceDescription.Ipv4Addr + port := *interfaceDescription.Port - statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, serviceUri, tags) - if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { - // We carry on if we tried to create a duplicate service. We depend on Kong route matching. - return statusCode, err + portAsInt := int(port) + interfaceDescriptionSeed := string(ipv4Addr) + strconv.Itoa(portAsInt) + interfaceDescUuid := uuid.NewSHA1(uuid.NameSpaceURL, []byte(interfaceDescriptionSeed)) + uriPrefix := "port-" + strconv.Itoa(portAsInt) + "-hash-" + interfaceDescUuid.String() + + resourceName := resource.ResourceName + + apiId := *sd.ApiId + kongServiceName := apiId + "-" + resourceName + kongServiceNamePrefix := kongServiceName + "-" + uriPrefix + + log.Debugf("createKongServiceRoute, kongServiceName %s", kongServiceName) + log.Debugf("createKongServiceRoute, kongServiceNamePrefix %s", kongServiceNamePrefix) + + tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName) + log.Debugf("createKongServiceRoute, tags %s", tags) + + kongServiceInfo := map[string]interface{}{ + "host": ipv4Addr, + "name": kongServiceNamePrefix, + "port": port, + "protocol": kongControlPlaneURLParsed.Scheme, + "path": kongServiceUri, + "tags": tags, + } + + // Kong admin API endpoint for creating a service + kongServicesURL := kongControlPlaneURL + "/services" + + // Make the POST request to create the Kong service + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(kongServiceInfo). + Post(kongServicesURL) + + // Check for errors in the request + if err != nil { + log.Errorf("createKongServiceRoute, Request Error: %v", err) + return "", http.StatusInternalServerError, err + } + + // Check the response status code + statusCode = resp.StatusCode() + if statusCode == http.StatusCreated { + log.Infof("kong service %s created successfully", kongServiceNamePrefix) + } else if resp.StatusCode() == http.StatusConflict { + log.Errorf("kong service already exists. Status code: %d", resp.StatusCode()) + err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service + statusCode = http.StatusForbidden // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API + } else { + err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode()) + } + if err != nil { + log.Errorf(err.Error()) + log.Errorf("response body: %s", resp.Body()) + return "", statusCode, err + } + + // Create matching route + routeName := kongServiceNamePrefix + kongRouteUri := uri + + kongRouteUri = prependUri(uriPrefix, kongRouteUri) + log.Debugf("createKongServiceRoute, kongRouteUri with uriPrefix %s", kongRouteUri) + + kongRouteUri = prependUri(sd.ApiName, kongRouteUri) + log.Debugf("createKongServiceRoute, kongRouteUri with apiName %s", kongRouteUri) + + kongRouteUri, statusCode, err = sd.createRouteForService(kongControlPlaneURL, client, resource, routeName, kongRouteUri, uri, tags, foundRegEx) + if err != nil { + log.Errorf(err.Error()) + return kongRouteUri, statusCode, err + } + + return kongRouteUri, statusCode, err +} + +func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string { + tagsMap := map[string]string{ + "apfId": apfId, + "aefId": aefId, + "apiId": apiId, + "apiVersion": apiVersion, + "resourceName": resourceName, + } + + // Convert the map to a slice of strings + var tagsSlice []string + for key, value := range tagsMap { + str := fmt.Sprintf("%s: %s", key, value) + tagsSlice = append(tagsSlice, str) + } + + return tagsSlice +} + +func prependUri(prependUri string, uri string) string { + if prependUri != "" { + trimmedUri := uri + foundRegEx := false + if strings.HasPrefix(uri, "~") { + log.Debug("prependUri, found regex prefix") + foundRegEx = true + trimmedUri = strings.TrimPrefix(uri, "~") + log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri) + } + + if prependUri[0] != '/' { + prependUri = "/" + prependUri + } + if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' { + prependUri = prependUri + "/" + } + uri = prependUri + trimmedUri + if foundRegEx { + uri = "~" + uri + } } + return uri +} + +func (sd *ServiceAPIDescription) createRouteForService( + kongControlPlaneURL string, + client *resty.Client, + resource Resource, + routeName string, + kongRouteUri string, + uri string, + tags []string, + foundRegEx bool) (string, int, error) { + + log.Debugf("createRouteForService, kongRouteUri %s", kongRouteUri) + // Create a url.Values map to hold the form data + data := url.Values{} + data.Set("strip_path", "true") + log.Debugf("createRouteForService, strip_path %s", data.Get("strip_path")) data.Set("name", routeName) - routeUriPaths := []string{routeUri} + routeUriPaths := []string{kongRouteUri} for _, path := range routeUriPaths { - log.Debugf("createKongRoute, path %s", path) + log.Debugf("createRouteForService, path %s", path) data.Add("paths", path) - } + } for _, tag := range tags { - log.Debugf("createKongRoute, tag %s", tag) + log.Debugf("createRouteForService, tag %s", tag) data.Add("tags", tag) - } + } for _, op := range *resource.Operations { - log.Debugf("createKongRoute, op %s", string(op)) + log.Debugf("createRouteForService, op %s", string(op)) data.Add("methods", string(op)) - } + } // Encode the data to application/x-www-form-urlencoded format encodedData := data.Encode() // Make the POST request to create the Kong service + serviceName := routeName kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes" resp, err := client.R(). SetHeader("Content-Type", "application/x-www-form-urlencoded"). @@ -177,8 +483,8 @@ func (sd *ServiceAPIDescription) createKongRoute( // Check for errors in the request if err != nil { - log.Debugf("createKongRoute POST Error: %v", err) - return resp.StatusCode(), err + log.Debugf("createRouteForService POST Error: %v", err) + return kongRouteUri, resp.StatusCode(), err } // Check the response status code @@ -187,7 +493,7 @@ func (sd *ServiceAPIDescription) createKongRoute( if (foundRegEx) { statusCode, err := sd.createRequestTransformer(kongControlPlaneURL, client, routeName, uri) if (err != nil) || ((statusCode != http.StatusCreated) && (statusCode != http.StatusForbidden)) { - return statusCode, err + return kongRouteUri, statusCode, err } } } else { @@ -195,10 +501,10 @@ func (sd *ServiceAPIDescription) createKongRoute( err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode()) log.Error(err.Error()) log.Errorf("response body: %s", resp.Body()) - return resp.StatusCode(), err + return kongRouteUri, resp.StatusCode(), err } - return resp.StatusCode(), nil + return kongRouteUri, resp.StatusCode(), nil } func (sd *ServiceAPIDescription) createRequestTransformer( @@ -235,7 +541,7 @@ func (sd *ServiceAPIDescription) createRequestTransformer( // Check the response status code if resp.StatusCode() == http.StatusCreated { - log.Infof("kong request transformer created successfully for route %s", routeName) + log.Infof("kong request transformer for route %s created successfully", routeName) } else { log.Debugf("kongRequestTransformerURL %s", kongRequestTransformerURL) err = fmt.Errorf("error creating Kong request transformer. Status code: %d", resp.StatusCode()) @@ -286,168 +592,6 @@ func deriveTransformPattern(routePattern string) (string, error) { return transformPattern, nil } -func insertVersion(version string, route string) string { - versionedRoute := route - - if version != "" { - sep := "/" - n := 3 - - foundRegEx := false - if strings.HasPrefix(route, "~") { - log.Debug("insertVersion, found regex prefix") - foundRegEx = true - route = strings.TrimPrefix(route, "~") - } - - log.Debugf("insertVersion route %s", route) - split := strings.SplitAfterN(route, sep, n) - log.Debugf("insertVersion split %q", split) - - versionedRoute = split[0] - if len(split) == 2 { - versionedRoute = split[0] + split[1] - } else if len(split) > 2 { - versionedRoute = split[0] + split[1] + version + sep + split[2] - } - - if foundRegEx { - versionedRoute = "~" + versionedRoute - } - } - log.Debugf("insertVersion versionedRoute %s", versionedRoute) - - return versionedRoute -} - -func prependUri(prependUri string, uri string) string { - if prependUri != "" { - trimmedUri := uri - foundRegEx := false - if strings.HasPrefix(uri, "~") { - log.Debug("prependUri, found regex prefix") - foundRegEx = true - trimmedUri = strings.TrimPrefix(uri, "~") - log.Debugf("prependUri, TrimPrefix trimmedUri %s", trimmedUri) - } - - if prependUri[0] != '/' { - prependUri = "/" + prependUri - } - if prependUri[len(prependUri)-1] != '/' && trimmedUri[0] != '/' { - prependUri = prependUri + "/" - } - uri = prependUri + trimmedUri - if foundRegEx { - uri = "~" + uri - } - } - return uri -} - -func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string { - tagsMap := map[string]string{ - "apfId": apfId, - "aefId": aefId, - "apiId": apiId, - "apiVersion": apiVersion, - "resourceName": resourceName, - } - - // Convert the map to a slice of strings - var tagsSlice []string - for key, value := range tagsMap { - str := fmt.Sprintf("%s: %s", key, value) - tagsSlice = append(tagsSlice, str) - } - - return tagsSlice -} - -func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, tags []string) (int, error) { - log.Tracef("entering createKongService") - log.Tracef("createKongService, kongServiceName %s", kongServiceName) - - // Define the service information for Kong - firstAEFProfileIpv4Addr, firstAEFProfilePort, err := sd.findFirstAEFProfile() - if err != nil { - return http.StatusBadRequest, err - } - - kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL) - if err != nil { - return http.StatusInternalServerError, err - } - log.Debugf("kongControlPlaneURL %s", kongControlPlaneURL) - log.Debugf("kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme) - - kongServiceInfo := map[string]interface{}{ - "host": firstAEFProfileIpv4Addr, - "name": kongServiceName, - "port": firstAEFProfilePort, - "protocol": kongControlPlaneURLParsed.Scheme, - "path": kongServiceUri, - "tags": tags, - } - - // Kong admin API endpoint for creating a service - kongServicesURL := kongControlPlaneURL + "/services" - - // Create a new Resty client - client := resty.New() - - // Make the POST request to create the Kong service - resp, err := client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(kongServiceInfo). - Post(kongServicesURL) - - // Check for errors in the request - if err != nil { - log.Errorf("create Kong Service Request Error: %v", err) - return http.StatusInternalServerError, err - } - - // Check the response status code - statusCode := resp.StatusCode() - if statusCode == http.StatusCreated { - log.Infof("kong service %s created successfully", kongServiceName) - } else if resp.StatusCode() == http.StatusConflict { - log.Errorf("kong service already exists. Status code: %d", resp.StatusCode()) - err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service - statusCode = http.StatusForbidden // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API - } else { - err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode()) - } - if err != nil { - log.Errorf(err.Error()) - log.Errorf("response body: %s", resp.Body()) - } - - return statusCode, err -} - -func (sd *ServiceAPIDescription) findFirstAEFProfile() (common29122.Ipv4Addr, common29122.Port, error) { - log.Tracef("entering findFirstAEFProfile") - var aefProfile AefProfile - if *sd.AefProfiles != nil { - aefProfile = (*sd.AefProfiles)[0] - } - if (*sd.AefProfiles == nil) || (aefProfile.InterfaceDescriptions == nil) { - err := errors.New("cannot read interfaceDescription") - log.Errorf(err.Error()) - return "", common29122.Port(0), err - } - - interfaceDescription := (*aefProfile.InterfaceDescriptions)[0] - firstIpv4Addr := *interfaceDescription.Ipv4Addr - firstPort := *interfaceDescription.Port - - log.Debugf("findFirstAEFProfile firstIpv4Addr %s firstPort %d", firstIpv4Addr, firstPort) - - return firstIpv4Addr, firstPort, nil -} - // Update our exposures to point to Kong by replacing in incoming interface description with Kong interface descriptions. func (sd *ServiceAPIDescription) updateInterfaceDescription(kongDataPlaneIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongDomain string) { log.Trace("updating InterfaceDescriptions") diff --git a/servicemanager/mockkong/kong_mock.go b/servicemanager/mockkong/kong_mock.go index c0a6dba..8191c03 100644 --- a/servicemanager/mockkong/kong_mock.go +++ b/servicemanager/mockkong/kong_mock.go @@ -85,6 +85,166 @@ func RegisterHandlers(e *echo.Echo) { return c.String(http.StatusCreated, string(body)) }) + e.POST("/services/api_id_apiName_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-id_helloworld-id-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-v1-id_helloworld-id-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-no-version_helloworld-no-version-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-no-version_helloworld-no-version-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2_helloworld-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1_app-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2_app-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-v1-id-helloworld-id-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-v1-id-helloworld-id-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_helloworld-no-version-helloworld-no-version-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/routes/api_id_helloworld-no-version-helloworld-no-version-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/plugins", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName1-app-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2-helloworld-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + + e.POST("/services/api_id_apiName2-app-port-30951-hash-04478a3a-d0ef-5a05-a575-db5ee2e33403/routes", func(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading request body") + } + return c.String(http.StatusCreated, string(body)) + }) + e.POST("/services/api_id_apiName_helloworld-id/routes", func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { @@ -245,4 +405,4 @@ func RegisterHandlers(e *echo.Echo) { return c.NoContent(http.StatusNoContent) }) -} \ No newline at end of file +}