NONRTRIC-946: Add support for Kong routes
[nonrtric/plt/sme.git] / servicemanager / internal / publishserviceapi / typeupdate.go
1 // -
2 //   ========================LICENSE_START=================================
3 //   O-RAN-SC
4 //   %%
5 //   Copyright (C) 2023-2024: OpenInfra Foundation Europe
6 //   %%
7 //   Licensed under the Apache License, Version 2.0 (the "License");
8 //   you may not use this file except in compliance with the License.
9 //   You may obtain a copy of the License at
10 //
11 //        http://www.apache.org/licenses/LICENSE-2.0
12 //
13 //   Unless required by applicable law or agreed to in writing, software
14 //   distributed under the License is distributed on an "AS IS" BASIS,
15 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 //   See the License for the specific language governing permissions and
17 //   limitations under the License.
18 //   ========================LICENSE_END===================================
19 //
20
21 package publishserviceapi
22
23 import (
24         "errors"
25         "fmt"
26         "net/http"
27         "net/url"
28         "strings"
29
30         resty "github.com/go-resty/resty/v2"
31         log "github.com/sirupsen/logrus"
32
33         common29122 "oransc.org/nonrtric/servicemanager/internal/common29122"
34 )
35
36 func (sd *ServiceAPIDescription) PrepareNewService() {
37         apiName := "api_id_" + strings.ReplaceAll(sd.ApiName, " ", "_")
38         sd.ApiId = &apiName
39 }
40
41 func (sd *ServiceAPIDescription) RegisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
42         log.Trace("entering RegisterKong")
43         var (
44                 statusCode int
45                 err        error
46         )
47         kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
48
49         statusCode, err = sd.createKongRoutes(kongControlPlaneURL)
50         if (err != nil) || (statusCode != http.StatusCreated) {
51                 return statusCode, err
52         }
53
54         sd.updateInterfaceDescription(kongIPv4, kongDataPlanePort, kongDomain)
55
56         log.Trace("exiting from RegisterKong")
57         return statusCode, nil
58 }
59
60 func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string) (int, error) {
61         log.Trace("entering createKongRoutes")
62         var (
63                 statusCode int
64                 err        error
65         )
66
67         client := resty.New()
68
69         profiles := *sd.AefProfiles
70         for _, profile := range profiles {
71                 log.Debugf("createKongRoutes, AefId %s", profile.AefId)
72                 for _, version := range profile.Versions {
73                         log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion)
74                         for _, resource := range *version.Resources {
75                                 statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
76                                 if (err != nil) || (statusCode != http.StatusCreated) {
77                                         return statusCode, err
78                                 }
79                         }
80                 }
81         }
82         return statusCode, nil
83 }
84
85 func (sd *ServiceAPIDescription) createKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
86         log.Trace("entering createKongRoute")
87         uri := resource.Uri
88
89         if apiVersion != "" {
90                 if apiVersion[0] != '/' {
91                         apiVersion = "/" + apiVersion
92                 }
93                 if apiVersion[len(apiVersion)-1] != '/' && resource.Uri[0] != '/' {
94                         apiVersion = apiVersion + "/"
95                 }
96                 uri = apiVersion + resource.Uri
97         }
98
99         log.Debugf("createKongRoute, uri %s", uri)
100
101         serviceName := *sd.ApiId + "_" + resource.ResourceName
102         log.Debugf("createKongRoute, serviceName %s", serviceName)
103         log.Debugf("createKongRoute, aefId %s", aefId)
104
105         statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, aefId)
106         if (err != nil) || (statusCode != http.StatusCreated) {
107                 return statusCode, err
108         }
109
110         routeName := serviceName
111         kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
112
113         // Define the route information for Kong
114         kongRouteInfo := map[string]interface{}{
115                 "name":       routeName,
116                 "paths":      []string{uri},
117                 "methods":    resource.Operations,
118                 "tags":       []string{aefId},
119                 "strip_path": true,
120         }
121
122         // Make the POST request to create the Kong service
123         resp, err := client.R().
124                 SetHeader("Content-Type", "application/json").
125                 SetBody(kongRouteInfo).
126                 Post(kongRoutesURL)
127
128         // Check for errors in the request
129         if err != nil {
130                 log.Debugf("createKongRoute POST Error: %v", err)
131                 return resp.StatusCode(), err
132         }
133
134         // Check the response status code
135         if resp.StatusCode() == http.StatusCreated {
136                 log.Infof("kong route %s created successfully", routeName)
137         } else {
138                 err = fmt.Errorf("the Kong service already exists. Status code: %d", resp.StatusCode())
139                 log.Error(err.Error())
140                 log.Errorf("response body: %s", resp.Body())
141                 return resp.StatusCode(), err
142         }
143
144         return resp.StatusCode(), nil
145 }
146
147 func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, aefId string) (int, error) {
148         log.Tracef("entering createKongService")
149         log.Tracef("createKongService, kongServiceName %s", kongServiceName)
150
151         // Define the service information for Kong
152         firstAEFProfileIpv4Addr, firstAEFProfilePort, err := sd.findFirstAEFProfile()
153         if err != nil {
154                 return http.StatusBadRequest, err
155         }
156
157         kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL)
158         if err != nil {
159                 return http.StatusInternalServerError, err
160         }
161         log.Debugf("kongControlPlaneURL %s", kongControlPlaneURL)
162         log.Debugf("kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme)
163
164         kongServiceInfo := map[string]interface{}{
165                 "host":     firstAEFProfileIpv4Addr,
166                 "name":     kongServiceName,
167                 "port":     firstAEFProfilePort,
168                 "protocol": kongControlPlaneURLParsed.Scheme,
169                 "path":     kongServiceUri,
170                 "tags":     []string{aefId},
171         }
172
173         // Kong admin API endpoint for creating a service
174         kongServicesURL := kongControlPlaneURL + "/services"
175
176         // Create a new Resty client
177         client := resty.New()
178
179         // Make the POST request to create the Kong service
180         resp, err := client.R().
181                 SetHeader("Content-Type", "application/json").
182                 SetBody(kongServiceInfo).
183                 Post(kongServicesURL)
184
185         // Check for errors in the request
186         if err != nil {
187                 log.Errorf("create Kong Service Request Error: %v", err)
188                 return http.StatusInternalServerError, err
189         }
190
191         // Check the response status code
192         statusCode := resp.StatusCode()
193         if statusCode == http.StatusCreated {
194                 log.Infof("kong service %s created successfully", kongServiceName)
195         } else if resp.StatusCode() == http.StatusConflict {
196                 log.Errorf("kong service already exists. Status code: %d", resp.StatusCode())
197                 err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service
198                 statusCode = http.StatusForbidden // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API
199         } else {
200                 err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode())
201         }
202         if err != nil {
203                 log.Errorf(err.Error())
204                 log.Errorf("response body: %s", resp.Body())
205         }
206
207         return statusCode, err
208 }
209
210 func (sd *ServiceAPIDescription) findFirstAEFProfile() (common29122.Ipv4Addr, common29122.Port, error) {
211         log.Tracef("entering findFirstAEFProfile")
212         var aefProfile AefProfile
213         if *sd.AefProfiles != nil {
214                 aefProfile = (*sd.AefProfiles)[0]
215         }
216         if (*sd.AefProfiles == nil) || (aefProfile.InterfaceDescriptions == nil) {
217                 err := errors.New("cannot read interfaceDescription")
218                 log.Errorf(err.Error())
219                 return "", common29122.Port(0), err
220         }
221
222         interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
223         firstIpv4Addr := *interfaceDescription.Ipv4Addr
224         firstPort := *interfaceDescription.Port
225
226         log.Debugf("findFirstAEFProfile firstIpv4Addr %s firstPort %d", firstIpv4Addr, firstPort)
227
228         return firstIpv4Addr, firstPort, nil
229 }
230
231 // Update our exposures to point to Kong by replacing in incoming interface description with Kong interface descriptions.
232 func (sd *ServiceAPIDescription) updateInterfaceDescription(kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongDomain string) {
233         log.Trace("updating InterfaceDescriptions")
234         interfaceDesc := InterfaceDescription{
235                 Ipv4Addr: &kongIPv4,
236                 Port:     &kongDataPlanePort,
237         }
238         interfaceDescs := []InterfaceDescription{interfaceDesc}
239
240         profiles := *sd.AefProfiles
241         for i, profile := range profiles {
242                 profile.DomainName = &kongDomain
243                 profile.InterfaceDescriptions = &interfaceDescs
244                 profiles[i] = profile
245         }
246 }
247
248 func (sd *ServiceAPIDescription) UnregisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
249         log.Trace("entering UnregisterKong")
250
251         var (
252                 statusCode int
253                 err        error
254         )
255         kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
256
257         statusCode, err = sd.deleteKongRoutes(kongControlPlaneURL)
258         if (err != nil) || (statusCode != http.StatusNoContent) {
259                 return statusCode, err
260         }
261
262         log.Trace("exiting from UnregisterKong")
263         return statusCode, nil
264 }
265
266 func (sd *ServiceAPIDescription) deleteKongRoutes(kongControlPlaneURL string) (int, error) {
267         log.Trace("entering deleteKongRoutes")
268
269         var (
270                 statusCode int
271                 err        error
272         )
273
274         client := resty.New()
275
276         profiles := *sd.AefProfiles
277         for _, profile := range profiles {
278                 log.Debugf("deleteKongRoutes, AefId %s", profile.AefId)
279                 for _, version := range profile.Versions {
280                         log.Debugf("deleteKongRoutes, apiVersion \"%s\"", version.ApiVersion)
281                         for _, resource := range *version.Resources {
282                                 statusCode, err = sd.deleteKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
283                                 if (err != nil) || (statusCode != http.StatusNoContent) {
284                                         return statusCode, err
285                                 }
286                         }
287                 }
288         }
289         return statusCode, nil
290 }
291
292 func (sd *ServiceAPIDescription) deleteKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
293         log.Trace("entering deleteKongRoute")
294         routeName := *sd.ApiId + "_" + resource.ResourceName
295         kongRoutesURL := kongControlPlaneURL + "/routes/" + routeName + "?tags=" + aefId
296         log.Debugf("deleteKongRoute, routeName %s, tag %s", routeName, aefId)
297
298         // Make the DELETE request to delete the Kong route
299         resp, err := client.R().Delete(kongRoutesURL)
300
301         // Check for errors in the request
302         if err != nil {
303                 log.Errorf("error on Kong route delete: %v", err)
304                 return resp.StatusCode(), err
305         }
306
307         // Check the response status code
308         if resp.StatusCode() == http.StatusNoContent {
309                 log.Infof("kong route %s deleted successfully", routeName)
310         } else {
311                 log.Errorf("error deleting Kong route. Status code: %d", resp.StatusCode())
312                 log.Errorf("response body: %s", resp.Body())
313                 return resp.StatusCode(), err
314         }
315
316         statusCode, err := sd.deleteKongService(kongControlPlaneURL, routeName, aefId)
317         if (err != nil) || (statusCode != http.StatusNoContent) {
318                 return statusCode, err
319         }
320         return statusCode, err
321 }
322
323 func (sd *ServiceAPIDescription) deleteKongService(kongControlPlaneURL string, serviceName string, aefId string) (int, error) {
324         log.Trace("entering deleteKongService")
325         // Define the service information for Kong
326         // Kong admin API endpoint for deleting a service
327         kongServicesURL := kongControlPlaneURL + "/services/" + serviceName + "?tags=" + aefId
328
329         // Create a new Resty client
330         client := resty.New()
331
332         // Make the DELETE request to delete the Kong service
333         resp, err := client.R().
334                 SetHeader("Content-Type", "application/json").
335                 Delete(kongServicesURL)
336
337         // Check for errors in the request
338         if err != nil {
339                 log.Errorf("delete kong service request: %v", err)
340                 return http.StatusInternalServerError, err
341         }
342
343         // Check the response status code
344         if resp.StatusCode() == http.StatusNoContent {
345                 log.Infof("kong service %s deleted successfully", serviceName)
346         } else {
347                 log.Errorf("deleting Kong service, status code: %d", resp.StatusCode())
348                 log.Errorf("response body: %s", resp.Body())
349         }
350         return resp.StatusCode(), nil
351 }