Update keycloak version
[nonrtric.git] / service-exposure / rapps-helm-installer.go
1 // -
2 //   ========================LICENSE_START=================================
3 //   O-RAN-SC
4 //   %%
5 //   Copyright (C) 2022-2023: 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         realm := rapp.Realm
229         client := rapp.Client
230         authenticator := rapp.Authenticator
231
232         if !rapp.SecurityEnabled {
233                 return nil
234         }
235         if rapp.Type == "provider" {
236                 // Remove istio objects for rapp
237                 fmt.Println("Removing istio services")
238                 _, err := http.Get("http://rapps-istio-mgr.default/remove-policy?name=" + chartName)
239                 if err != nil {
240                         return err
241                 }
242                 // remove keycloak client
243                 fmt.Println("Removing keycloak client")
244                 url = "http://rapps-keycloak-mgr.default/remove?"
245                 params = "name=" + client + "&realm=" + realm + "&authType=" + authenticator
246                 url += params
247                 _, err = http.Get(url)
248                 if err != nil {
249                         return err
250                 }
251         }
252         if rapp.Type == "invoker" {
253                 // Remove istio objects for rapp
254                 fmt.Println("Removing istio services")
255                 _, err := http.Get("http://rapps-istio-mgr.default/remove-filter?name=" + chartName)
256                 if err != nil {
257                         return err
258                 }
259         }
260         return nil
261 }
262
263 func runList(res http.ResponseWriter, req *http.Request) {
264         chartInfo := list()
265         // create response binary data
266         data, err := json.Marshal(chartInfo)
267         if err != nil {
268                 fmt.Printf("Error happened in JSON marshal. Err: %s\n", err)
269         }
270         // write `data` to response
271         res.Write(data)
272 }
273
274 func main() {
275         //flag.StringVar(&url, "url", "http://chartmuseum:8080", "ChartMuseum url")
276         flag.StringVar(&repoName, "repoName", "local-dev", "Repository name")
277         flag.StringVar(&namespace, "namespace", "istio-nonrtric", "namespace for install")
278         flag.Parse()
279         settings = cli.New()
280
281         runInstallHandler := http.HandlerFunc(runInstall)
282         http.Handle("/install", runInstallHandler)
283         runUninstallHandler := http.HandlerFunc(runUninstall)
284         http.Handle("/uninstall", runUninstallHandler)
285         runListHandler := http.HandlerFunc(runList)
286         http.Handle("/list", runListHandler)
287         http.ListenAndServe(":9000", nil)
288 }
289
290 func addToRepo(url string) (string, error) {
291         repoFile := settings.RepositoryConfig
292         fmt.Printf("Repo File %s\n", repoFile)
293
294         //Ensure the file directory exists as it is required for file locking
295         err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
296         if err != nil && !os.IsExist(err) {
297                 return "", err
298         }
299
300         b, err := ioutil.ReadFile(repoFile)
301         if err != nil && !os.IsNotExist(err) {
302                 return "", err
303         }
304
305         var f repo.File
306         if err := yaml.Unmarshal(b, &f); err != nil {
307                 return "", err
308         }
309
310         if f.Has(repoName) {
311                 fmt.Printf("repository name (%s) already exists\n", repoName)
312                 return "", nil
313         }
314
315         c := repo.Entry{
316                 Name: repoName,
317                 URL:  url,
318         }
319
320         r, err := repo.NewChartRepository(&c, getter.All(settings))
321         if err != nil {
322                 return "", err
323         }
324
325         if _, err := r.DownloadIndexFile(); err != nil {
326                 err := errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
327                 return "", err
328         }
329
330         f.Update(&c)
331
332         if err := f.WriteFile(repoFile, 0644); err != nil {
333                 return "", err
334         }
335         fmt.Printf("%q has been added to your repositories\n", repoName)
336         return "", nil
337 }
338
339 func dryRun() (*action.Install, error) {
340         actionConfig, err := getActionConfig(namespace)
341
342         install := action.NewInstall(actionConfig)
343
344         fmt.Printf("Repo Name: %s\n",repoName)
345         fmt.Printf("Chart Name: %s\n",chartName)
346         cp, err := install.ChartPathOptions.LocateChart(fmt.Sprintf("%s/%s", repoName, chartName), settings)
347         fmt.Printf("Chart location: %s\n",cp)
348
349         chartRequested, err = loader.Load(cp)
350
351         install.Namespace = namespace
352         install.ReleaseName = releaseName
353         install.DryRun = true
354         rel, err := install.Run(chartRequested, nil)
355         if err != nil {
356                 fmt.Println(err)
357                 return install, err
358         }
359
360         rappMap := rel.Chart.Values["rapp"]
361         // Convert map to json string
362         jsonStr, err := json.Marshal(rappMap)
363         if err != nil {
364                 fmt.Println(err)
365                 return install, err
366         }
367
368         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
369                 fmt.Println(err)
370                 return install, err
371         }
372         fmt.Printf("Keycloak key/value pairs in values.yaml - Realm: %s Client: %s Client Role: %s\n", rapp.Realm, rapp.Client, rapp.Roles)
373         return install, nil
374 }
375
376 func installHelmChart(install *action.Install) (string, error) {
377
378         install.DryRun = false
379         rel, err := install.Run(chartRequested, nil)
380         if err != nil {
381                 fmt.Println(err)
382         }
383         fmt.Println("Successfully installed release: ", rel.Name)
384
385         return rel.Name, err
386 }
387
388 func getActionConfig(namespace string) (*action.Configuration, error) {
389         actionConfig := new(action.Configuration)
390         // Create the rest config instance with ServiceAccount values loaded in them
391         config, err := rest.InClusterConfig()
392         if err != nil {
393                 // fallback to kubeconfig
394                 home, exists := os.LookupEnv("HOME")
395                 if !exists {
396                         home = "/root"
397                 }
398                 kubeconfigPath := filepath.Join(home, ".kube", "config")
399                 if envvar := os.Getenv("KUBECONFIG"); len(envvar) > 0 {
400                         kubeconfigPath = envvar
401                 }
402                 if err := actionConfig.Init(kube.GetConfig(kubeconfigPath, "", namespace), namespace, os.Getenv("HELM_DRIVER"),
403                         func(format string, v ...interface{}) {
404                                 fmt.Sprintf(format, v)
405                         }); err != nil {
406                         fmt.Println(err)
407                 }
408         } else {
409                 // Create the ConfigFlags struct instance with initialized values from ServiceAccount
410                 var kubeConfig *genericclioptions.ConfigFlags
411                 kubeConfig = genericclioptions.NewConfigFlags(false)
412                 kubeConfig.APIServer = &config.Host
413                 kubeConfig.BearerToken = &config.BearerToken
414                 kubeConfig.CAFile = &config.CAFile
415                 kubeConfig.Namespace = &namespace
416                 if err := actionConfig.Init(kubeConfig, namespace, os.Getenv("HELM_DRIVER"), func(format string, v ...interface{}) {
417                         fmt.Sprintf(format, v)
418                 }); err != nil {
419                         fmt.Println(err)
420                 }
421         }
422         return actionConfig, err
423 }
424
425 func uninstallHelmChart(name string) (string, error) {
426         actionConfig, err := getActionConfig(namespace)
427         if err != nil {
428                 fmt.Println(err)
429         }
430
431         iCli := action.NewUninstall(actionConfig)
432
433         resp, err := iCli.Run(name)
434         if err != nil {
435                 fmt.Println(err)
436         }
437         fmt.Println("Successfully uninstalled release: ", resp.Release.Name)
438         return resp.Release.Name, err
439 }
440
441 func connectToK8s() *kubernetes.Clientset {
442         config, err := rest.InClusterConfig()
443         if err != nil {
444                 fmt.Println("failed to create K8s config")
445         }
446
447         clientset, err := kubernetes.NewForConfig(config)
448         if err != nil {
449                 fmt.Println("Failed to create K8s clientset")
450         }
451
452         return clientset
453 }
454
455 func findService(serviceName, namespace string) (string, int32) {
456         clientset := connectToK8s()
457         svc, err := clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
458         if err != nil {
459                 fmt.Println(err.Error())
460         }
461         return svc.Name, svc.Spec.Ports[0].Port
462 }
463
464 func list() []ChartInfo {
465         var charts = []ChartInfo{}
466         var chart ChartInfo
467         actionConfig, err := getActionConfig(namespace)
468         if err != nil {
469                 panic(err)
470         }
471
472         listAction := action.NewList(actionConfig)
473         releases, err := listAction.Run()
474         if err != nil {
475                 fmt.Println(err)
476         }
477         for _, release := range releases {
478                 //fmt.Println("Release: " + release.Name + " Status: " + release.Info.Status.String())
479                 chart.Name = release.Name
480                 chart.Namespace = release.Namespace
481                 chart.Revision = release.Version
482                 chart.Updated = release.Info.LastDeployed.String()
483                 chart.Status = release.Info.Status.String()
484                 chart.Chart = release.Chart.Metadata.Name + "-" + release.Chart.Metadata.Version
485                 chart.AppVersion = release.Chart.Metadata.AppVersion
486                 chart.Values = release.Chart.Values
487                 charts = append(charts, chart)
488         }
489         return charts
490 }
491
492 func chartInstalled(chartName string) bool {
493         charts := list()
494         for _, chart := range charts {
495                 if chart.Name == chartName {
496                         return true
497                 }
498         }
499         return false
500 }
501
502 func getChartValues(chartName string) error {
503         charts := list()
504         for _, chart := range charts {
505                 if chart.Name == chartName {
506                         rappMap := chart.Values["rapp"]
507                         fmt.Println("rappMap:", rappMap)
508                         // Convert map to json string
509                         jsonStr, err := json.Marshal(rappMap)
510                         if err != nil {
511                                 fmt.Println("Error:", err)
512                                 return err
513                         }
514
515                         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
516                                 fmt.Println("Error:", err)
517                                 return err
518                         }
519                         return nil
520                 }
521         }
522         return errors.New("Chart: cannot retrieve values")
523 }
524
525 func registrerRapp(chartName, chartType string) {
526         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
527
528         db, err := sql.Open("postgres", psqlconn)
529         if err != nil {
530                 fmt.Println(err)
531         } else {
532                 fmt.Println("Connected!")
533         }
534
535         defer db.Close()
536
537         // create
538         // hardcoded
539         createStmt := `CREATE TABLE IF NOT EXISTS services (
540         id serial PRIMARY KEY,
541         name VARCHAR ( 50 ) UNIQUE NOT NULL,
542         type VARCHAR ( 50 ) NOT NULL,
543         created_on TIMESTAMP DEFAULT NOW() 
544         );`
545         _, err = db.Exec(createStmt)
546         if err != nil {
547                 fmt.Println(err)
548         } else {
549                 fmt.Println("Created table for service registry")
550         }
551
552         // dynamic
553         insertDynStmt := `insert into "services"("name", "type") values($1, $2)`
554         _, err = db.Exec(insertDynStmt, chartName, chartType)
555         if err != nil {
556                 fmt.Println(err)
557         } else {
558                 fmt.Println("Inserted " + chartName + " into service registry")
559         }
560 }
561
562 func unregistrerRapp(chartName, chartType string) {
563         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
564
565         db, err := sql.Open("postgres", psqlconn)
566         if err != nil {
567                 fmt.Println(err)
568         } else {
569                 fmt.Println("Connected!")
570         }
571
572         defer db.Close()
573
574         // dynamic
575         deleteDynStmt := `delete from services where name=$1 and type=$2`
576         _, err = db.Exec(deleteDynStmt, chartName, chartType)
577         if err != nil {
578                 fmt.Println(err)
579         } else {
580                 fmt.Println("Deleted " + chartName + " from service registry")
581         }
582 }