NONRTRIC-946: Servicemanager - mock kong and capif as library
[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,
42                 kongProtocol string,
43                 kongIPv4 common29122.Ipv4Addr,
44                 kongDataPlanePort common29122.Port,
45                 kongControlPlanePort common29122.Port,
46                 apfId string) (int, error) {
47
48         log.Trace("entering RegisterKong")
49         var (
50                 statusCode int
51                 err        error
52         )
53         kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
54
55         statusCode, err = sd.createKongRoutes(kongControlPlaneURL, apfId)
56         if (err != nil) || (statusCode != http.StatusCreated) {
57                 return statusCode, err
58         }
59
60         sd.updateInterfaceDescription(kongIPv4, kongDataPlanePort, kongDomain)
61
62         log.Trace("exiting from RegisterKong")
63         return statusCode, nil
64 }
65
66 func (sd *ServiceAPIDescription) createKongRoutes(kongControlPlaneURL string, apfId string) (int, error) {
67         log.Trace("entering createKongRoutes")
68         var (
69                 statusCode int
70                 err        error
71         )
72
73         client := resty.New()
74
75         profiles := *sd.AefProfiles
76         for _, profile := range profiles {
77                 log.Debugf("createKongRoutes, AefId %s", profile.AefId)
78                 for _, version := range profile.Versions {
79                         log.Debugf("createKongRoutes, apiVersion \"%s\"", version.ApiVersion)
80                         for _, resource := range *version.Resources {
81                                 statusCode, err = sd.createKongRoute(kongControlPlaneURL, client, resource, apfId, profile.AefId, version.ApiVersion)
82                                 if (err != nil) || (statusCode != http.StatusCreated) {
83                                         return statusCode, err
84                                 }
85                         }
86                 }
87         }
88         return statusCode, nil
89 }
90
91 func (sd *ServiceAPIDescription) createKongRoute(
92                 kongControlPlaneURL string,
93                 client *resty.Client,
94                 resource Resource,
95                 apfId string,
96                 aefId string,
97                 apiVersion string ) (int, error) {
98         log.Trace("entering createKongRoute")
99
100         resourceName := resource.ResourceName
101         apiId := *sd.ApiId
102
103         tags := buildTags(apfId, aefId, apiId, apiVersion, resourceName)
104         log.Debugf("createKongRoute, tags %s", tags)
105
106         serviceName := apiId + "_" + resourceName
107         routeName := serviceName
108
109         log.Debugf("createKongRoute, serviceName %s", serviceName)
110         log.Debugf("createKongRoute, routeName %s", routeName)
111         log.Debugf("createKongRoute, aefId %s", aefId)
112
113         uri := buildUriWithVersion(apiVersion, resource.Uri)
114         log.Debugf("createKongRoute, uri %s", uri)
115
116         statusCode, err := sd.createKongService(kongControlPlaneURL, serviceName, uri, tags)
117         if (err != nil) || (statusCode != http.StatusCreated) {
118                 return statusCode, err
119         }
120
121         kongRoutesURL := kongControlPlaneURL + "/services/" + serviceName + "/routes"
122
123         // Define the route information for Kong
124         kongRouteInfo := map[string]interface{}{
125                 "name":       routeName,
126                 "paths":      []string{uri},
127                 "methods":    resource.Operations,
128                 "tags":       tags,
129                 "strip_path": true,
130         }
131
132         // Make the POST request to create the Kong service
133         resp, err := client.R().
134                 SetHeader("Content-Type", "application/json").
135                 SetBody(kongRouteInfo).
136                 Post(kongRoutesURL)
137
138         // Check for errors in the request
139         if err != nil {
140                 log.Debugf("createKongRoute POST Error: %v", err)
141                 return resp.StatusCode(), err
142         }
143
144         // Check the response status code
145         if resp.StatusCode() == http.StatusCreated {
146                 log.Infof("kong route %s created successfully", routeName)
147         } else {
148                 log.Debugf("kongRoutesURL %s", kongRoutesURL)
149                 err = fmt.Errorf("error creating Kong route. Status code: %d", resp.StatusCode())
150                 log.Error(err.Error())
151                 log.Errorf("response body: %s", resp.Body())
152                 return resp.StatusCode(), err
153         }
154
155         return resp.StatusCode(), nil
156 }
157
158 func buildUriWithVersion(apiVersion string, uri string) string {
159         if apiVersion != "" {
160                 if apiVersion[0] != '/' {
161                         apiVersion = "/" + apiVersion
162                 }
163                 if apiVersion[len(apiVersion)-1] != '/' && uri[0] != '/' {
164                         apiVersion = apiVersion + "/"
165                 }
166                 uri = apiVersion + uri
167         }
168         return uri
169 }
170
171 func buildTags(apfId string, aefId string, apiId string, apiVersion string, resourceName string) []string  {
172         tagsMap := map[string]string{
173                 "apfId": apfId,
174                 "aefId": aefId,
175                 "apiId": apiId,
176                 "apiVersion": apiVersion,
177                 "resourceName": resourceName,
178         }
179
180         // Convert the map to a slice of strings
181         var tagsSlice []string
182         for key, value := range tagsMap {
183                 str := fmt.Sprintf("%s: %s", key, value)
184                 tagsSlice = append(tagsSlice, str)
185         }
186
187         return tagsSlice
188 }
189
190 func (sd *ServiceAPIDescription) createKongService(kongControlPlaneURL string, kongServiceName string, kongServiceUri string, tags []string) (int, error) {
191         log.Tracef("entering createKongService")
192         log.Tracef("createKongService, kongServiceName %s", kongServiceName)
193
194         // Define the service information for Kong
195         firstAEFProfileIpv4Addr, firstAEFProfilePort, err := sd.findFirstAEFProfile()
196         if err != nil {
197                 return http.StatusBadRequest, err
198         }
199
200         kongControlPlaneURLParsed, err := url.Parse(kongControlPlaneURL)
201         if err != nil {
202                 return http.StatusInternalServerError, err
203         }
204         log.Debugf("kongControlPlaneURL %s", kongControlPlaneURL)
205         log.Debugf("kongControlPlaneURLParsed.Scheme %s", kongControlPlaneURLParsed.Scheme)
206
207         kongServiceInfo := map[string]interface{}{
208                 "host":     firstAEFProfileIpv4Addr,
209                 "name":     kongServiceName,
210                 "port":     firstAEFProfilePort,
211                 "protocol": kongControlPlaneURLParsed.Scheme,
212                 "path":     kongServiceUri,
213                 "tags":     tags,
214         }
215
216         // Kong admin API endpoint for creating a service
217         kongServicesURL := kongControlPlaneURL + "/services"
218
219         // Create a new Resty client
220         client := resty.New()
221
222         // Make the POST request to create the Kong service
223         resp, err := client.R().
224                 SetHeader("Content-Type", "application/json").
225                 SetBody(kongServiceInfo).
226                 Post(kongServicesURL)
227
228         // Check for errors in the request
229         if err != nil {
230                 log.Errorf("create Kong Service Request Error: %v", err)
231                 return http.StatusInternalServerError, err
232         }
233
234         // Check the response status code
235         statusCode := resp.StatusCode()
236         if statusCode == http.StatusCreated {
237                 log.Infof("kong service %s created successfully", kongServiceName)
238         } else if resp.StatusCode() == http.StatusConflict {
239                 log.Errorf("kong service already exists. Status code: %d", resp.StatusCode())
240                 err = fmt.Errorf("service with identical apiName is already published") // for compatibilty with Capif error message on a duplicate service
241                 statusCode = http.StatusForbidden                                       // for compatibilty with the spec, TS29222_CAPIF_Publish_Service_API
242         } else {
243                 err = fmt.Errorf("error creating Kong service. Status code: %d", resp.StatusCode())
244         }
245         if err != nil {
246                 log.Errorf(err.Error())
247                 log.Errorf("response body: %s", resp.Body())
248         }
249
250         return statusCode, err
251 }
252
253 func (sd *ServiceAPIDescription) findFirstAEFProfile() (common29122.Ipv4Addr, common29122.Port, error) {
254         log.Tracef("entering findFirstAEFProfile")
255         var aefProfile AefProfile
256         if *sd.AefProfiles != nil {
257                 aefProfile = (*sd.AefProfiles)[0]
258         }
259         if (*sd.AefProfiles == nil) || (aefProfile.InterfaceDescriptions == nil) {
260                 err := errors.New("cannot read interfaceDescription")
261                 log.Errorf(err.Error())
262                 return "", common29122.Port(0), err
263         }
264
265         interfaceDescription := (*aefProfile.InterfaceDescriptions)[0]
266         firstIpv4Addr := *interfaceDescription.Ipv4Addr
267         firstPort := *interfaceDescription.Port
268
269         log.Debugf("findFirstAEFProfile firstIpv4Addr %s firstPort %d", firstIpv4Addr, firstPort)
270
271         return firstIpv4Addr, firstPort, nil
272 }
273
274 // Update our exposures to point to Kong by replacing in incoming interface description with Kong interface descriptions.
275 func (sd *ServiceAPIDescription) updateInterfaceDescription(kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongDomain string) {
276         log.Trace("updating InterfaceDescriptions")
277         interfaceDesc := InterfaceDescription{
278                 Ipv4Addr: &kongIPv4,
279                 Port:     &kongDataPlanePort,
280         }
281         interfaceDescs := []InterfaceDescription{interfaceDesc}
282
283         profiles := *sd.AefProfiles
284         for i, profile := range profiles {
285                 profile.DomainName = &kongDomain
286                 profile.InterfaceDescriptions = &interfaceDescs
287                 profiles[i] = profile
288         }
289 }
290
291 func (sd *ServiceAPIDescription) UnregisterKong(kongDomain string, kongProtocol string, kongIPv4 common29122.Ipv4Addr, kongDataPlanePort common29122.Port, kongControlPlanePort common29122.Port) (int, error) {
292         log.Trace("entering UnregisterKong")
293
294         var (
295                 statusCode int
296                 err        error
297         )
298         kongControlPlaneURL := fmt.Sprintf("%s://%s:%d", kongProtocol, kongIPv4, kongControlPlanePort)
299
300         statusCode, err = sd.deleteKongRoutes(kongControlPlaneURL)
301         if (err != nil) || (statusCode != http.StatusNoContent) {
302                 return statusCode, err
303         }
304
305         log.Trace("exiting from UnregisterKong")
306         return statusCode, nil
307 }
308
309 func (sd *ServiceAPIDescription) deleteKongRoutes(kongControlPlaneURL string) (int, error) {
310         log.Trace("entering deleteKongRoutes")
311
312         var (
313                 statusCode int
314                 err        error
315         )
316
317         client := resty.New()
318
319         profiles := *sd.AefProfiles
320         for _, profile := range profiles {
321                 log.Debugf("deleteKongRoutes, AefId %s", profile.AefId)
322                 for _, version := range profile.Versions {
323                         log.Debugf("deleteKongRoutes, apiVersion \"%s\"", version.ApiVersion)
324                         for _, resource := range *version.Resources {
325                                 statusCode, err = sd.deleteKongRoute(kongControlPlaneURL, client, resource, profile.AefId, version.ApiVersion)
326                                 if (err != nil) || (statusCode != http.StatusNoContent) {
327                                         return statusCode, err
328                                 }
329                         }
330                 }
331         }
332         return statusCode, nil
333 }
334
335 func (sd *ServiceAPIDescription) deleteKongRoute(kongControlPlaneURL string, client *resty.Client, resource Resource, aefId string, apiVersion string) (int, error) {
336         log.Trace("entering deleteKongRoute")
337         routeName := *sd.ApiId + "_" + resource.ResourceName
338         kongRoutesURL := kongControlPlaneURL + "/routes/" + routeName + "?tags=" + aefId
339         log.Debugf("deleteKongRoute, routeName %s, tag %s", routeName, aefId)
340
341         // Make the DELETE request to delete the Kong route
342         resp, err := client.R().Delete(kongRoutesURL)
343
344         // Check for errors in the request
345         if err != nil {
346                 log.Errorf("error on Kong route delete: %v", err)
347                 return resp.StatusCode(), err
348         }
349
350         // Check the response status code
351         if resp.StatusCode() == http.StatusNoContent {
352                 log.Infof("kong route %s deleted successfully", routeName)
353         } else {
354                 log.Debugf("kongRoutesURL: %s", kongRoutesURL)
355                 log.Errorf("error deleting Kong route. Status code: %d", resp.StatusCode())
356                 log.Errorf("response body: %s", resp.Body())
357                 return resp.StatusCode(), err
358         }
359
360         statusCode, err := sd.deleteKongService(kongControlPlaneURL, routeName, aefId)
361         if (err != nil) || (statusCode != http.StatusNoContent) {
362                 return statusCode, err
363         }
364         return statusCode, err
365 }
366
367 func (sd *ServiceAPIDescription) deleteKongService(kongControlPlaneURL string, serviceName string, aefId string) (int, error) {
368         log.Trace("entering deleteKongService")
369         // Define the service information for Kong
370         // Kong admin API endpoint for deleting a service
371         kongServicesURL := kongControlPlaneURL + "/services/" + serviceName + "?tags=" + aefId
372
373         // Create a new Resty client
374         client := resty.New()
375
376         // Make the DELETE request to delete the Kong service
377         resp, err := client.R().
378                 SetHeader("Content-Type", "application/json").
379                 Delete(kongServicesURL)
380
381         // Check for errors in the request
382         if err != nil {
383                 log.Errorf("delete kong service request: %v", err)
384                 return http.StatusInternalServerError, err
385         }
386
387         // Check the response status code
388         if resp.StatusCode() == http.StatusNoContent {
389                 log.Infof("kong service %s deleted successfully", serviceName)
390         } else {
391                 log.Debugf("kongServicesURL: %s", kongServicesURL)
392                 log.Errorf("deleting Kong service, status code: %d", resp.StatusCode())
393                 log.Errorf("response body: %s", resp.Body())
394         }
395         return resp.StatusCode(), nil
396 }