0dc93419931675896a8b56aead7de0a00cebfc60
[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
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         cp, err := install.ChartPathOptions.LocateChart(fmt.Sprintf("%s/%s", repoName, chartName), settings)
345
346         chartRequested, err = loader.Load(cp)
347
348         install.Namespace = namespace
349         install.ReleaseName = releaseName
350         install.DryRun = true
351         rel, err := install.Run(chartRequested, nil)
352         if err != nil {
353                 fmt.Println(err)
354                 return install, err
355         }
356
357         rappMap := rel.Chart.Values["rapp"]
358         // Convert map to json string
359         jsonStr, err := json.Marshal(rappMap)
360         if err != nil {
361                 fmt.Println(err)
362                 return install, err
363         }
364
365         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
366                 fmt.Println(err)
367                 return install, err
368         }
369         fmt.Printf("Keycloak key/value pairs in values.yaml - Realm: %s Client: %s Client Role: %s\n", rapp.Realm, rapp.Client, rapp.Roles)
370         return install, nil
371 }
372
373 func installHelmChart(install *action.Install) (string, error) {
374
375         install.DryRun = false
376         rel, err := install.Run(chartRequested, nil)
377         if err != nil {
378                 fmt.Println(err)
379         }
380         fmt.Println("Successfully installed release: ", rel.Name)
381
382         return rel.Name, err
383 }
384
385 func getActionConfig(namespace string) (*action.Configuration, error) {
386         actionConfig := new(action.Configuration)
387         // Create the rest config instance with ServiceAccount values loaded in them
388         config, err := rest.InClusterConfig()
389         if err != nil {
390                 // fallback to kubeconfig
391                 home, exists := os.LookupEnv("HOME")
392                 if !exists {
393                         home = "/root"
394                 }
395                 kubeconfigPath := filepath.Join(home, ".kube", "config")
396                 if envvar := os.Getenv("KUBECONFIG"); len(envvar) > 0 {
397                         kubeconfigPath = envvar
398                 }
399                 if err := actionConfig.Init(kube.GetConfig(kubeconfigPath, "", namespace), namespace, os.Getenv("HELM_DRIVER"),
400                         func(format string, v ...interface{}) {
401                                 fmt.Sprintf(format, v)
402                         }); err != nil {
403                         fmt.Println(err)
404                 }
405         } else {
406                 // Create the ConfigFlags struct instance with initialized values from ServiceAccount
407                 var kubeConfig *genericclioptions.ConfigFlags
408                 kubeConfig = genericclioptions.NewConfigFlags(false)
409                 kubeConfig.APIServer = &config.Host
410                 kubeConfig.BearerToken = &config.BearerToken
411                 kubeConfig.CAFile = &config.CAFile
412                 kubeConfig.Namespace = &namespace
413                 if err := actionConfig.Init(kubeConfig, namespace, os.Getenv("HELM_DRIVER"), func(format string, v ...interface{}) {
414                         fmt.Sprintf(format, v)
415                 }); err != nil {
416                         fmt.Println(err)
417                 }
418         }
419         return actionConfig, err
420 }
421
422 func uninstallHelmChart(name string) (string, error) {
423         actionConfig, err := getActionConfig(namespace)
424         if err != nil {
425                 fmt.Println(err)
426         }
427
428         iCli := action.NewUninstall(actionConfig)
429
430         resp, err := iCli.Run(name)
431         if err != nil {
432                 fmt.Println(err)
433         }
434         fmt.Println("Successfully uninstalled release: ", resp.Release.Name)
435         return resp.Release.Name, err
436 }
437
438 func connectToK8s() *kubernetes.Clientset {
439         config, err := rest.InClusterConfig()
440         if err != nil {
441                 fmt.Println("failed to create K8s config")
442         }
443
444         clientset, err := kubernetes.NewForConfig(config)
445         if err != nil {
446                 fmt.Println("Failed to create K8s clientset")
447         }
448
449         return clientset
450 }
451
452 func findService(serviceName, namespace string) (string, int32) {
453         clientset := connectToK8s()
454         svc, err := clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{})
455         if err != nil {
456                 fmt.Println(err.Error())
457         }
458         return svc.Name, svc.Spec.Ports[0].Port
459 }
460
461 func list() []ChartInfo {
462         var charts = []ChartInfo{}
463         var chart ChartInfo
464         actionConfig, err := getActionConfig(namespace)
465         if err != nil {
466                 panic(err)
467         }
468
469         listAction := action.NewList(actionConfig)
470         releases, err := listAction.Run()
471         if err != nil {
472                 fmt.Println(err)
473         }
474         for _, release := range releases {
475                 //fmt.Println("Release: " + release.Name + " Status: " + release.Info.Status.String())
476                 chart.Name = release.Name
477                 chart.Namespace = release.Namespace
478                 chart.Revision = release.Version
479                 chart.Updated = release.Info.LastDeployed.String()
480                 chart.Status = release.Info.Status.String()
481                 chart.Chart = release.Chart.Metadata.Name + "-" + release.Chart.Metadata.Version
482                 chart.AppVersion = release.Chart.Metadata.AppVersion
483                 chart.Values = release.Chart.Values
484                 charts = append(charts, chart)
485         }
486         return charts
487 }
488
489 func chartInstalled(chartName string) bool {
490         charts := list()
491         for _, chart := range charts {
492                 if chart.Name == chartName {
493                         return true
494                 }
495         }
496         return false
497 }
498
499 func getChartValues(chartName string) error {
500         charts := list()
501         for _, chart := range charts {
502                 if chart.Name == chartName {
503                         rappMap := chart.Values["rapp"]
504                         fmt.Println("rappMap:", rappMap)
505                         // Convert map to json string
506                         jsonStr, err := json.Marshal(rappMap)
507                         if err != nil {
508                                 fmt.Println("Error:", err)
509                                 return err
510                         }
511
512                         if err := json.Unmarshal(jsonStr, &rapp); err != nil {
513                                 fmt.Println("Error:", err)
514                                 return err
515                         }
516                         return nil
517                 }
518         }
519         return errors.New("Chart: cannot retrieve values")
520 }
521
522 func registrerRapp(chartName, chartType string) {
523         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
524
525         db, err := sql.Open("postgres", psqlconn)
526         if err != nil {
527                 fmt.Println(err)
528         } else {
529                 fmt.Println("Connected!")
530         }
531
532         defer db.Close()
533
534         // create
535         // hardcoded
536         createStmt := `CREATE TABLE IF NOT EXISTS services (
537         id serial PRIMARY KEY,
538         name VARCHAR ( 50 ) UNIQUE NOT NULL,
539         type VARCHAR ( 50 ) NOT NULL,
540         created_on TIMESTAMP DEFAULT NOW()
541         );`
542         _, err = db.Exec(createStmt)
543         if err != nil {
544                 fmt.Println(err)
545         } else {
546                 fmt.Println("Created table for service registry")
547         }
548
549         // dynamic
550         insertDynStmt := `insert into "services"("name", "type") values($1, $2)`
551         _, err = db.Exec(insertDynStmt, chartName, chartType)
552         if err != nil {
553                 fmt.Println(err)
554         } else {
555                 fmt.Println("Inserted " + chartName + " into service registry")
556         }
557 }
558
559 func unregistrerRapp(chartName, chartType string) {
560         psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
561
562         db, err := sql.Open("postgres", psqlconn)
563         if err != nil {
564                 fmt.Println(err)
565         } else {
566                 fmt.Println("Connected!")
567         }
568
569         defer db.Close()
570
571         // dynamic
572         deleteDynStmt := `delete from services where name=$1 and type=$2`
573         _, err = db.Exec(deleteDynStmt, chartName, chartType)
574         if err != nil {
575                 fmt.Println(err)
576         } else {
577                 fmt.Println("Deleted " + chartName + " from service registry")
578         }
579 }