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