Improve error messages and tests
[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         "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         AreAPIsPublished(serviceDescriptions *[]publishserviceapi.ServiceAPIDescription) bool
45         IsAPIPublished(aefId, path string) bool
46 }
47
48 type PublishService struct {
49         publishedServices map[string][]*publishserviceapi.ServiceAPIDescription
50         serviceRegister   providermanagement.ServiceRegister
51         helmManager       helmmanagement.HelmManager
52         lock              sync.Mutex
53 }
54
55 func NewPublishService(serviceRegister providermanagement.ServiceRegister, hm helmmanagement.HelmManager) *PublishService {
56         return &PublishService{
57                 helmManager:       hm,
58                 publishedServices: make(map[string][]*publishserviceapi.ServiceAPIDescription),
59                 serviceRegister:   serviceRegister,
60         }
61 }
62
63 func (ps *PublishService) AreAPIsPublished(serviceDescriptions *[]publishserviceapi.ServiceAPIDescription) bool {
64
65         if serviceDescriptions != nil {
66                 registeredApis := ps.getAllAefIds()
67                 return checkNewDescriptions(*serviceDescriptions, registeredApis)
68         }
69         return true
70 }
71
72 func (ps *PublishService) getAllAefIds() []string {
73         ps.lock.Lock()
74         defer ps.lock.Unlock()
75
76         allIds := []string{}
77         for _, descriptions := range ps.publishedServices {
78                 for _, description := range descriptions {
79                         allIds = append(allIds, getIdsFromDescription(*description)...)
80                 }
81         }
82         return allIds
83 }
84
85 func getIdsFromDescription(description publishserviceapi.ServiceAPIDescription) []string {
86         allIds := []string{}
87         if description.AefProfiles != nil {
88                 for _, aefProfile := range *description.AefProfiles {
89                         allIds = append(allIds, aefProfile.AefId)
90                 }
91         }
92         return allIds
93 }
94
95 func checkNewDescriptions(newDescriptions []publishserviceapi.ServiceAPIDescription, registeredAefIds []string) bool {
96         registered := true
97         for _, newApi := range newDescriptions {
98                 if !checkProfiles(newApi.AefProfiles, registeredAefIds) {
99                         registered = false
100                         break
101                 }
102         }
103         return registered
104 }
105
106 func checkProfiles(newProfiles *[]publishserviceapi.AefProfile, registeredAefIds []string) bool {
107         allRegistered := true
108         if newProfiles != nil {
109                 for _, profile := range *newProfiles {
110                         if !slices.Contains(registeredAefIds, profile.AefId) {
111                                 allRegistered = false
112                                 break
113                         }
114                 }
115         }
116         return allRegistered
117 }
118
119 func (ps *PublishService) IsAPIPublished(aefId, path string) bool {
120         return slices.Contains(ps.getAllAefIds(), aefId)
121 }
122
123 func (ps *PublishService) GetApfIdServiceApis(ctx echo.Context, apfId string) error {
124         serviceDescriptions, ok := ps.publishedServices[apfId]
125         if ok {
126                 err := ctx.JSON(http.StatusOK, serviceDescriptions)
127                 if err != nil {
128                         // Something really bad happened, tell Echo that our handler failed
129                         return err
130                 }
131         } else {
132                 return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Provider %s not registered", apfId))
133         }
134
135         return nil
136 }
137
138 func (ps *PublishService) PostApfIdServiceApis(ctx echo.Context, apfId string) error {
139         var newServiceAPIDescription publishserviceapi.ServiceAPIDescription
140         err := ctx.Bind(&newServiceAPIDescription)
141         if err != nil {
142                 return sendCoreError(ctx, http.StatusBadRequest, "Invalid format for service "+apfId)
143         }
144
145         ps.lock.Lock()
146         defer ps.lock.Unlock()
147
148         registeredFuncs := ps.serviceRegister.GetAefsForPublisher(apfId)
149         for _, profile := range *newServiceAPIDescription.AefProfiles {
150                 if !slices.Contains(registeredFuncs, profile.AefId) {
151                         return sendCoreError(ctx, http.StatusNotFound, fmt.Sprintf("Function %s not registered", profile.AefId))
152                 }
153         }
154
155         newId := "api_id_" + newServiceAPIDescription.ApiName
156         newServiceAPIDescription.ApiId = &newId
157
158         shouldReturn, returnValue := ps.installHelmChart(newServiceAPIDescription, ctx)
159         if shouldReturn {
160                 return returnValue
161         }
162
163         _, ok := ps.publishedServices[apfId]
164         if ok {
165                 ps.publishedServices[apfId] = append(ps.publishedServices[apfId], &newServiceAPIDescription)
166         } else {
167                 ps.publishedServices[apfId] = append([]*publishserviceapi.ServiceAPIDescription{}, &newServiceAPIDescription)
168         }
169
170         uri := ctx.Request().Host + ctx.Request().URL.String()
171         ctx.Response().Header().Set(echo.HeaderLocation, ctx.Scheme()+`://`+path.Join(uri, *newServiceAPIDescription.ApiId))
172         err = ctx.JSON(http.StatusCreated, newServiceAPIDescription)
173         if err != nil {
174                 // Something really bad happened, tell Echo that our handler failed
175                 return err
176         }
177
178         return nil
179 }
180
181 func (ps *PublishService) installHelmChart(newServiceAPIDescription publishserviceapi.ServiceAPIDescription, ctx echo.Context) (bool, error) {
182         info := strings.Split(*newServiceAPIDescription.Description, ",")
183         if len(info) == 5 {
184                 err := ps.helmManager.InstallHelmChart(info[1], info[2], info[3], info[4])
185                 if err != nil {
186                         return true, sendCoreError(ctx, http.StatusBadRequest, fmt.Sprintf("Unable to install Helm chart %s due to: %s", info[3], err.Error()))
187                 }
188                 log.Debug("Installed service: ", newServiceAPIDescription.ApiId)
189         }
190         return false, nil
191 }
192
193 func (ps *PublishService) DeleteApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
194         serviceDescriptions, ok := ps.publishedServices[string(apfId)]
195         if ok {
196                 pos, description := getServiceDescription(serviceApiId, serviceDescriptions)
197                 if description != nil {
198                         info := strings.Split(*description.Description, ",")
199                         if len(info) == 5 {
200                                 ps.helmManager.UninstallHelmChart(info[1], info[3])
201                                 log.Debug("Deleted service: ", serviceApiId)
202                         }
203                         ps.lock.Lock()
204                         defer ps.lock.Unlock()
205                         ps.publishedServices[string(apfId)] = removeServiceDescription(pos, serviceDescriptions)
206                 }
207         }
208         return ctx.NoContent(http.StatusNoContent)
209 }
210
211 func (ps *PublishService) GetApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
212         ps.lock.Lock()
213         defer ps.lock.Unlock()
214
215         serviceDescriptions, ok := ps.publishedServices[apfId]
216         if ok {
217                 _, serviceDescription := getServiceDescription(serviceApiId, serviceDescriptions)
218                 if serviceDescription == nil {
219                         return ctx.NoContent(http.StatusNotFound)
220                 }
221                 err := ctx.JSON(http.StatusOK, serviceDescription)
222                 if err != nil {
223                         // Something really bad happened, tell Echo that our handler failed
224                         return err
225                 }
226
227                 return nil
228         }
229         return ctx.NoContent(http.StatusNotFound)
230 }
231
232 func getServiceDescription(serviceApiId string, descriptions []*publishserviceapi.ServiceAPIDescription) (int, *publishserviceapi.ServiceAPIDescription) {
233         for pos, description := range descriptions {
234                 if serviceApiId == *description.ApiId {
235                         return pos, description
236                 }
237         }
238         return -1, nil
239 }
240
241 func removeServiceDescription(i int, a []*publishserviceapi.ServiceAPIDescription) []*publishserviceapi.ServiceAPIDescription {
242         a[i] = a[len(a)-1] // Copy last element to index i.
243         a[len(a)-1] = nil  // Erase last element (write zero value).
244         a = a[:len(a)-1]   // Truncate slice.
245         return a
246 }
247
248 func (ps *PublishService) ModifyIndAPFPubAPI(ctx echo.Context, apfId string, serviceApiId string) error {
249         return ctx.NoContent(http.StatusNotImplemented)
250 }
251
252 func (ps *PublishService) PutApfIdServiceApisServiceApiId(ctx echo.Context, apfId string, serviceApiId string) error {
253         return ctx.NoContent(http.StatusNotImplemented)
254 }
255
256 // This function wraps sending of an error in the Error format, and
257 // handling the failure to marshal that.
258 func sendCoreError(ctx echo.Context, code int, message string) error {
259         pd := common29122.ProblemDetails{
260                 Cause:  &message,
261                 Status: &code,
262         }
263         err := ctx.JSON(code, pd)
264         return err
265 }