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