From 7edf4a86c57e5656e732467fed77324965bff330 Mon Sep 17 00:00:00 2001 From: Prasad Ghangal Date: Mon, 31 May 2021 16:49:05 +0530 Subject: [PATCH] Collect events for analytics (#77) - Add new command `kbrew analytics` to check status and turn on/off the analytics collection - Collect following events: `install-success`, `install-fail`, `install-timeout` and `k8s-events` - Collect and report `k8s-events` in case of `install-fail` and `install-timeout`. - `k8s-events` are the `Warning` level events for involved Pods --- cmd/cli/main.go | 33 ++++++ go.mod | 3 + go.sum | 16 +++ pkg/apps/apps.go | 54 ++++++++-- pkg/apps/helm/helm.go | 23 +++- pkg/apps/raw/raw.go | 67 ++++++++---- pkg/config/config.go | 46 +++++++- pkg/events/events.go | 242 ++++++++++++++++++++++++++++++++++++++++++ pkg/kube/kube.go | 98 +++++++++++++++++ 9 files changed, 553 insertions(+), 29 deletions(-) create mode 100644 pkg/events/events.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 644d1dc..d7a928c 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/kbrew-dev/kbrew/pkg/apps" "github.com/kbrew-dev/kbrew/pkg/config" @@ -99,6 +100,14 @@ var ( return reg.Update() }, } + + analyticsCmd = &cobra.Command{ + Use: "analytics [on|off|status]", + Short: "Manage analytics setting", + RunE: func(cmd *cobra.Command, args []string) error { + return manageAnalytics(args) + }, + } ) func init() { @@ -112,6 +121,7 @@ func init() { rootCmd.AddCommand(removeCmd) rootCmd.AddCommand(searchCmd) rootCmd.AddCommand(updateCmd) + rootCmd.AddCommand(analyticsCmd) installCmd.PersistentFlags().StringVarP(&timeout, "timeout", "t", "", "time to wait for app components to be in a ready state (default 15m0s)") } @@ -158,3 +168,26 @@ func manageApp(m apps.Method, args []string) error { } return nil } + +func manageAnalytics(args []string) error { + if len(args) == 0 { + return errors.New("Missing subcommand") + } + switch args[0] { + case "on": + viper.Set(config.AnalyticsEnabled, true) + return viper.WriteConfig() + case "off": + viper.Set(config.AnalyticsEnabled, false) + return viper.WriteConfig() + case "status": + kc, err := config.NewKbrew() + if err != nil { + return err + } + fmt.Println("Analytics enabled:", kc.AnalyticsEnabled) + default: + return errors.New("Invalid subcommand") + } + return nil +} diff --git a/go.mod b/go.mod index 9ab4f2d..5fbe3c7 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,16 @@ require ( github.com/openshift/api v0.0.0-20200526144822-34f54f12813a github.com/openshift/client-go v0.0.0-20200521150516-05eb9880269c github.com/pkg/errors v0.9.1 + github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v1.1.3 + github.com/spf13/viper v1.7.1 golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gopkg.in/yaml.v2 v2.4.0 helm.sh/helm/v3 v3.5.4 k8s.io/api v0.20.4 + k8s.io/apimachinery v0.20.4 k8s.io/client-go v0.20.4 sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 45db974..762d8cc 100644 --- a/go.sum +++ b/go.sum @@ -329,6 +329,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -495,6 +496,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/googleapis/gnostic v0.5.3 h1:2qsuRm+bzgwSIKikigPASa2GhW8H2Dn4Qq7UxD8K/48= github.com/googleapis/gnostic v0.5.3/go.mod h1:TRWw1s4gxBGjSe301Dai3c7wXJAZy57+/6tawkOvqHQ= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -545,6 +547,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -597,6 +600,7 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -650,6 +654,7 @@ github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f/go.mod h1:aS446i8akEg0DAtNKTVYpNpLPMc0SzsZ0RtGhjl0uFM= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -705,6 +710,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -788,6 +794,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -868,6 +875,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU= @@ -885,9 +893,11 @@ github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= github.com/softlayer/softlayer-go v0.0.0-20190615201252-ba6e7f295217/go.mod h1:Cw4GTlQccdRGSEf6KiMju767x0NEHE0YIVPJSaXjlsw= @@ -896,6 +906,7 @@ github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= @@ -906,6 +917,7 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -915,6 +927,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -930,6 +944,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/studio-b12/gowebdav v0.0.0-20200929080739-bdacfab94796/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tg123/go-htpasswd v1.0.0/go.mod h1:eQTgl67UrNKQvEPKrDLGBssjVwYQClFZjALVLhIv8C0= github.com/tidwall/gjson v1.3.5/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= @@ -1436,6 +1451,7 @@ gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= diff --git a/pkg/apps/apps.go b/pkg/apps/apps.go index de62051..25c2bb0 100644 --- a/pkg/apps/apps.go +++ b/pkg/apps/apps.go @@ -8,10 +8,13 @@ import ( "path/filepath" "github.com/pkg/errors" + "github.com/spf13/viper" + corev1 "k8s.io/api/core/v1" "github.com/kbrew-dev/kbrew/pkg/apps/helm" "github.com/kbrew-dev/kbrew/pkg/apps/raw" "github.com/kbrew-dev/kbrew/pkg/config" + "github.com/kbrew-dev/kbrew/pkg/events" ) // Method defines operation performed on the apps @@ -29,11 +32,12 @@ type App interface { Install(ctx context.Context, name, namespace string, version string, opt map[string]string) error Uninstall(ctx context.Context, name, namespace string) error Search(ctx context.Context, name string) (string, error) + Workloads(ctx context.Context, namespace string) ([]corev1.ObjectReference, error) } // Run fetches recipe from registry for the app and performs given operation func Run(ctx context.Context, m Method, appName, namespace, appConfigPath string) error { - c, err := config.New(appConfigPath) + c, err := config.NewApp(appName, appConfigPath) if err != nil { return err } @@ -67,38 +71,46 @@ func Run(ctx context.Context, m Method, appName, namespace, appConfigPath string namespace = "" } + // Event report + event := events.NewKbrewEvent(c) + switch m { case Install: // Run preinstall for _, phase := range c.App.PreInstall { for _, a := range phase.Apps { if err := Run(ctx, m, a, namespace, filepath.Join(filepath.Dir(appConfigPath), a+".yaml")); err != nil { - return err + return handleInstallError(ctx, err, event, app, namespace) } } for _, a := range phase.Steps { if err := execCommand(a); err != nil { - return err + return handleInstallError(ctx, err, event, app, namespace) } } } // Run install if err := app.Install(ctx, appName, namespace, c.App.Version, nil); err != nil { - return err + return handleInstallError(ctx, err, event, app, namespace) } // Run postinstall for _, phase := range c.App.PostInstall { for _, a := range phase.Apps { if err := Run(ctx, m, a, namespace, filepath.Join(filepath.Dir(appConfigPath), a+".yaml")); err != nil { - return err + return handleInstallError(ctx, err, event, app, namespace) } } for _, a := range phase.Steps { if err := execCommand(a); err != nil { - return err + return handleInstallError(ctx, err, event, app, namespace) } } } + if viper.GetBool(config.AnalyticsEnabled) { + if err1 := event.Report(context.TODO(), events.ECInstallSuccess, nil, nil); err1 != nil { + fmt.Printf("Failed to report event. %s\n", err1.Error()) + } + } case Uninstall: return app.Uninstall(ctx, appName, namespace) default: @@ -107,6 +119,36 @@ func Run(ctx context.Context, m Method, appName, namespace, appConfigPath string return nil } +func handleInstallError(ctx context.Context, err error, event *events.KbrewEvent, app App, namespace string) error { + if err == nil { + return nil + } + if !viper.GetBool(config.AnalyticsEnabled) { + return err + } + wkl, err1 := app.Workloads(context.TODO(), namespace) + if err1 != nil { + fmt.Printf("Failed to report event. %s\n", err.Error()) + } + + if ctx.Err() != nil && ctx.Err() == context.DeadlineExceeded { + if err1 := event.Report(context.TODO(), events.ECInstallTimeout, err, nil); err1 != nil { + fmt.Printf("Failed to report event. %s\n", err1.Error()) + } + if err1 := event.ReportK8sEvents(context.TODO(), err, wkl); err1 != nil { + fmt.Printf("Failed to report event. %s\n", err1.Error()) + } + return err + } + if err1 := event.Report(context.TODO(), events.ECInstallFail, err, nil); err1 != nil { + fmt.Printf("Failed to report event. %s\n", err1.Error()) + } + if err1 := event.ReportK8sEvents(context.TODO(), err, wkl); err1 != nil { + fmt.Printf("Failed to report event. %s\n", err1.Error()) + } + return err +} + func execCommand(cmd string) error { c := exec.Command("sh", "-c", cmd) c.Stdout = os.Stdout diff --git a/pkg/apps/helm/helm.go b/pkg/apps/helm/helm.go index 409784d..129bdfe 100644 --- a/pkg/apps/helm/helm.go +++ b/pkg/apps/helm/helm.go @@ -7,8 +7,10 @@ import ( "strings" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" + "github.com/kbrew-dev/kbrew/pkg/apps/raw" "github.com/kbrew-dev/kbrew/pkg/config" "github.com/kbrew-dev/kbrew/pkg/engine" ) @@ -16,9 +18,9 @@ import ( type method string const ( - installMethod method = "install" - uninstallMethod method = "delete" - upgrade method = "upgrade" + installMethod method = "install" + uninstallMethod method = "delete" + getManifestMethod method = "get manifest" ) // App holds helm app details @@ -104,6 +106,12 @@ func (ha *App) updateRepo(ctx context.Context) (string, error) { return string(out), err } +func (ha *App) getManifests(ctx context.Context, namespace string) (string, error) { + c := exec.CommandContext(ctx, "helm", "get", "manifest", ha.App.Name, "--namespace", namespace) + out, err := c.CombinedOutput() + return string(out), err +} + // Search searches the name passed in helm repo func (ha *App) Search(ctx context.Context, name string) (string, error) { // Needs helm 3.2+ @@ -121,6 +129,15 @@ func (ha *App) Search(ctx context.Context, name string) (string, error) { return string(out), err } +// Workloads returns K8s workload object reference list for the helm app +func (ha *App) Workloads(ctx context.Context, namespace string) ([]corev1.ObjectReference, error) { + manifest, err := ha.getManifests(ctx, namespace) + if err != nil { + return nil, errors.Wrap(err, "Failed to get helm chart manifests") + } + return raw.ParseManifestYAML(manifest, namespace) +} + func helmCommand(ctx context.Context, m method, name, version, namespace, chart string, chartArgs map[string]interface{}) (string, error) { // Needs helm 3.2+ c := exec.CommandContext(ctx, "helm", string(m), name, "--namespace", namespace) diff --git a/pkg/apps/raw/raw.go b/pkg/apps/raw/raw.go index 8513039..98b41c2 100644 --- a/pkg/apps/raw/raw.go +++ b/pkg/apps/raw/raw.go @@ -93,6 +93,7 @@ func (r *App) Install(ctx context.Context, name, namespace, version string, opti if err := kubectlCommand(ctx, install, name, namespace, patchedManifest); err != nil { return err } + fmt.Printf("Waiting for components to be ready for %s", name) return r.waitForReady(ctx, namespace) } @@ -128,20 +129,57 @@ func kubectlCommand(ctx context.Context, m method, name, namespace, manifest str return c.Run() } -func (r *App) waitForReady(ctx context.Context, namespace string) error { +// Workloads returns K8s workload object reference list for the raw app +func (r *App) Workloads(ctx context.Context, namespace string) ([]corev1.ObjectReference, error) { resp, err := http.Get(r.App.Repository.URL) if err != nil { - return errors.Wrap(err, "Failed to read resource manifest from URL") + return nil, errors.Wrap(err, "Failed to read resource manifest from URL") } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - return errors.Wrap(err, "Failed to read resource manifest from URL") + return nil, errors.Wrap(err, "Failed to read resource manifest from URL") + } + return ParseManifestYAML(string(data), namespace) +} + +func (r *App) waitForReady(ctx context.Context, namespace string) error { + workloads, err := r.Workloads(ctx, namespace) + if err != nil { + return err + } + for _, wRef := range workloads { + switch wRef.Kind { + case "Pod": + if err := kube.WaitForPodReady(ctx, r.KubeCli, wRef.Namespace, wRef.Name); err != nil { + return errors.Wrap(err, fmt.Sprintf("Pod not in ready state. Namespace: %s, Name: %s", wRef.Namespace, wRef.Name)) + } + + case "Deployment": + if err := kube.WaitForDeploymentReady(ctx, r.KubeCli, wRef.Namespace, wRef.Name); err != nil { + return errors.Wrap(err, fmt.Sprintf("Deployment not in ready state. Namespace: %s, Name: %s", wRef.Namespace, wRef.Name)) + } + + case "StatefulSet": + if err := kube.WaitForStatefulSetReady(ctx, r.KubeCli, wRef.Namespace, wRef.Name); err != nil { + return errors.Wrap(err, fmt.Sprintf("StatefulSet not in ready state. Namespace: %s, Name: %s", wRef.Namespace, wRef.Name)) + } + + case "DeploymentConfig": + if err := kube.WaitForDeploymentConfigReady(ctx, r.OSAppCli, r.KubeCli, wRef.Namespace, wRef.Name); err != nil { + return errors.Wrap(err, fmt.Sprintf("DeploymentConfig not in ready state. Namespace: %s, Name: %s", wRef.Namespace, wRef.Name)) + } + } } + return nil +} +// ParseManifestYAML splits yaml manifests with multiple K8s object specs and returns list of workload object references +func ParseManifestYAML(manifest, namespace string) ([]corev1.ObjectReference, error) { decode := scheme.Codecs.UniversalDeserializer().Decode - for _, spec := range yamlDelimiter.Split(string(data), -1) { + objRefs := []corev1.ObjectReference{} + for _, spec := range yamlDelimiter.Split(manifest, -1) { if len(spec) == 0 { continue } @@ -154,42 +192,35 @@ func (r *App) waitForReady(ctx context.Context, namespace string) error { if namespace == "" { namespace = "default" } + switch w := obj.(type) { case *corev1.Pod: if w.GetNamespace() != "" { namespace = w.GetNamespace() } - if err := kube.WaitForPodReady(ctx, r.KubeCli, namespace, w.GetName()); err != nil { - return errors.Wrap(err, fmt.Sprintf("Pod not in ready state. Namespace: %s, Name: %s", namespace, w.GetName())) - } + objRefs = append(objRefs, corev1.ObjectReference{Name: w.GetName(), Namespace: namespace, Kind: "Pod"}) case *appsv1.Deployment: if w.GetNamespace() != "" { namespace = w.GetNamespace() } - if err := kube.WaitForDeploymentReady(ctx, r.KubeCli, namespace, w.GetName()); err != nil { - return errors.Wrap(err, fmt.Sprintf("Deployment not in ready state. Namespace: %s, Name: %s", namespace, w.GetName())) - } + objRefs = append(objRefs, corev1.ObjectReference{Name: w.GetName(), Namespace: namespace, Kind: "Deployment"}) case *appsv1.StatefulSet: if w.GetNamespace() != "" { namespace = w.GetNamespace() } - if err := kube.WaitForStatefulSetReady(ctx, r.KubeCli, namespace, w.GetName()); err != nil { - return errors.Wrap(err, fmt.Sprintf("StatefulSet not in ready state. Namespace: %s, Name: %s", namespace, w.GetName())) - } + objRefs = append(objRefs, corev1.ObjectReference{Name: w.GetName(), Namespace: namespace, Kind: "StatefulSet"}) case *osappsv1.DeploymentConfig: if w.GetNamespace() != "" { namespace = w.GetNamespace() } - if err := kube.WaitForDeploymentConfigReady(ctx, r.OSAppCli, r.KubeCli, namespace, w.GetName()); err != nil { - return errors.Wrap(err, fmt.Sprintf("DeploymentConfig not in ready state. Namespace: %s, Name: %s", namespace, w.GetName())) - } + objRefs = append(objRefs, corev1.ObjectReference{Name: w.GetName(), Namespace: namespace, Kind: "DeploymentConfig"}) } - } - return nil + } + return objRefs, nil } func printList(app config.App) string { diff --git a/pkg/config/config.go b/pkg/config/config.go index f6ba895..91d6d47 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,7 +6,10 @@ import ( "path/filepath" homedir "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" "github.com/spf13/cobra" + "github.com/spf13/viper" "gopkg.in/yaml.v2" ) @@ -23,8 +26,21 @@ const ( Helm RepoType = "helm" // RegistriesDirName represents the dir name within ConfigDir holding all the kbrew registries RegistriesDirName = "registries" + + // Analytics setting flags + + // AnalyticsUUID represents unique ID used as customer ID + AnalyticsUUID = "analyticsUUID" + // AnalyticsEnabled to toggle GA event collection + AnalyticsEnabled = "analyticsEnabled" ) +// KbrewConfig is a kbrew config stored at CONFIG_DIR/config.yaml +type KbrewConfig struct { + AnalyticsUUID string `yaml:"analyticsUUID"` + AnalyticsEnabled bool `yaml:"analyticsEnabled"` +} + // AppConfig is the kbrew recipe configuration type AppConfig struct { APIVersion string `yaml:"apiVersion"` @@ -64,8 +80,8 @@ type PostInstall struct { Steps []string } -// New parses kbrew recipe configuration and returns AppConfig instance -func New(path string) (*AppConfig, error) { +// NewApp parses kbrew recipe configuration and returns AppConfig instance +func NewApp(name, path string) (*AppConfig, error) { c := &AppConfig{} configFile, err := os.Open(path) defer configFile.Close() @@ -81,9 +97,20 @@ func New(path string) (*AppConfig, error) { if len(b) != 0 { yaml.Unmarshal(b, c) } + c.App.Name = name return c, nil } +// NewKbrew parses Kbrew config and returns KbrewConfig struct object +func NewKbrew() (*KbrewConfig, error) { + kc := &KbrewConfig{} + err := viper.Unmarshal(kc) + if err != nil { + return nil, errors.Wrap(err, "Failed to read kbrew config") + } + return kc, nil +} + // InitConfig initializes ConfigDir. // If ConfigDir does not exists, create it func InitConfig() { @@ -100,4 +127,19 @@ func InitConfig() { err := os.MkdirAll(ConfigDir, os.ModePerm) cobra.CheckErr(err) } + + // Create kbrew config yaml + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(ConfigDir) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Create config file + viper.Set(AnalyticsUUID, uuid.NewV4().String()) + viper.Set(AnalyticsEnabled, true) + cobra.CheckErr(viper.SafeWriteConfig()) + } else { + cobra.CheckErr(err) + } + } } diff --git a/pkg/events/events.go b/pkg/events/events.go new file mode 100644 index 0000000..aa26460 --- /dev/null +++ b/pkg/events/events.go @@ -0,0 +1,242 @@ +package events + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/pkg/errors" + "github.com/spf13/viper" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + + "github.com/kbrew-dev/kbrew/pkg/config" + "github.com/kbrew-dev/kbrew/pkg/kube" + "github.com/kbrew-dev/kbrew/pkg/version" +) + +// EventCatagory is the Google Analytics Event catagory +type EventCatagory string + +const ( + kbrewTrackingID = "UA-195717361-1" + gaCollectURL = "https://www.google-analytics.com/collect" + httpTimeout = 5 * time.Second +) + +var ( + k8sVersion string + + // ECInstallSuccess represents install success event catagory + ECInstallSuccess EventCatagory = "install-success" + // ECInstallFail represents install failure event catagory + ECInstallFail EventCatagory = "install-fail" + // ECInstallTimeout represents install timeout event catagory + ECInstallTimeout EventCatagory = "install-timeout" + // ECK8sEvent represents k8s events event catagory + ECK8sEvent EventCatagory = "k8s-event" +) + +type k8sEvent struct { + Reason string + Message string + Object string + Action string +} + +// KbrewEvent contains information to report Event to Google Analytics +type KbrewEvent struct { + gaVersion string + gaType string + gaTID string + gaCID string + gaAIP string + gaAppName string + gaAppVersion string + gaEvCatagory string + gaEvAction string + gaEvLabel string + gaKbrewArgs string +} + +// String returns string representation of Event Catagory +func (ec EventCatagory) String() string { + return string(ec) +} + +// NewKbrewEvent return new KbrewEvent +func NewKbrewEvent(appConfig *config.AppConfig) *KbrewEvent { + return &KbrewEvent{ + gaVersion: "1", + gaType: "event", + gaTID: kbrewTrackingID, + gaCID: viper.GetString(config.AnalyticsUUID), + gaAIP: "1", + gaAppName: "kbrew", + gaAppVersion: version.Short(), + gaEvLabel: fmt.Sprintf("k8s %s", k8sVersion), + gaEvAction: appConfig.App.Name, + gaKbrewArgs: labels.FormatLabels(argsToLabels(appConfig.App.Args)), + } +} + +// Report sends event to Google Analytics +func (kv *KbrewEvent) Report(ctx context.Context, ec EventCatagory, err error, k8sEvent *k8sEvent) error { + v := url.Values{ + "v": {kv.gaVersion}, + "tid": {kv.gaTID}, + "cid": {kv.gaCID}, + "aip": {kv.gaAIP}, + "t": {kv.gaType}, + "ec": {ec.String()}, + "ea": {kv.gaEvAction}, + "el": {kv.gaEvLabel}, + "an": {kv.gaAppName}, + "av": {kv.gaAppVersion}, + "cd1": {}, + "cd2": {}, + "cd3": {}, + "cd4": {}, + "cd5": {}, + "cd6": {kv.gaKbrewArgs}, + } + + if err != nil { + // Set kbrew message + v.Set("cd5", err.Error()) + } + + if k8sEvent != nil { + // Set k8s_reason + v.Set("cd1", k8sEvent.Reason) + // Set k8s_message + v.Set("cd2", k8sEvent.Message) + // Set k8s_action + v.Set("cd3", k8sEvent.Action) + // Set k8s_object + v.Set("cd4", k8sEvent.Object) + } + + buf := bytes.NewBufferString(v.Encode()) + req, err1 := http.NewRequest("POST", gaCollectURL, buf) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("User-Agent", fmt.Sprintf("kbrew/%s", version.Short())) + if err1 != nil { + return err1 + } + ctx, cancel := context.WithTimeout(ctx, httpTimeout) + defer cancel() + + req = req.WithContext(ctx) + + client := http.DefaultClient + resp, err1 := client.Do(req) + if err1 != nil { + return err1 + } + if resp.StatusCode != http.StatusOK { + return errors.New(fmt.Sprintf("Analytics report failed with status code %d", resp.StatusCode)) + } + defer resp.Body.Close() + return err1 +} + +// ReportK8sEvents sends kbrew events with K8s events to Google Analytics +func (kv *KbrewEvent) ReportK8sEvents(ctx context.Context, err error, workloads []corev1.ObjectReference) error { + k8sEvents, err1 := getPodEvents(ctx, workloads) + if err1 != nil { + return err1 + } + for _, event := range k8sEvents { + err1 := kv.Report(ctx, ECK8sEvent, err, &event) + if err1 != nil { + return err1 + } + } + return nil +} + +func getPodEvents(ctx context.Context, workloads []corev1.ObjectReference) ([]k8sEvent, error) { + notRunningPods, err := kube.FetchNonRunningPods(ctx, workloads) + if err != nil { + return nil, err + } + events := []k8sEvent{} + for _, pod := range notRunningPods { + ks8Events, err := getK8sEvents(ctx, corev1.ObjectReference{Name: pod.GetName(), Namespace: pod.GetNamespace(), UID: pod.GetUID(), Kind: "Pod"}) + if err != nil { + return nil, err + } + events = append(events, ks8Events...) + } + return events, nil +} + +func prepareObjectSelector(objReference corev1.ObjectReference) string { + return labels.Set{ + "involvedObject.name": objReference.Name, + "involvedObject.namespace": objReference.Namespace, + "involvedObject.uid": string(objReference.UID), + "involvedObject.kind": objReference.Kind, + "type": "Warning", + }.String() +} + +func init() { + var err error + k8sVersion, err = getK8sVersion() + if err != nil { + fmt.Printf("ERROR: Failed to get K8s version. %s", err.Error()) + } +} + +func getK8sEvents(ctx context.Context, objReference corev1.ObjectReference) ([]k8sEvent, error) { + clis, err := kube.NewClient() + if err != nil { + return nil, err + } + objSelector := prepareObjectSelector(objReference) + eventList, err := clis.KubeCli.CoreV1().Events(objReference.Namespace).List(ctx, metav1.ListOptions{FieldSelector: objSelector}) + if err != nil { + return nil, err + } + retEventList := []k8sEvent{} + for _, event := range eventList.Items { + objRef := corev1.ObjectReference{ + Name: event.InvolvedObject.Name, + Namespace: event.InvolvedObject.Namespace, + Kind: event.InvolvedObject.Kind, + } + retEventList = append(retEventList, k8sEvent{ + Reason: event.Reason, + Message: event.Message, + Object: objRef.String(), + Action: event.Action, + }) + } + return retEventList, nil +} + +func getK8sVersion() (string, error) { + clis, err := kube.NewClient() + if err != nil { + return "", err + } + versionInfo, err := clis.DiscoveryCli.ServerVersion() + if err != nil { + return "", err + } + return versionInfo.String(), nil +} + +func argsToLabels(args map[string]interface{}) map[string]string { + labels := make(map[string]string) + for k, v := range args { + labels[k] = fmt.Sprintf("%v", v) + } + return labels +} diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go index 73e814f..48e35f0 100644 --- a/pkg/kube/kube.go +++ b/pkg/kube/kube.go @@ -2,12 +2,28 @@ package kube import ( "context" + "fmt" + "os" + "path/filepath" "github.com/kanisterio/kanister/pkg/kube" osversioned "github.com/openshift/client-go/apps/clientset/versioned" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" ) +// Client contains Kubernetes clients to call APIs +type Client struct { + KubeCli kubernetes.Interface + OSCli osversioned.Interface + DiscoveryCli discovery.DiscoveryInterface +} + // WaitForPodReady waits till the pod gets ready func WaitForPodReady(ctx context.Context, kubeCli kubernetes.Interface, namespace string, name string) error { return kube.WaitForPodReady(ctx, kubeCli, namespace, name) @@ -27,3 +43,85 @@ func WaitForStatefulSetReady(ctx context.Context, kubeCli kubernetes.Interface, func WaitForDeploymentConfigReady(ctx context.Context, osCli osversioned.Interface, kubeCli kubernetes.Interface, namespace string, name string) error { return kube.WaitOnDeploymentConfigReady(ctx, osCli, kubeCli, namespace, name) } + +// FetchNonRunningPods returns list of non running Pods owned by the workloads +func FetchNonRunningPods(ctx context.Context, workloads []corev1.ObjectReference) ([]corev1.Pod, error) { + clis, err := NewClient() + if err != nil { + return nil, err + } + + pods := []corev1.Pod{} + for _, wRef := range workloads { + fmt.Println("WORKLOAD", wRef) + switch wRef.Kind { + case "Pod": + pod, err := clis.KubeCli.CoreV1().Pods(wRef.Namespace).Get(ctx, wRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if pod.Status.Phase != corev1.PodRunning { + pods = append(pods, *pod) + } + + case "Deployment": + _, notRunningPods, err := kube.DeploymentPods(ctx, clis.KubeCli, wRef.Namespace, wRef.Name) + if err != nil { + return nil, err + } + pods = append(pods, notRunningPods...) + case "StatefulSet": + _, notRunningPods, err := kube.StatefulSetPods(ctx, clis.KubeCli, wRef.Namespace, wRef.Name) + if err != nil { + return nil, err + } + pods = append(pods, notRunningPods...) + + case "DeploymentConfig": + _, notRunningPods, err := kube.DeploymentConfigPods(ctx, clis.OSCli, clis.KubeCli, wRef.Namespace, wRef.Name) + if err != nil { + return nil, err + } + pods = append(pods, notRunningPods...) + } + } + return pods, nil +} + +func newConfig() (*rest.Config, error) { + kubeConfig, err := rest.InClusterConfig() + if err == nil { + return kubeConfig, nil + } + kubeconfig, ok := os.LookupEnv("KUBECONFIG") + if !ok { + kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config") + } + + return clientcmd.BuildConfigFromFlags("", kubeconfig) +} + +// NewClient initializes and returns client object +func NewClient() (*Client, error) { + kubeConfig, err := newConfig() + if err != nil { + return nil, err + } + disClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig) + if err != nil { + return nil, err + } + kubeCli, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, err + } + osCli, err := osversioned.NewForConfig(kubeConfig) + if err != nil { + return nil, err + } + return &Client{ + KubeCli: kubeCli, + DiscoveryCli: disClient, + OSCli: osCli, + }, nil +}