01d924f9c65e5e060a092ac9c26677107b8ebab5
[multicloud/k8s.git] / src / k8splugin / internal / app / instance.go
1 /*
2  * Copyright 2018 Intel Corporation, Inc
3  * Copyright © 2021 Samsung Electronics
4  * Copyright © 2021 Orange
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 package app
20
21 import (
22         "encoding/json"
23         "log"
24         "strings"
25
26         "github.com/onap/multicloud-k8s/src/k8splugin/internal/db"
27         "github.com/onap/multicloud-k8s/src/k8splugin/internal/helm"
28         "github.com/onap/multicloud-k8s/src/k8splugin/internal/namegenerator"
29         "github.com/onap/multicloud-k8s/src/k8splugin/internal/rb"
30
31         pkgerrors "github.com/pkg/errors"
32 )
33
34 // InstanceRequest contains the parameters needed for instantiation
35 // of profiles
36 type InstanceRequest struct {
37         RBName         string            `json:"rb-name"`
38         RBVersion      string            `json:"rb-version"`
39         ProfileName    string            `json:"profile-name"`
40         ReleaseName    string            `json:"release-name"`
41         CloudRegion    string            `json:"cloud-region"`
42         Labels         map[string]string `json:"labels"`
43         OverrideValues map[string]string `json:"override-values"`
44 }
45
46 // InstanceResponse contains the response from instantiation
47 type InstanceResponse struct {
48         ID          string                    `json:"id"`
49         Request     InstanceRequest           `json:"request"`
50         Namespace   string                    `json:"namespace"`
51         ReleaseName string                    `json:"release-name"`
52         Resources   []helm.KubernetesResource `json:"resources"`
53         Hooks       []*helm.Hook              `json:"-"`
54 }
55
56 // InstanceMiniResponse contains the response from instantiation
57 // It does NOT include the created resources.
58 // Use the regular GET to get the created resources for a particular instance
59 type InstanceMiniResponse struct {
60         ID          string          `json:"id"`
61         Request     InstanceRequest `json:"request"`
62         ReleaseName string          `json:"release-name"`
63         Namespace   string          `json:"namespace"`
64 }
65
66 // InstanceStatus is what is returned when status is queried for an instance
67 type InstanceStatus struct {
68         Request         InstanceRequest  `json:"request"`
69         Ready           bool             `json:"ready"`
70         ResourceCount   int32            `json:"resourceCount"`
71         ResourcesStatus []ResourceStatus `json:"resourcesStatus"`
72 }
73
74 // InstanceManager is an interface exposes the instantiation functionality
75 type InstanceManager interface {
76         Create(i InstanceRequest) (InstanceResponse, error)
77         Get(id string) (InstanceResponse, error)
78         Status(id string) (InstanceStatus, error)
79         Query(id, apiVersion, kind, name, labels string) (InstanceStatus, error)
80         List(rbname, rbversion, profilename string) ([]InstanceMiniResponse, error)
81         Find(rbName string, ver string, profile string, labelKeys map[string]string) ([]InstanceMiniResponse, error)
82         Delete(id string) error
83 }
84
85 // InstanceKey is used as the primary key in the db
86 type InstanceKey struct {
87         ID string `json:"id"`
88 }
89
90 // We will use json marshalling to convert to string to
91 // preserve the underlying structure.
92 func (dk InstanceKey) String() string {
93         out, err := json.Marshal(dk)
94         if err != nil {
95                 return ""
96         }
97
98         return string(out)
99 }
100
101 // InstanceClient implements the InstanceManager interface
102 // It will also be used to maintain some localized state
103 type InstanceClient struct {
104         storeName string
105         tagInst   string
106 }
107
108 // NewInstanceClient returns an instance of the InstanceClient
109 // which implements the InstanceManager
110 func NewInstanceClient() *InstanceClient {
111         return &InstanceClient{
112                 storeName: "rbdef",
113                 tagInst:   "instance",
114         }
115 }
116
117 // Simplified function to retrieve model data from instance ID
118 func resolveModelFromInstance(instanceID string) (rbName, rbVersion, profileName, releaseName string, err error) {
119         v := NewInstanceClient()
120         resp, err := v.Get(instanceID)
121         if err != nil {
122                 return "", "", "", "", pkgerrors.Wrap(err, "Getting instance")
123         }
124         return resp.Request.RBName, resp.Request.RBVersion, resp.Request.ProfileName, resp.ReleaseName, nil
125 }
126
127 // Create an instance of rb on the cluster  in the database
128 func (v *InstanceClient) Create(i InstanceRequest) (InstanceResponse, error) {
129
130         // Name is required
131         if i.RBName == "" || i.RBVersion == "" || i.ProfileName == "" || i.CloudRegion == "" {
132                 return InstanceResponse{},
133                         pkgerrors.New("RBName, RBversion, ProfileName, CloudRegion are required to create a new instance")
134         }
135
136         //Check if profile exists
137         profile, err := rb.NewProfileClient().Get(i.RBName, i.RBVersion, i.ProfileName)
138         if err != nil {
139                 return InstanceResponse{}, pkgerrors.New("Unable to find Profile to create instance")
140         }
141
142         //Convert override values from map to array of strings of the following format
143         //foo=bar
144         overrideValues := []string{}
145         if i.OverrideValues != nil {
146                 for k, v := range i.OverrideValues {
147                         overrideValues = append(overrideValues, k+"="+v)
148                 }
149         }
150
151         //Execute the kubernetes create command
152         sortedTemplates, hookList, releaseName, err := rb.NewProfileClient().Resolve(i.RBName, i.RBVersion, i.ProfileName, overrideValues, i.ReleaseName)
153         if err != nil {
154                 return InstanceResponse{}, pkgerrors.Wrap(err, "Error resolving helm charts")
155         }
156
157         // TODO: Only generate if id is not provided
158         id := namegenerator.Generate()
159
160         k8sClient := KubernetesClient{}
161         err = k8sClient.Init(i.CloudRegion, id)
162         if err != nil {
163                 return InstanceResponse{}, pkgerrors.Wrap(err, "Getting CloudRegion Information")
164         }
165
166         createdResources, err := k8sClient.createResources(sortedTemplates, profile.Namespace)
167         if err != nil {
168                 return InstanceResponse{}, pkgerrors.Wrap(err, "Create Kubernetes Resources")
169         }
170
171         //Compose the return response
172         resp := InstanceResponse{
173                 ID:          id,
174                 Request:     i,
175                 Namespace:   profile.Namespace,
176                 ReleaseName: releaseName,
177                 Resources:   createdResources,
178                 Hooks:       hookList,
179         }
180
181         key := InstanceKey{
182                 ID: id,
183         }
184         err = db.DBconn.Create(v.storeName, key, v.tagInst, resp)
185         if err != nil {
186                 return InstanceResponse{}, pkgerrors.Wrap(err, "Creating Instance DB Entry")
187         }
188
189         return resp, nil
190 }
191
192 // Get returns the instance for corresponding ID
193 func (v *InstanceClient) Get(id string) (InstanceResponse, error) {
194         key := InstanceKey{
195                 ID: id,
196         }
197         value, err := db.DBconn.Read(v.storeName, key, v.tagInst)
198         if err != nil {
199                 return InstanceResponse{}, pkgerrors.Wrap(err, "Get Instance")
200         }
201
202         //value is a byte array
203         if value != nil {
204                 resp := InstanceResponse{}
205                 err = db.DBconn.Unmarshal(value, &resp)
206                 if err != nil {
207                         return InstanceResponse{}, pkgerrors.Wrap(err, "Unmarshaling Instance Value")
208                 }
209                 return resp, nil
210         }
211
212         return InstanceResponse{}, pkgerrors.New("Error getting Instance")
213 }
214
215 // Query returns state of instance's filtered resources
216 func (v *InstanceClient) Query(id, apiVersion, kind, name, labels string) (InstanceStatus, error) {
217
218         queryClient := NewQueryClient()
219         //Read the status from the DB
220         key := InstanceKey{
221                 ID: id,
222         }
223         value, err := db.DBconn.Read(v.storeName, key, v.tagInst)
224         if err != nil {
225                 return InstanceStatus{}, pkgerrors.Wrap(err, "Get Instance")
226         }
227         if value == nil { //value is a byte array
228                 return InstanceStatus{}, pkgerrors.New("Status is not available")
229         }
230         resResp := InstanceResponse{}
231         err = db.DBconn.Unmarshal(value, &resResp)
232         if err != nil {
233                 return InstanceStatus{}, pkgerrors.Wrap(err, "Unmarshaling Instance Value")
234         }
235
236         resources, err := queryClient.Query(resResp.Namespace, resResp.Request.CloudRegion, apiVersion, kind, name, labels, id)
237         if err != nil {
238                 return InstanceStatus{}, pkgerrors.Wrap(err, "Querying Resources")
239         }
240
241         resp := InstanceStatus{
242                 Request:         resResp.Request,
243                 ResourceCount:   resources.ResourceCount,
244                 ResourcesStatus: resources.ResourcesStatus,
245         }
246         return resp, nil
247 }
248
249 // Status returns the status for the instance
250 func (v *InstanceClient) Status(id string) (InstanceStatus, error) {
251
252         //Read the status from the DB
253         key := InstanceKey{
254                 ID: id,
255         }
256
257         value, err := db.DBconn.Read(v.storeName, key, v.tagInst)
258         if err != nil {
259                 return InstanceStatus{}, pkgerrors.Wrap(err, "Get Instance")
260         }
261
262         //value is a byte array
263         if value == nil {
264                 return InstanceStatus{}, pkgerrors.New("Status is not available")
265         }
266
267         resResp := InstanceResponse{}
268         err = db.DBconn.Unmarshal(value, &resResp)
269         if err != nil {
270                 return InstanceStatus{}, pkgerrors.Wrap(err, "Unmarshaling Instance Value")
271         }
272
273         k8sClient := KubernetesClient{}
274         err = k8sClient.Init(resResp.Request.CloudRegion, id)
275         if err != nil {
276                 return InstanceStatus{}, pkgerrors.Wrap(err, "Getting CloudRegion Information")
277         }
278
279         cumulatedErrorMsg := make([]string, 0)
280         podsStatus, err := k8sClient.getPodsByLabel(resResp.Namespace)
281         if err != nil {
282                 cumulatedErrorMsg = append(cumulatedErrorMsg, err.Error())
283         }
284
285         generalStatus := make([]ResourceStatus, 0, len(resResp.Resources))
286 Main:
287         for _, resource := range resResp.Resources {
288                 for _, pod := range podsStatus {
289                         if resource.GVK == pod.GVK && resource.Name == pod.Name {
290                                 continue Main //Don't double check pods if someone decided to define pod explicitly in helm chart
291                         }
292                 }
293                 status, err := k8sClient.GetResourceStatus(resource, resResp.Namespace)
294                 if err != nil {
295                         cumulatedErrorMsg = append(cumulatedErrorMsg, err.Error())
296                 } else {
297                         generalStatus = append(generalStatus, status)
298                 }
299         }
300         resp := InstanceStatus{
301                 Request:         resResp.Request,
302                 ResourceCount:   int32(len(generalStatus) + len(podsStatus)),
303                 Ready:           false, //FIXME To determine readiness, some parsing of status fields is necessary
304                 ResourcesStatus: append(generalStatus, podsStatus...),
305         }
306
307         if len(cumulatedErrorMsg) != 0 {
308                 err = pkgerrors.New("Getting Resources Status:\n" +
309                         strings.Join(cumulatedErrorMsg, "\n"))
310                 return resp, err
311         }
312         //TODO Filter response content by requested verbosity (brief, ...)?
313
314         return resp, nil
315 }
316
317 // List returns the instance for corresponding ID
318 // Empty string returns all
319 func (v *InstanceClient) List(rbname, rbversion, profilename string) ([]InstanceMiniResponse, error) {
320
321         dbres, err := db.DBconn.ReadAll(v.storeName, v.tagInst)
322         if err != nil || len(dbres) == 0 {
323                 return []InstanceMiniResponse{}, pkgerrors.Wrap(err, "Listing Instances")
324         }
325
326         var results []InstanceMiniResponse
327
328         for key, value := range dbres {
329                 //value is a byte array
330                 if value != nil {
331                         resp := InstanceResponse{}
332                         err = db.DBconn.Unmarshal(value, &resp)
333                         if err != nil {
334                                 log.Printf("[Instance] Error: %s Unmarshaling Instance: %s", err.Error(), key)
335                         }
336
337                         miniresp := InstanceMiniResponse{
338                                 ID:          resp.ID,
339                                 Request:     resp.Request,
340                                 Namespace:   resp.Namespace,
341                                 ReleaseName: resp.ReleaseName,
342                         }
343
344                         //Filter based on the accepted keys
345                         if len(rbname) != 0 &&
346                                 miniresp.Request.RBName != rbname {
347                                 continue
348                         }
349                         if len(rbversion) != 0 &&
350                                 miniresp.Request.RBVersion != rbversion {
351                                 continue
352                         }
353                         if len(profilename) != 0 &&
354                                 miniresp.Request.ProfileName != profilename {
355                                 continue
356                         }
357
358                         results = append(results, miniresp)
359                 }
360         }
361
362         return results, nil
363 }
364
365 // Find returns the instances that match the given criteria
366 // If version is empty, it will return all instances for a given rbName
367 // If profile is empty, it will return all instances for a given rbName+version
368 // If labelKeys are provided, the results are filtered based on that.
369 // It is an AND operation for labelkeys.
370 func (v *InstanceClient) Find(rbName string, version string, profile string, labelKeys map[string]string) ([]InstanceMiniResponse, error) {
371         if rbName == "" && len(labelKeys) == 0 {
372                 return []InstanceMiniResponse{}, pkgerrors.New("rbName or labelkeys is required and cannot be empty")
373         }
374
375         responses, err := v.List(rbName, version, profile)
376         if err != nil {
377                 return []InstanceMiniResponse{}, pkgerrors.Wrap(err, "Listing Instances")
378         }
379
380         ret := []InstanceMiniResponse{}
381
382         //filter the list by labelKeys now
383         for _, resp := range responses {
384
385                 add := true
386                 for k, v := range labelKeys {
387                         if resp.Request.Labels[k] != v {
388                                 add = false
389                                 break
390                         }
391                 }
392                 // If label was not found in the response, don't add it
393                 if add {
394                         ret = append(ret, resp)
395                 }
396
397         }
398
399         return ret, nil
400 }
401
402 // Delete the Instance from database
403 func (v *InstanceClient) Delete(id string) error {
404         inst, err := v.Get(id)
405         if err != nil {
406                 return pkgerrors.Wrap(err, "Error getting Instance")
407         }
408
409         k8sClient := KubernetesClient{}
410         err = k8sClient.Init(inst.Request.CloudRegion, inst.ID)
411         if err != nil {
412                 return pkgerrors.Wrap(err, "Getting CloudRegion Information")
413         }
414
415         err = k8sClient.deleteResources(inst.Resources, inst.Namespace)
416         if err != nil {
417                 return pkgerrors.Wrap(err, "Deleting Instance Resources")
418         }
419
420         key := InstanceKey{
421                 ID: id,
422         }
423         err = db.DBconn.Delete(v.storeName, key, v.tagInst)
424         if err != nil {
425                 return pkgerrors.Wrap(err, "Delete Instance")
426         }
427
428         return nil
429 }