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