9dec9f7dbad595f0ea978ea5b61d49881661f1db
[nonrtric.git] / service-exposure / rapps-helm-installer.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 package main
21
22 import (
23         "context"
24         "database/sql"
25         "encoding/json"
26         "flag"
27         "fmt"
28         "github.com/pkg/errors"
29         "gopkg.in/yaml.v2"
30         "helm.sh/helm/v3/pkg/action"
31         "helm.sh/helm/v3/pkg/chart"
32         "helm.sh/helm/v3/pkg/chart/loader"
33         "helm.sh/helm/v3/pkg/cli"
34         "helm.sh/helm/v3/pkg/getter"
35         "helm.sh/helm/v3/pkg/kube"
36         "helm.sh/helm/v3/pkg/repo"
37         "io/ioutil"
38         metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
39         "k8s.io/cli-runtime/pkg/genericclioptions"
40         kubernetes "k8s.io/client-go/kubernetes"
41         "k8s.io/client-go/rest"
42         "net/http"
43         "os"
44         "path/filepath"
45 )
46
47 var settings *cli.EnvSettings
48 var chartRequested *chart.Chart
49
50 //var url string
51 var repoName string
52 var chartName string
53 var releaseName string
54 var namespace string
55
56 type ChartInfo struct {
57         Name       string                 `json:",omitempty"`
58         Namespace  string                 `json:",omitempty"`
59         Revision   int                    `json:",omitempty"`
60         Updated    string                 `json:",omitempty"`
61         Status     string                 `json:",omitempty"`
62         Chart      string                 `json:",omitempty"`
63         AppVersion string                 `json:",omitempty"`
64         Values     map[string]interface{} `json:"-"`
65 }
66
67 type Rapp struct {
68         Type            string
69         SecurityEnabled bool
70         Realm           string
71         Client          string
72         Authenticator   string
73         Roles           []struct {
74                 Role   string
75                 Grants []string
76         }
77         Apps []struct {
78                 Prefix  string
79                 Methods []string
80         }
81 }
82
83 var rapp Rapp
84
85 const (
86         host     = "postgres.default"
87         port     = 5432
88         user     = "capif"
89         password = "capif"
90         dbname   = "capif"
91 )
92
93 func runInstall(res http.ResponseWriter, req *http.Request) {
94         query := req.URL.Query()
95         chartName = query.Get("chart")
96         releaseName = chartName
97         fmt.Println("Installing ", chartName)
98
99         var msg string
100         var chart string
101         var install *action.Install
102         chartMuseumService, chartMuseumPort := findService("chartmuseum", "default")
103         fmt.Printf("Chart Museum service:%s, Port:%d\n", chartMuseumService, chartMuseumPort)
104         url := "http://" + chartMuseumService + ":" + fmt.Sprint(chartMuseumPort)
105         if !chartInstalled(chartName) {
106                 // Add repo
107                 fmt.Printf("Adding %s to Helm Repo\n", url)
108                 _, err := addToRepo(url)
109                 if err != nil {
110                         msg = err.Error()
111                 } else {
112                         install, err = dryRun()
113                         if err != nil {
114                                 msg = err.Error()
115                         } else {
116                                 err := installSecurity(rapp)
117                                 if err != nil {
118                                         msg = err.Error()
119                                 } else {
120                                         fmt.Printf("Installing chart %s to %s namespace\n", chartName, namespace)
121                                         chart, err = installHelmChart(install)
122                                         if err != nil {
123                                                 msg = "Error occurred during installation " + err.Error()
124                                         } else {
125                                                 msg = "Successfully installed release: " + chart
126                                         }
127
128                                 }
129                         }
130                 }
131                 registrerRapp(chartName, rapp.Type)
132         } else {
133                 msg = chartName + " has already been installed"
134         }
135
136         // create response binary data
137         data := []byte(msg) // slice of bytes
138         // write `data` to response
139         res.Write(data)
140 }
141
142 func installSecurity(rapp Rapp) error {
143         var url string
144         var params string
145         role := rapp.Roles[0].Role
146         grants := rapp.Roles[0].Grants[0]
147         realm := rapp.Realm
148         client := rapp.Client
149         authenticator := rapp.Authenticator
150
151         if !rapp.SecurityEnabled {
152                 return nil
153         }
154         // Different security requirements depending on the rapp type
155         if rapp.Type == "provider" {
156                 // keycloak client setup
157                 fmt.Println("Setting up keycloak")
158                 url = "http://rapps-keycloak-mgr.default/create?"
159                 params = "realm=" + realm + "&name=" + client + "&role=" + role + "&authType=" + authenticator
160                 url += params
161                 _, err := http.Get(url)
162                 if err != nil {
163                         return err
164                 } else {
165                         fmt.Println("Setting up istio")
166                         url = "http://rapps-istio-mgr.default/create-policy?"
167                         params = "name=" + chartName + "&realm=" + realm + "&role=" + role + "&method=" + grants
168                         url += params
169
170                         _, err := http.Get(url)
171                         if err != nil {
172                                 return err
173                         }
174                 }
175         } else {
176                 fmt.Println("Setting up istio")
177                 url = "http://rapps-istio-mgr.default/create-filter?"
178                 params = "name=" + chartName + "&realm=" + realm + "&client=" + client + "&authType=" + authenticator
179                 url += params
180                 _, err := http.Get(url)
181                 if err != nil {
182                         return err
183                 }
184         }
185
186         return nil
187
188 }
189
190 func runUninstall(res http.ResponseWriter, req *http.Request) {
191         query := req.URL.Query()
192         chartName = query.Get("chart")
193         releaseName = chartName
194         fmt.Println("Uninstalling ", chartName)
195
196         var msg string
197         var chart string
198         if chartInstalled(chartName) {
199                 err := getChartValues(chartName)
200                 if err != nil {
201                         msg = err.Error()
202                 } else {
203                         chart, err = uninstallHelmChart(chartName)
204                         if err != nil {
205                                 msg = "Error occurred during uninstall " + err.Error()
206                         } else {
207                                 msg = "Successfully uninstalled release: " + chart
208                         }
209                         err := uninstallSecurity(rapp, chartName)
210                         if err != nil {
211                                 msg = err.Error()
212                         }
213                 }
214                 unregistrerRapp(chartName, rapp.Type)
215         } else {
216                 msg = chartName + " is not installed"
217         }
218
219         // create response binary data
220         data := []byte(msg) // slice of bytes
221         // write `data` to response
222         res.Write(data)
223 }
224
225 func uninstallSecurity(rapp Rapp, chartName string) error {
226         var url string
227         var params string
228         role := rapp.Roles[0].Role
229         realm := rapp.Realm
230         client := rapp.Client
231         authenticator := rapp.Authenticator
232
233         if !rapp.SecurityEnabled {
234                 return nil
235         }
236         if rapp.Type == "provider" {
237                 // Remove istio objects for rapp
238                 fmt.Println("Removing istio services")
239                 _, err := http.Get("http://rapps-istio-mgr.default/remove-policy?name=" + chartName)
240                 if err != nil {
241                         return err
242                 }
243                 // remove keycloak client
244                 fmt.Println("Removing keycloak client")
245                 url = "http://rapps-keycloak-mgr.default/remove?"
246                 params = "name=" + client + "&realm=" + realm + "&role=" + role + "&authType=" + authenticator
247                 url += params
248                 _, err = http.Get(url)
249                 if err != nil {
250                         return err
251                 }
252         }
253         if rapp.Type == "invoker" {
254                 // Remove istio objects for rapp
255                 fmt.Println("Removing istio services")
256                 _, err := http.Get("http://rapps-istio-mgr.default/remove-filter?name=" + chartName)
257                 if err != nil {
258                         return err
259                 }
260         }
261         return nil
262 }
263
264 func runList(res http.ResponseWriter, req *http.Request) {
265         chartInfo := list()
266         // create response binary data
267         data, err := json.Marshal(chartInfo)
268         if err != nil {
269                 fmt.Printf("Error happened in JSON marshal. Err: %s\n", err)
270         }
271         // write `data` to response
272         res.Write(data)
273 }
274
275 func main() {
276         //flag.StringVar(&url, "url", "http://chartmuseum:8080", "ChartMuseum url")
277         flag.StringVar(&repoName, "repoName", "local-dev", "Repository name")
278         flag.StringVar(&namespace, "namespace", "istio-nonrtric", "namespace for install")
279         flag.Parse()
280         settings = cli.New()
281
282         runInstallHandler := http.HandlerFunc(runInstall)
283         http.Handle("/install", runInstallHandler)
284         runUninstallHandler := http.HandlerFunc(runUninstall)
285         http.Handle("/uninstall", runUninstallHandler)
286         runListHandler := http.HandlerFunc(runList)
287         http.Handle("/list", runListHandler)
288         http.ListenAndServe(":9000", nil)
289 }
290
291 func addToRepo(url string) (string, error) {
292         repoFile := settings.RepositoryConfig
293         fmt.Printf("Repo File %s\n", repoFile)
294
295         //Ensure the file directory exists as it is required for file locking
296         err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
297         if err != nil && !os.IsExist(err) {
298                 return "", err
299         }
300
301         b, err := ioutil.ReadFile(repoFile)
302         if err != nil && !os.IsNotExist(err) {
303                 return "", err
304         }
305
306         var f repo.File
307         if err := yaml.Unmarshal(b, &f); err != nil {
308                 return "", err
309         }
310
311         if f.Has(repoName) {
312                 fmt.Printf("repository name (%s) already exists\n", repoName)
313                 return "", nil
314         }
315
316         c := repo.Entry{
317                 Name: repoName,
318                 URL:  url,
319         }
320
321         r, err := repo.NewChartRepository(&c, getter.All(settings))
322         if err != nil {
323                 return "", err
324         }
325
326         if _, err := r.DownloadIndexFile(); err != nil {
327                 err := errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
328                 return "", err
329         }
330
331         f.Update(&c)
332
333         if err := f.WriteFile(repoFile, 0644); err != nil {
334                 return "", err
335         }
336         fmt.Printf("%q has been added to your repositories\n", repoName)
337         return "", nil
338 }
339
340 func dryRun() (*action.Install, error) {
341         actionConfig, err := getActionConfig(namespace)
342
343         install := action.NewInstall(actionConfig)
344
345         fmt.Printf("Repo Name: %s\n",repoName)
346         fmt.Printf("Chart Name: %s\n",chartName)
347         cp, err := install.ChartPathOptions.LocateChart(fmt.Sprintf("%s/%s", repoName, chartName), settings)
348         fmt.Printf("Chart location: %s\n",cp)
349
350         chartRequested, err = loader.Load(cp)
351
352         install.Namespace = namespace
353         install.ReleaseName = releaseName
354         install.DryRun = true
355         rel, err := install.Run(chartRequested, nil)
356         if err != nil {
357                 fmt.Println(err)
358                 return install, err
359         }
360
361         rappMap := rel.Chart.Values["rapp"]
362         // Convert map to json string
363         jsonStr, err := json.Marshal(rappMap)
364         if err != nil {
365                 fmt.Println(err)
366                 return install, err
367         }
368
369         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
370                 fmt.Println(err)
371                 return install, err
372         }
373         fmt.Printf("Keycloak key/value pairs in values.yaml - Realm: %s Client: %s Client Role: %s\n", rapp.Realm, rapp.Client, rapp.Roles)
374         return install, nil
375 }
376
377 func installHelmChart(install *action.Install) (string, error) {
378
379         install.DryRun = false
380         rel, err := install.Run(chartRequested, nil)
381         if err != nil {
382                 fmt.Println(err)
383         }
384         fmt.Println("Successfully installed release: ", rel.Name)
385
386         return rel.Name, err
387 }
388
389 func getActionConfig(namespace string) (*action.Configuration, error) {
390         actionConfig := new(action.Configuration)
391         // Create the rest config instance with ServiceAccount values loaded in them
392         config, err := rest.InClusterConfig()
393         if err != nil {
394                 // fallback to kubeconfig
395                 home, exists := os.LookupEnv("HOME")
396                 if !exists {
397                         home = "/root"
398                 }
399                 kubeconfigPath := filepath.Join(home, ".kube", "config")
400                 if envvar := os.Getenv("KUBECONFIG"); len(envvar) > 0 {
401                         kubeconfigPath = envvar
402                 }
403                 if err := actionConfig.Init(kube.GetConfig(kubeconfigPath, "", namespace), namespace, os.Getenv("HELM_DRIVER"),
404                         func(format string, v ...interface{}) {
405                                 fmt.Sprintf(format, v)
406                         }); err != nil {
407                         fmt.Println(err)
408                 }
409         } else {
410                 // Create the ConfigFlags struct instance with initialized values from ServiceAccount
411                 var kubeConfig *genericclioptions.ConfigFlags
412                 kubeConfig = genericclioptions.NewConfigFlags(false)
413                 kubeConfig.APIServer = &config.Host
414                 kubeConfig.BearerToken = &config.BearerToken
415                 kubeConfig.CAFile = &config.CAFile
416                 kubeConfig.Namespace = &namespace
417                 if err := actionConfig.Init(kubeConfig, namespace, os.Getenv("HELM_DRIVER"), func(format string, v ...interface{}) {
418                         fmt.Sprintf(format, v)
419                 }); err != nil {
420                         fmt.Println(err)
421                 }
422         }
423         return actionConfig, err
424 }
425
426 func uninstallHelmChart(name string) (string, error) {
427         actionConfig, err := getActionConfig(namespace)
428         if err != nil {
429                 fmt.Println(err)
430         }
431
432         iCli := action.NewUninstall(actionConfig)
433
434         resp, err := iCli.Run(name)
435         if err != nil {
436                 fmt.Println(err)
437         }
438         fmt.Println("Successfully uninstalled release: ", resp.Release.Name)
439         return resp.Release.Name, err
440 }
441
442 func connectToK8s() *kubernetes.Clientset {
443         config, err := rest.InClusterConfig()
444         if err != nil {
445                 fmt.Println("failed to create K8s config")
446         }
447
448         clientset, err := kubernetes.NewForConfig(config)
449         if err != nil {
450                 fmt.Println("Failed to create K8s clientset")
451         }
452
453         return clientset
454 }
455
456 func findService(serviceName, namespace string) (string, int32) {
457         clientset := connectToK8s()
458         svc, err := clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
459         if err != nil {
460                 fmt.Println(err.Error())
461         }
462         return svc.Name, svc.Spec.Ports[0].Port
463 }
464
465 func list() []ChartInfo {
466         var charts = []ChartInfo{}
467         var chart ChartInfo
468         actionConfig, err := getActionConfig(namespace)
469         if err != nil {
470                 panic(err)
471         }
472
473         listAction := action.NewList(actionConfig)
474         releases, err := listAction.Run()
475         if err != nil {
476                 fmt.Println(err)
477         }
478         for _, release := range releases {
479                 //fmt.Println("Release: " + release.Name + " Status: " + release.Info.Status.String())
480                 chart.Name = release.Name
481                 chart.Namespace = release.Namespace
482                 chart.Revision = release.Version
483                 chart.Updated = release.Info.LastDeployed.String()
484                 chart.Status = release.Info.Status.String()
485                 chart.Chart = release.Chart.Metadata.Name + "-" + release.Chart.Metadata.Version
486                 chart.AppVersion = release.Chart.Metadata.AppVersion
487                 chart.Values = release.Chart.Values
488                 charts = append(charts, chart)
489         }
490         return charts
491 }
492
493 func chartInstalled(chartName string) bool {
494         charts := list()
495         for _, chart := range charts {
496                 if chart.Name == chartName {
497                         return true
498                 }
499         }
500         return false
501 }
502
503 func getChartValues(chartName string) error {
504         charts := list()
505         for _, chart := range charts {
506                 if chart.Name == chartName {
507                         rappMap := chart.Values["rapp"]
508                         fmt.Println("rappMap:", rappMap)
509                         // Convert map to json string
510                         jsonStr, err := json.Marshal(rappMap)
511                         if err != nil {
512                                 fmt.Println("Error:", err)
513                                 return err
514                         }
515
516                         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
517                                 fmt.Println("Error:", err)
518                                 return err
519                         }
520                         return nil
521                 }
522         }
523         return errors.New("Chart: cannot retrieve values")
524 }
525
526 func registrerRapp(chartName, chartType string) {
527         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
528
529         db, err := sql.Open("postgres", psqlconn)
530         if err != nil {
531                 fmt.Println(err)
532         } else {
533                 fmt.Println("Connected!")
534         }
535
536         defer db.Close()
537
538         // create
539         // hardcoded
540         createStmt := `CREATE TABLE IF NOT EXISTS services (
541         id serial PRIMARY KEY,
542         name VARCHAR ( 50 ) UNIQUE NOT NULL,
543         type VARCHAR ( 50 ) NOT NULL,
544         created_on TIMESTAMP DEFAULT NOW() 
545         );`
546         _, err = db.Exec(createStmt)
547         if err != nil {
548                 fmt.Println(err)
549         } else {
550                 fmt.Println("Created table for service registry")
551         }
552
553         // dynamic
554         insertDynStmt := `insert into "services"("name", "type") values($1, $2)`
555         _, err = db.Exec(insertDynStmt, chartName, chartType)
556         if err != nil {
557                 fmt.Println(err)
558         } else {
559                 fmt.Println("Inserted " + chartName + " into service registry")
560         }
561 }
562
563 func unregistrerRapp(chartName, chartType string) {
564         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
565
566         db, err := sql.Open("postgres", psqlconn)
567         if err != nil {
568                 fmt.Println(err)
569         } else {
570                 fmt.Println("Connected!")
571         }
572
573         defer db.Close()
574
575         // dynamic
576         deleteDynStmt := `delete from services where name=$1 and type=$2`
577         _, err = db.Exec(deleteDynStmt, chartName, chartType)
578         if err != nil {
579                 fmt.Println(err)
580         } else {
581                 fmt.Println("Deleted " + chartName + " from service registry")
582         }
583 }