NONRTRIC-946: Add support for Kong routes
[nonrtric/plt/sme.git] / servicemanager / internal / publishserviceapi / typeupdate.go
diff --git a/servicemanager/internal/publishserviceapi/typeupdate.go b/servicemanager/internal/publishserviceapi/typeupdate.go
new file mode 100644 (file)
index 0000000..fe681f9
--- /dev/null
@@ -0,0 +1,351 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2023-2024: OpenInfra Foundation Europe
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package publishserviceapi
+
+import (
+       "errors"
+       "fmt"
+       "net/http"
+       "net/url"
+       "strings"
+
+       resty "github.com/go-resty/resty/v2"
+       log "github.com/sirupsen/logrus"
+
+       common29122 "oransc.org/nonrtric/servicemanager/internal/common29122"
+)
+
+func (sd *ServiceAPIDescription) PrepareNewService() {
+       apiName := "api_id_" + strings.ReplaceAll(sd.ApiName, " ", "_")
+       sd.ApiId = &apiName
+}
+
+func (sd *ServiceAPIDescription) RegisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
+       log.Trace("entering RegisterKong")
+       var (
+               statusCode int
+               err        error
+       )
+       kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
+
+       statusCode, err = sd.createKongRoutes(kongControlPlaneURL)
+       if (err != nil) || (statusCode != http.StatusCreated) {
+               return statusCode, err
+       }
+
+       sd.updateInterfaceDescription(kongIPv4, kongDataPlanePort, kongDomain)
+
+       log.Trace("exiting from RegisterKong")
+       return statusCode, nil
+}
+
+func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string) (int, error) {
+       log.Trace("entering createKongRoutes")
+       var (
+               statusCode int
+               err        error
+       )
+
+       client := resty.New()
+
+       profiles := *sd.AefProfiles
+       for _, profile := range profiles {
+               log.Debugf("createKongRoutes, AefId %s", profile.AefId)
+               for _, version := range profile.Versions {
+                       log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion)
+                       for _, resource := range *version.Resources {
+                               statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
+                               if (err != nil) || (statusCode != http.StatusCreated) {
+                                       return statusCode, err
+                               }
+                       }
+               }
+       }
+       return statusCode, nil
+}
+
+func (sd *ServiceAPIDescription) createKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
+       log.Trace("entering createKongRoute")
+       uri := resource.Uri
+
+       if apiVersion != "" {
+               if apiVersion[0] != '/' {
+                       apiVersion = "/" + apiVersion
+               }
+               if apiVersion[len(apiVersion)-1] != '/' && resource.Uri[0] != '/' {
+                       apiVersion = apiVersion + "/"
+               }
+               uri = apiVersion + resource.Uri
+       }
+
+       log.Debugf("createKongRoute, uri %s", uri)
+
+       serviceName := *sd.ApiId + "_" + resource.ResourceName
+       log.Debugf("createKongRoute, serviceName %s", serviceName)
+       log.Debugf("createKongRoute, aefId %s", aefId)
+
+       statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, aefId)
+       if (err != nil) || (statusCode != http.StatusCreated) {
+               return statusCode, err
+       }
+
+       routeName := serviceName
+       kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
+
+       // Define the route information for Kong
+       kongRouteInfo := map[string]interface{}{
+               "name":       routeName,
+               "paths":      []string{uri},
+               "methods":    resource.Operations,
+               "tags":       []string{aefId},
+               "strip_path": true,
+       }
+
+       // Make the POST request to create the Kong service
+       resp, err := client.R().
+               SetHeader("Content-Type", "application/json").
+               SetBody(kongRouteInfo).
+               Post(kongRoutesURL)
+
+       // Check for errors in the request
+       if err != nil {
+               log.Debugf("createKongRoute POST Error: %v", err)
+               return resp.StatusCode(), err
+       }
+
+       // Check the response status code
+       if resp.StatusCode() == http.StatusCreated {
+               log.Infof("kong route %s created successfully", routeName)
+       } else {
+               err = fmt.Errorf("the Kong service already exists. Status code: %d", resp.StatusCode())
+               log.Error(err.Error())
+               log.Errorf("response body: %s", resp.Body())
+               return resp.StatusCode(), err
+       }
+
+       return resp.StatusCode(), nil
+}
+
+func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, aefId 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":     []string{aefId},
+       }
+
+       // 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(kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongDomain string) {
+       log.Trace("updating InterfaceDescriptions")
+       interfaceDesc := InterfaceDescription{
+               Ipv4Addr: &kongIPv4,
+               Port:     &kongDataPlanePort,
+       }
+       interfaceDescs := []InterfaceDescription{interfaceDesc}
+
+       profiles := *sd.AefProfiles
+       for i, profile := range profiles {
+               profile.DomainName = &kongDomain
+               profile.InterfaceDescriptions = &interfaceDescs
+               profiles[i] = profile
+       }
+}
+
+func (sd *ServiceAPIDescription) UnregisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
+       log.Trace("entering UnregisterKong")
+
+       var (
+               statusCode int
+               err        error
+       )
+       kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
+
+       statusCode, err = sd.deleteKongRoutes(kongControlPlaneURL)
+       if (err != nil) || (statusCode != http.StatusNoContent) {
+               return statusCode, err
+       }
+
+       log.Trace("exiting from UnregisterKong")
+       return statusCode, nil
+}
+
+func (sd *ServiceAPIDescription) deleteKongRoutes(kongControlPlaneURL string) (int, error) {
+       log.Trace("entering deleteKongRoutes")
+
+       var (
+               statusCode int
+               err        error
+       )
+
+       client := resty.New()
+
+       profiles := *sd.AefProfiles
+       for _, profile := range profiles {
+               log.Debugf("deleteKongRoutes, AefId %s", profile.AefId)
+               for _, version := range profile.Versions {
+                       log.Debugf("deleteKongRoutes, apiVersion \"%s\"", version.ApiVersion)
+                       for _, resource := range *version.Resources {
+                               statusCode, err = sd.deleteKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
+                               if (err != nil) || (statusCode != http.StatusNoContent) {
+                                       return statusCode, err
+                               }
+                       }
+               }
+       }
+       return statusCode, nil
+}
+
+func (sd *ServiceAPIDescription) deleteKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
+       log.Trace("entering deleteKongRoute")
+       routeName := *sd.ApiId + "_" + resource.ResourceName
+       kongRoutesURL := kongControlPlaneURL + "/routes/" + routeName + "?tags=" + aefId
+       log.Debugf("deleteKongRoute, routeName %s, tag %s", routeName, aefId)
+
+       // Make the DELETE request to delete the Kong route
+       resp, err := client.R().Delete(kongRoutesURL)
+
+       // Check for errors in the request
+       if err != nil {
+               log.Errorf("error on Kong route delete: %v", err)
+               return resp.StatusCode(), err
+       }
+
+       // Check the response status code
+       if resp.StatusCode() == http.StatusNoContent {
+               log.Infof("kong route %s deleted successfully", routeName)
+       } else {
+               log.Errorf("error deleting Kong route. Status code: %d", resp.StatusCode())
+               log.Errorf("response body: %s", resp.Body())
+               return resp.StatusCode(), err
+       }
+
+       statusCode, err := sd.deleteKongService(kongControlPlaneURL, routeName, aefId)
+       if (err != nil) || (statusCode != http.StatusNoContent) {
+               return statusCode, err
+       }
+       return statusCode, err
+}
+
+func (sd *ServiceAPIDescription) deleteKongService(kongControlPlaneURL string, serviceName string, aefId string) (int, error) {
+       log.Trace("entering deleteKongService")
+       // Define the service information for Kong
+       // Kong admin API endpoint for deleting a service
+       kongServicesURL := kongControlPlaneURL + "/services/" + serviceName + "?tags=" + aefId
+
+       // Create a new Resty client
+       client := resty.New()
+
+       // Make the DELETE request to delete the Kong service
+       resp, err := client.R().
+               SetHeader("Content-Type", "application/json").
+               Delete(kongServicesURL)
+
+       // Check for errors in the request
+       if err != nil {
+               log.Errorf("delete kong service request: %v", err)
+               return http.StatusInternalServerError, err
+       }
+
+       // Check the response status code
+       if resp.StatusCode() == http.StatusNoContent {
+               log.Infof("kong service %s deleted successfully", serviceName)
+       } else {
+               log.Errorf("deleting Kong service, status code: %d", resp.StatusCode())
+               log.Errorf("response body: %s", resp.Body())
+       }
+       return resp.StatusCode(), nil
+}