Add update functionality to publish service
[nonrtric/plt/sme.git] / capifcore / internal / publishservice / publishservice.go
1 // -
2 //   ========================LICENSE_START=================================
3 //   O-RAN-SC
4 //   %%
5 //   Copyright (C) 2022: Nordix Foundation
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 publishservice
22
23 import (
24         "fmt"
25         "net/http"
26         "path"
27         "strings"
28         "sync"
29
30         "github.com/labstack/echo/v4"
31         "k8s.io/utils/strings/slices"
32
33         "oransc.org/nonrtric/capifcore/internal/common29122"
34         publishapi "oransc.org/nonrtric/capifcore/internal/publishserviceapi"
35
36         "oransc.org/nonrtric/capifcore/internal/helmmanagement"
37         "oransc.org/nonrtric/capifcore/internal/providermanagement"
38
39         log "github.com/sirupsen/logrus"
40 )
41
42 //go:generate mockery --name PublishRegister
43 type PublishRegister interface {
44         // Checks if the provided APIs are published.
45         // Returns true if all provided APIs have been published, false otherwise.
46         AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool
47         // Checks if the provided API is published.
48         // Returns true if the provided API has been published, false otherwise.
49         IsAPIPublished(aefId, path string) bool
50         // Gets all published APIs.
51         // Returns a list of all APIs that has been published.
52         GetAllPublishedServices() []publishapi.ServiceAPIDescription
53 }
54
55 type PublishService struct {
56         publishedServices map[string][]publishapi.ServiceAPIDescription
57         serviceRegister   providermanagement.ServiceRegister
58         helmManager       helmmanagement.HelmManager
59         lock              sync.Mutex
60 }
61
62 // Creates a service that implements both the PublishRegister and the publishserviceapi.ServerInterface interfaces.
63 func NewPublishService(serviceRegister providermanagement.ServiceRegister, hm helmmanagement.HelmManager) *PublishService {
64         return &PublishService{
65                 helmManager:       hm,
66                 publishedServices: make(map[string][]publishapi.ServiceAPIDescription),
67                 serviceRegister:   serviceRegister,
68         }
69 }
70
71 func (ps *PublishService) AreAPIsPublished(serviceDescriptions *[]publishapi.ServiceAPIDescription) bool {
72
73         if serviceDescriptions != nil {
74                 registeredApis := ps.getAllAefIds()
75                 return checkNewDescriptions(*serviceDescriptions, registeredApis)
76         }
77         return true
78 }
79
80 func (ps *PublishService) getAllAefIds() []string {
81         ps.lock.Lock()
82         defer ps.lock.Unlock()
83
84         allIds := []string{}
85         for _, descriptions := range ps.publishedServices {
86                 for _, description := range descriptions {
87                         allIds = append(allIds, getIdsFromDescription(description)...)
88                 }
89         }
90         return allIds
91 }
92
93 func getIdsFromDescription(description publishapi.ServiceAPIDescription) []string {
94         allIds := []string{}
95         if description.AefProfiles != nil {
96                 for _, aefProfile := range *description.AefProfiles {
97                         allIds = append(allIds, aefProfile.AefId)
98                 }
99         }
100         return allIds
101 }
102
103 func checkNewDescriptions(newDescriptions []publishapi.ServiceAPIDescription, registeredAefIds []string) bool {
104         registered := true
105         for _, newApi := range newDescriptions {
106                 if !checkProfiles(newApi.AefProfiles, registeredAefIds) {
107                         registered = false
108                         break
109                 }
110         }
111         return registered
112 }
113
114 func checkProfiles(newProfiles *[]publishapi.AefProfile, registeredAefIds []string) bool {
115         allRegistered := true
116         if newProfiles != nil {
117                 for _, profile := range *newProfiles {
118                         if !slices.Contains(registeredAefIds, profile.AefId) {
119                                 allRegistered = false
120                                 break
121                         }
122                 }
123         }
124         return allRegistered
125 }
126
127 func (ps *PublishService) IsAPIPublished(aefId, path string) bool {
128         return slices.Contains(ps.getAllAefIds(), aefId)
129 }
130
131 func (ps *PublishService) GetAllPublishedServices() []publishapi.ServiceAPIDescription {
132         publishedDescriptions := []publishapi.ServiceAPIDescription{}
133         for _, descriptions := range ps.publishedServices {
134                 publishedDescriptions = append(publishedDescriptions, descriptions...)
135         }
136         return publishedDescriptions
137 }
138
139 // Retrieve all published APIs.
140 func (ps *PublishService) GetApfIdServiceApis(ctx echo.Context, apfId string) error {
141         serviceDescriptions, ok := ps.publishedServices[apfId]
142         if ok {
143                 err := ctx.JSON(http.StatusOK, serviceDescriptions)
144                 if err != nil {
145                         // Something really bad happened, tell Echo that our handler failed
146                         return err
147                 }
148         } else {
149                 return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Provider %s not registered", apfId))
150         }
151
152         return nil
153 }
154
155 // Publish a new API.
156 func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) error {
157         var newServiceAPIDescription publishapi.ServiceAPIDescription
158         err := ctx.Bind(&newServiceAPIDescription)
159         if err != nil {
160                 return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for service "+apfId)
161         }
162
163         ps.lock.Lock()
164         defer ps.lock.Unlock()
165
166         registeredFuncs := ps.serviceRegister.GetAefsForPublisher(apfId)
167         for _, profile := range *newServiceAPIDescription.AefProfiles {
168                 if !slices.Contains(registeredFuncs, profile.AefId) {
169                         return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Function %s not registered", profile.AefId))
170                 }
171         }
172
173         newId := "api_id_" + newServiceAPIDescription.ApiName
174         newServiceAPIDescription.ApiId = &newId
175
176         shouldReturn, returnValue := ps.installHelmChart(newServiceAPIDescription, ctx)
177         if shouldReturn {
178                 return returnValue
179         }
180
181         _, ok := ps.publishedServices[apfId]
182         if ok {
183                 ps.publishedServices[apfId] = append(ps.publishedServices[apfId], newServiceAPIDescription)
184         } else {
185                 ps.publishedServices[apfId] = append([]publishapi.ServiceAPIDescription{}, newServiceAPIDescription)
186         }
187
188         uri := ctx.Request().Host + ctx.Request().URL.String()
189         ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, *newServiceAPIDescription.ApiId))
190         err = ctx.JSON(http.StatusCreated, newServiceAPIDescription)
191         if err != nil {
192                 // Something really bad happened, tell Echo that our handler failed
193                 return err
194         }
195
196         return nil
197 }
198
199 func (ps *PublishService) installHelmChart(newServiceAPIDescription publishapi.ServiceAPIDescription, ctx echo.Context) (bool, error) {
200         info := strings.Split(*newServiceAPIDescription.Description, ",")
201         if len(info) == 5 {
202                 err := ps.helmManager.InstallHelmChart(info[1], info[2], info[3], info[4])
203                 if err != nil {
204                         return true, sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf("Unable to install Helm chart %s due to: %s", info[3], err.Error()))
205                 }
206                 log.Debug("Installed service: ", newServiceAPIDescription.ApiId)
207         }
208         return false, nil
209 }
210
211 // Unpublish a published service API.
212 func (ps *PublishService) DeleteApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
213         serviceDescriptions, ok := ps.publishedServices[string(apfId)]
214         if ok {
215                 pos, description := getServiceDescription(serviceApiId, serviceDescriptions)
216                 if description != nil {
217                         info := strings.Split(*description.Description, ",")
218                         if len(info) == 5 {
219                                 ps.helmManager.UninstallHelmChart(info[1], info[3])
220                                 log.Debug("Deleted service: ", serviceApiId)
221                         }
222                         ps.lock.Lock()
223                         defer ps.lock.Unlock()
224                         ps.publishedServices[string(apfId)] = removeServiceDescription(pos, serviceDescriptions)
225                 }
226         }
227         return ctx.NoContent(http.StatusNoContent)
228 }
229
230 // Retrieve a published service API.
231 func (ps *PublishService) GetApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
232         ps.lock.Lock()
233         defer ps.lock.Unlock()
234
235         serviceDescriptions, ok := ps.publishedServices[apfId]
236         if ok {
237                 _, serviceDescription := getServiceDescription(serviceApiId, serviceDescriptions)
238                 if serviceDescription == nil {
239                         return ctx.NoContent(http.StatusNotFound)
240                 }
241                 err := ctx.JSON(http.StatusOK, serviceDescription)
242                 if err != nil {
243                         // Something really bad happened, tell Echo that our handler failed
244                         return err
245                 }
246
247                 return nil
248         }
249         return ctx.NoContent(http.StatusNotFound)
250 }
251
252 func getServiceDescription(serviceApiId string, descriptions []publishapi.ServiceAPIDescription) (int, *publishapi.ServiceAPIDescription) {
253         for pos, description := range descriptions {
254                 if serviceApiId == *description.ApiId {
255                         return pos, &description
256                 }
257         }
258         return -1, nil
259 }
260
261 func removeServiceDescription(i int, a []publishapi.ServiceAPIDescription) []publishapi.ServiceAPIDescription {
262         a[i] = a[len(a)-1]                               // Copy last element to index i.
263         a[len(a)-1] = publishapi.ServiceAPIDescription{} // Erase last element (write zero value).
264         a = a[:len(a)-1]                                 // Truncate slice.
265         return a
266 }
267
268 // Modify an existing published service API.
269 func (ps *PublishService) ModifyIndAPFPubAPI(ctx echo.Context, apfId string, serviceApiId string) error {
270         return ctx.NoContent(http.StatusNotImplemented)
271 }
272
273 // Update a published service API.
274 func (ps *PublishService) PutApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
275         ps.lock.Lock()
276         defer ps.lock.Unlock()
277
278         pos, publishedService, shouldReturn, returnValue := ps.checkIfServiceIsPublished(apfId, serviceApiId, ctx)
279         if shouldReturn {
280                 return returnValue
281         }
282
283         updatedServiceDescription, shouldReturn, returnValue := getServiceFromRequest(ctx)
284         if shouldReturn {
285                 return returnValue
286         }
287
288         if updatedServiceDescription.Description != nil {
289                 publishedService.Description = updatedServiceDescription.Description
290                 ps.publishedServices[apfId][pos] = publishedService
291         }
292
293         err := ctx.JSON(http.StatusOK, ps.publishedServices[apfId][pos])
294         if err != nil {
295                 // Something really bad happened, tell Echo that our handler failed
296                 return err
297         }
298
299         return nil
300 }
301
302 func (ps *PublishService) checkIfServiceIsPublished(apfId string, serviceApiId string, ctx echo.Context) (int, publishapi.ServiceAPIDescription, bool, error) {
303
304         publishedServices, ok := ps.publishedServices[apfId]
305         if !ok {
306                 return 0, publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Service must be published before updating it")
307         } else {
308                 for pos, description := range publishedServices {
309                         if *description.ApiId == serviceApiId {
310                                 return pos, description, false, nil
311
312                         }
313
314                 }
315
316         }
317         return 0, publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Service must be published before updating it")
318
319 }
320
321 func getServiceFromRequest(ctx echo.Context) (publishapi.ServiceAPIDescription, bool, error) {
322         var updatedServiceDescription publishapi.ServiceAPIDescription
323         err := ctx.Bind(&updatedServiceDescription)
324         if err != nil {
325                 return publishapi.ServiceAPIDescription{}, true, sendCoreError(ctx, http.StatusBadRequest, "Invalid format for service")
326         }
327         return updatedServiceDescription, false, nil
328 }
329
330 // This function wraps sending of an error in the Error format, and
331 // handling the failure to marshal that.
332 func sendCoreError(ctx echo.Context, code int, message string) error {
333         pd := common29122.ProblemDetails{
334                 Cause:  &message,
335                 Status: &code,
336         }
337         err := ctx.JSON(code, pd)
338         return err
339 }