Compare commits

..

318 Commits

Author SHA1 Message Date
Andriy Bulynko
2eae380287 Upgrading kustomize api to version qlik/v0.0.21 (#341) 2020-04-22 16:23:46 -04:00
Ashwathi Shiva
ed7e97332b Preflight fix output to remove symbols (#340)
* removed symbols and have all preflight checks working
2020-04-22 09:01:07 -04:00
Ashwathi Shiva
eb7517f88d Preflight beautify output (#332)
* kubernetes version check fixed with new output style
2020-04-20 09:02:55 -04:00
Foysal Iqbal
ea048f1b5f Unify install apply (#328) 2020-04-16 17:04:21 -04:00
Ilir Bekteshi
0992286e23 [action] test on supported platforms (#319) (#321)
* [action] test on supported platforms (#319)

* [action] install make on windows

* [action] go@beta to clean WFs from workarounds

* [action] Remove MacOS from matrix
2020-04-16 17:16:40 +02:00
Foysal Iqbal
6d87fadae0 Install latest (#326) 2020-04-16 10:29:56 -04:00
Andriy Bulynko
04e2cc5b22 install and apply commands can pull/push images (#322) 2020-04-16 09:58:01 -04:00
Andriy Bulynko
7b7cd7b4bf Fixing windows tests (#324) 2020-04-15 16:41:38 -04:00
Ashwathi Shiva
7a0bbcd5d8 Preflight mongo (#325)
* preflight mongo check working with ca cert
2020-04-15 16:17:21 -04:00
Sanat Nayar
6b6ef14fb1 Merge pull request #318 from qlik-oss/uninstall_confirm
Uninstall Confirmation
2020-04-15 16:03:16 -04:00
Sanat Nayar
fa0c6528e4 Merge pull request #317 from qlik-oss/context_delete
Delete Context Confirmation
2020-04-15 16:02:09 -04:00
Sanat Nayar
e6070a33c2 changed to bool 2020-04-15 10:04:53 -04:00
Sanat Nayar
0c9f264ed2 changed to bool 2020-04-15 10:01:09 -04:00
Sanat Nayar
22b9b902a9 modified tests 2020-04-15 09:32:36 -04:00
Ilir Bekteshi
3a49745622 Merge pull request #269 from qlik-oss/docs2
Update docs
2020-04-14 20:16:14 +02:00
Sanat Nayar
5795988d01 added flags 2020-04-14 13:46:40 -04:00
Sanat Nayar
e9b359c1bd changes 2020-04-14 13:31:52 -04:00
Foysal Iqbal
9ab2478aba fix go git version (#320) 2020-04-14 10:16:26 -04:00
Ilir Bekteshi
044e00afc5 [docs] reorder cmds, cleanup about cmd 2020-04-14 14:07:12 +02:00
Ilir Bekteshi
2f718649f4 [docs] preflight cleanup 2020-04-14 13:42:03 +02:00
Ilir Bekteshi
c6fe7084f5 remove preflight page 2020-04-14 13:17:39 +02:00
Ilir Bekteshi
e60ce7d62d Update docs 2020-04-14 13:17:34 +02:00
Andriy Bulynko
0d2e436639 Accounting for imageRegistry CR setting when executing preflight checks (#314) 2020-04-14 06:39:48 -04:00
Sanat Nayar
6093552ba9 changes 2020-04-13 17:33:44 -04:00
Sanat Nayar
449642e6f4 changes 2020-04-13 17:32:57 -04:00
Sanat Nayar
97b6cf21a7 changes 2020-04-13 17:14:58 -04:00
Sanat Nayar
14b6447154 changes 2020-04-13 17:09:10 -04:00
Sanat Nayar
1c60ce4cc0 changes 2020-04-13 16:57:04 -04:00
Sanat Nayar
7a8926773f changes 2020-04-13 16:50:33 -04:00
Sanat Nayar
0b868732a7 changes 2020-04-13 16:48:35 -04:00
Sanat Nayar
4f2581cde2 changes 2020-04-13 15:52:12 -04:00
Foysal Iqbal
ca15145499 update k-api (#315)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-13 14:03:03 -04:00
Foysal Iqbal
3274ebd12a fix access token encrypt (#313) 2020-04-13 09:43:35 -04:00
Foysal Iqbal
505b4ef4ce add base64 flag and input pipe (#312)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-13 09:21:28 -04:00
Foysal Iqbal
a4e2b0dfe6 Change encryption (#307) 2020-04-09 22:56:05 -04:00
Foysal Iqbal
7cf2b00f0b fix windows copy error (#306) 2020-04-09 13:49:44 -04:00
Ilir Bekteshi
d94454b832 Merge pull request #302 from qlik-oss/buildall
[action] Build for all platforms on PR
2020-04-09 16:21:37 +02:00
Ashwathi Shiva
f4d0bd87f6 Preflight- Provide command into container (#301)
* Supply command as a script into container working
2020-04-09 08:05:43 -04:00
Ilir Bekteshi
645d1496d4 [action] Build for all platforms on PR 2020-04-09 14:04:48 +02:00
Foysal Iqbal
65ce074981 move crds to base/crds folder (#298) 2020-04-08 23:10:12 -04:00
Andriy Bulynko
323014d137 Fixing operator deployment's image renaming if the imageRegistry is set in the CR (#297) 2020-04-08 20:59:33 -04:00
Foysal Iqbal
31262df504 add struct for fetch repo (#287) 2020-04-08 13:37:08 -04:00
Andriy Bulynko
a15fe75b6c Pulling/pushing GitOps and Preflight images (#286) 2020-04-07 19:32:00 -04:00
Ashwathi Shiva
a59bf2d015 mongo image included for preflight check (#288) 2020-04-07 18:40:38 -04:00
Foysal Iqbal
b36b8917da add prefix preflight (#282)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-06 02:24:25 -04:00
Ashwathi Shiva
eed4d49665 Removing namespaces from role, rolebinding and sa checks (#280)
* remove namespace from role, rolebinding and SA checks
2020-04-06 00:23:40 -04:00
Foysal Iqbal
34d35909a4 remove regex again (#279)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-05 22:13:32 -04:00
Ashwathi Shiva
813bec2377 Preflight commands (#276)
* preflight role, roleBinding, serviceAccount, mongo, all checks working
* Updated readme

Co-authored-by: Foysal Iqbal <mqb@qlik.com>
2020-04-05 19:07:01 -04:00
Foysal Iqbal
97cbfa050c Fix regex (#278) 2020-04-05 15:32:14 -04:00
Foysal Iqbal
44b936a9aa add custom crds (#277)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-04 01:16:13 -04:00
Foysal Iqbal
0e6a1ab18d add first version of editor (#275)
* add first version of editor

Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-03 01:22:29 -04:00
Foysal Iqbal
60a77dab5c Merge pull request #274 from qlik-oss/preflight-config
Preflight config
2020-04-02 17:30:24 -04:00
Foysal Iqbal
b041d8be3c Merge branch 'master' into preflight-config 2020-04-02 16:15:11 -04:00
Foysal Iqbal
a73209864c add preflight config in a file for pull/push
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-02 16:14:46 -04:00
Jacob Martin
a662e26867 update kustomize to qlik/v.0.0.18 (#273) 2020-04-02 14:14:47 -04:00
Foysal Iqbal
198c631bd1 Merge pull request #271 from qlik-oss/fix-pull
fix pull
2020-04-02 12:10:24 -04:00
Foysal Iqbal
6c38708c9f Merge pull request #272 from qlik-oss/temp-version
change min version
2020-04-02 11:57:02 -04:00
Foysal Iqbal
8c0ffc667d change min version
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-02 09:40:09 -04:00
Foysal Iqbal
b8fc1474f8 fix pull
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-02 01:04:03 -04:00
Foysal Iqbal
bcb0c44300 fix pull
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-02 01:01:50 -04:00
Foysal Iqbal
e2294e48c4 fix pull
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-02 00:58:23 -04:00
Foysal Iqbal
ad7861cd13 Merge pull request #270 from qlik-oss/prepare-preflight
prepare for pre-flight
2020-04-01 20:39:19 -04:00
Foysal Iqbal
f17e27f2ef prepare for pre-flight
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-01 20:14:31 -04:00
Foysal Iqbal
77bf52e0b0 prepare for pre-flight
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-04-01 20:12:09 -04:00
Ashwathi Shiva
3819f29412 Preflight service, pod and deployment (#268)
* Added preflight deployment, service and pod checks and updated readme
2020-04-01 16:08:49 -04:00
Andriy Bulynko
bdbcc665ae Upgrading qlik-oss/kustomize to version qlik/v0.0.17 (#267) 2020-04-01 12:15:36 -04:00
Ashwathi Shiva
b3d0eff376 troubleshoot-preflight cleaned up and reworked (#263)
* switched to client-go approach to doing preflight-checks, dns check, k8s-version check added, updated readme
2020-03-31 17:07:08 -04:00
Foysal Iqbal
070abea0d8 Merge pull request #265 from qlik-oss/fix-flag-all
fix flag all
2020-03-31 09:44:37 -04:00
Foysal Iqbal
d04defdf13 fix flag all
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-31 09:35:32 -04:00
Ilir Bekteshi
738b934f0e Merge pull request #260 from qlik-oss/license
Add License
2020-03-30 14:43:45 +02:00
Ilir Bekteshi
87f5c740c7 Add License 2020-03-30 12:43:15 +02:00
Foysal Iqbal
1cbc243ca1 Complete apply (#253) 2020-03-30 00:23:33 -04:00
Foysal Iqbal
b944d8a8dd Merge branch 'master' into complete-apply 2020-03-29 23:56:57 -04:00
Foysal Iqbal
6baa8c8a6d fix gomplate secret patch
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 23:53:45 -04:00
Andriy Bulynko
315af4d76e Fixing an issue with EULA prompt (#259) 2020-03-29 23:42:01 -04:00
Foysal Iqbal
81862bad30 update k-api
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 21:49:34 -04:00
Foysal Iqbal
a58119ef6a fix set context
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 21:25:06 -04:00
Foysal Iqbal
be1016400b fix set context
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 20:46:15 -04:00
Foysal Iqbal
46b16426df fix version issue
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 20:37:18 -04:00
Foysal Iqbal
f873a7e45a fix apply
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 20:23:46 -04:00
Foysal Iqbal
3afa9f0c44 fix apply
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 20:11:45 -04:00
Foysal Iqbal
7c0df2ec32 Merge branch 'master' into complete-apply 2020-03-29 19:59:33 -04:00
Foysal Iqbal
c619a02691 fix apply
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-29 19:57:20 -04:00
Ashwathi Shiva
66236e1888 Preflight win error (#250)
* Windows yaml file path modified to address error- unsupported protocol schema
2020-03-28 11:21:22 -04:00
Foysal Iqbal
b9b62b2a2e Merge branch 'master' into complete-apply 2020-03-27 23:01:58 -04:00
Foysal Iqbal
31fb9dd532 make path forward slash while saving (#249) 2020-03-27 23:01:31 -04:00
Foysal Iqbal
e0da9621a4 merge conflict 2020-03-27 16:26:39 -04:00
Sanat Nayar
cb78b4da9f added confirmation for context-delete 2020-03-27 16:26:31 -04:00
Sanat Nayar
a1be6d6b59 Merge pull request #230 from qlik-oss/cr_validation
Support setting any attribute in Spec (through CR Spec Validation)
2020-03-27 16:18:47 -04:00
Foysal Iqbal
2eaae7bdc3 fix feedback
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-27 16:16:45 -04:00
Foysal Iqbal
a2111be51e Merge branch 'master' into fix-path-slash 2020-03-27 13:53:30 -04:00
Foysal Iqbal
9c1deae17e fix feedback
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-27 13:52:56 -04:00
Foysal Iqbal
5582e2e15d fix feedback
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-27 13:46:56 -04:00
Andriy Bulynko
f66a4bf245 Eula prompt integration (#248) 2020-03-27 11:59:16 -04:00
Sanat Nayar
f052ff7882 Merge branch 'master' into cr_validation 2020-03-27 11:15:22 -04:00
Sanat Nayar
72497d7255 consolidated docs for command references 2020-03-26 12:22:49 -04:00
Foysal Iqbal
b6235f20d4 fix set default context (#245)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-25 14:46:57 -04:00
Boris Kuschel
93af9b4386 Merge pull request #243 from qlik-oss/2nd-relative
change config and cr file path relative to ~/.qliksense
2020-03-25 13:51:18 -04:00
Foysal Iqbal
37fad3dbcf Merge branch 'master' into 2nd-relative 2020-03-25 10:01:56 -04:00
Foysal Iqbal
7a6a2b2d2b Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-25 09:57:02 -04:00
Foysal Iqbal
184bc6f81a fix relative path manifestsroot
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-25 09:54:36 -04:00
Foysal Iqbal
140d9a6c33 fix relative path manifestsroot
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-25 09:22:28 -04:00
Foysal Iqbal
68ec172226 fix relative path
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-25 00:55:26 -04:00
Foysal Iqbal
e3c81fd717 fix relative path
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-25 00:07:14 -04:00
Foysal Iqbal
864d186f0b fix relative path
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-24 23:44:48 -04:00
Foysal Iqbal
a0f25848c7 fix relative path issue
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-24 22:02:08 -04:00
Ashwathi Shiva
9469bd8893 Preflight k8s version check (#240)
* qliksense preflight check-k8s-version working
* qliksense preflight all checks working
2020-03-24 18:43:26 -04:00
Foysal Iqbal
6ea5c3e1a8 fix relative path issue
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-24 17:03:19 -04:00
Foysal Iqbal
085e718ba8 merge conflict 2020-03-24 16:15:03 -04:00
Foysal Iqbal
29ebf2b499 Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-24 16:12:42 -04:00
Foysal Iqbal
a4a7b3f0bd fix relative path issue
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-24 16:12:30 -04:00
Foysal Iqbal
f1871279d0 Install from file (#238) 2020-03-24 16:00:50 -04:00
Foysal Iqbal
e7b256dfd5 Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-24 13:54:17 -04:00
Andriy Bulynko
775f438762 Enforcing eula acceptance for all context/CR based commands (#239) 2020-03-24 13:37:44 -04:00
Ashwathi Shiva
aa180b4af1 Port preflight (#237)
Demo comments incorporated
2020-03-23 09:22:33 -04:00
Ilir Bekteshi
af679c89bf Merge pull request #235 from qlik-oss/ibiqlik-patch-1
Remove space in zip path
2020-03-23 10:00:20 +01:00
Ilir Bekteshi
dcd3c0a99b Merge pull request #228 from qlik-oss/ibiqlik/mkdocswf
Fix trigger paths for mkdocs
2020-03-23 09:59:01 +01:00
Foysal Iqbal
ddcaba4fff Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-20 22:58:17 -04:00
Sanat Nayar
19c4d37b42 fixed suggestion bug 2020-03-20 12:30:08 -04:00
Sanat Nayar
dcd90ed81a revert commit 2020-03-20 11:53:35 -04:00
Sanat Nayar
05e90c057c added getaccesstoken 2020-03-20 11:52:05 -04:00
Ashwathi Shiva
2ddfab9440 Port preflight into sense-installer (#234)
porting preflight
2020-03-20 10:00:44 -04:00
Ilir Bekteshi
2bc65f0bad Remove space in zip path 2020-03-20 11:39:27 +01:00
Foysal Iqbal
1eccc50e66 Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-19 22:11:46 -04:00
Foysal Iqbal
1a2de669ba FIx gke issue (#233) 2020-03-19 21:39:44 -04:00
Sanat Nayar
aec352df32 added additional test cases 2020-03-19 13:43:43 -04:00
Sanat Nayar
c1bee27dff removed debugging printouts 2020-03-19 13:34:44 -04:00
Sanat Nayar
3c464e3316 Merge branch 'master' into cr_validation 2020-03-19 13:04:14 -04:00
Sanat Nayar
a71caf080e pkg/qliksense/context_configs.go 2020-03-19 12:54:03 -04:00
Ilir Bekteshi
b2a980de3a Generalizing gitops sample CR section 2020-03-19 15:41:26 +01:00
Ilir Bekteshi
bfba8198cf Update mkdocs.yml 2020-03-19 15:36:29 +01:00
Foysal Iqbal
3638994b91 Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-19 09:38:53 -04:00
Foysal Iqbal
86e8805bc7 Fix doc for gitops (#227) 2020-03-19 09:31:37 -04:00
Foysal Iqbal
7e9dea4e5f Merge branch 'master' of github.com:qlik-oss/sense-installer 2020-03-19 09:07:23 -04:00
Foysal Iqbal
c2430c3817 fix doc for gitips (#226) 2020-03-19 09:04:57 -04:00
Sanat Nayar
5e9903ef3c init 2020-03-18 17:08:14 -04:00
Foysal Iqbal
436162f173 fix doc for gitips
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-18 16:43:44 -04:00
Sanat Nayar
0adb31360a init 2020-03-18 16:35:27 -04:00
Andriy Bulynko
2f039f2d2e get-versions command (#225) 2020-03-18 10:23:42 -04:00
Sanat Nayar
48ee673ddc refined auto-suggestion 2020-03-17 15:56:40 -04:00
Sanat Nayar
57a80a9533 refined auto-suggestion 2020-03-17 15:20:18 -04:00
Sanat Nayar
590abfd5bf added crd spec 2020-03-17 14:58:29 -04:00
Andriy Bulynko
4fe04d6142 Upgrading k-apis to v0.0.21 (#220) 2020-03-17 12:31:24 -04:00
Sanat Nayar
1fd3310e05 Merge pull request #219 from qlik-oss/support_config_set_gitops
changed k-api version
2020-03-17 11:33:54 -04:00
Sanat Nayar
b85269d908 changed k-api version 2020-03-17 11:29:39 -04:00
Sanat Nayar
cbdafadbaf changed k-api version 2020-03-17 11:24:27 -04:00
Sanat Nayar
fa5c854d3a init 2020-03-17 10:17:54 -04:00
Sanat Nayar
c0e2128d5d Merge pull request #215 from qlik-oss/support_config_set_gitops
Support config set gitops
2020-03-17 08:59:21 -04:00
Ilir Bekteshi
df19cadcb6 Merge pull request #207 from qlik-oss/ibiqlik/upx
Move build from CircleCi to GitHub Actions; use UPX to compress binaries #88
2020-03-17 12:51:46 +01:00
Ilir Bekteshi
d9cbbf54cc Merge pull request #211 from qlik-oss/ibiqlik/tidydocs
Tidying up docs/readme
2020-03-17 10:39:16 +01:00
Sanat Nayar
c4f0ddcea3 added cron parser 2020-03-16 17:09:40 -04:00
Sanat Nayar
f57457029d added cron parser 2020-03-16 16:24:44 -04:00
Foysal Iqbal
69aca05a86 fix doubble print (#217)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-16 16:08:07 -04:00
Sanat Nayar
aa737b0594 changed gitops arch 2020-03-16 14:58:17 -04:00
Ilir Bekteshi
e4d69f059a Archive uncompressed binaries, remove lzma in upx 2020-03-16 17:58:11 +01:00
Sanat Nayar
b7c0fd48b7 added basic test cases 2020-03-16 12:57:31 -04:00
Sanat Nayar
4530d1d9e2 fixed error 2020-03-16 12:50:40 -04:00
Sanat Nayar
ca20f8c992 go.sum 2020-03-13 17:44:11 -04:00
Sanat Nayar
b2c16a490b go.sum 2020-03-13 17:40:26 -04:00
Sanat Nayar
7f70cc661e go.sum 2020-03-13 17:06:30 -04:00
Sanat Nayar
2c054cd54e mod. go.sum 2020-03-13 17:04:02 -04:00
Andriy Bulynko
0b2fdae015 Scoping ejson keys and their rotations to the current context/CR (#214) 2020-03-13 16:28:06 -04:00
Ilir Bekteshi
cfc8fbb1f1 Tidying up docs/readme 2020-03-13 14:58:34 +01:00
Sanat Nayar
30f00461ec added GitOps to spec 2020-03-13 09:30:48 -04:00
Ilir Bekteshi
d38852398e Merge pull request #212 from qlik-oss/ibiqlik/archiveignore
Ignore files/dirs on export
2020-03-12 14:06:33 +01:00
Ilir Bekteshi
e85636822d Ignore files/dirs on export 2020-03-12 14:03:20 +01:00
Mo Kassem
b9a80f588d Merge pull request #204 from qlik-oss/initmkdocs
Init mkdocs and gh workflow for publishing docs
2020-03-11 16:44:43 -04:00
Ilir Bekteshi
b9074d9f3c Change branch to master 2020-03-11 21:33:49 +01:00
Ilir Bekteshi
f3a3e97618 Init mkdocs and gh workflow for publishing docs
Initialize mkdocs for serving documentation on GitHub pages
On push to ms-3 branch a workflow publishes the documentation to gh-pages which gets served by GitHub
The content is based on README.md
2020-03-11 21:32:34 +01:00
Sanat Nayar
5c56013a70 Merge pull request #208 from qlik-oss/context_delete
context-delete Unit Tests
2020-03-11 14:58:19 -04:00
Sanat Nayar
134dbd44ed fixed error handling 2020-03-11 14:54:30 -04:00
Sanat Nayar
9898d3b9ec added tests 2020-03-11 11:27:46 -04:00
Sanat Nayar
613b918dde init 2020-03-11 11:20:53 -04:00
Ilir Bekteshi
bdcadebeca Split workflow for PR and Release
Release workflow builds all variants and compresses them using UPX. All files under bin/ are uploaded to the release.
2020-03-11 11:46:37 +01:00
Ilir Bekteshi
626a2ebe68 Fetch git tags 2020-03-11 11:46:37 +01:00
Ilir Bekteshi
1f64641ab1 Use https instead of ssh for cloning git repo (ssh key issues) 2020-03-11 11:46:37 +01:00
Ilir Bekteshi
b764fd179d Add GOPATH workaround 2020-03-11 11:46:37 +01:00
Ilir Bekteshi
e8d1899a41 Use UPX to compress; mv build from circle to GH
The build process is moving to GitHub Actions from CircleCi
The tar archiving is removed as it does not serve a purpose when UPX compression is at the same file size
2020-03-11 11:46:37 +01:00
Sanat Nayar
32fa0a6570 added tests 2020-03-10 14:42:34 -04:00
Sanat Nayar
0bf1f3ca3a added tests 2020-03-10 14:22:39 -04:00
Sanat Nayar
8f56872842 Merge branch 'ms-3' into context_delete 2020-03-10 10:31:08 -04:00
Andriy Bulynko
defdb899b7 Locking pack2 dependency to v2.7.1 (#203) 2020-03-10 10:06:46 -04:00
Sanat Nayar
c7478fb8c1 added tests 2020-03-09 14:59:21 -04:00
Sanat Nayar
34df4b3a5c added tests 2020-03-09 14:54:48 -04:00
Sanat Nayar
c7bac06533 Merge branch 'ms-3' into context_delete 2020-03-09 14:52:03 -04:00
Sanat Nayar
89d5e261ab added tests 2020-03-09 14:50:33 -04:00
Sanat Nayar
6cd70cb643 added tests 2020-03-09 14:48:16 -04:00
Andriy Bulynko
941bb76444 No need to fetch anything before executing "qliksense crds" commands (#195) 2020-03-09 11:32:28 -04:00
Foysal Iqbal
513daa54f4 Text from encrypted (#196)
* fix secret naming
* Fixed a bug with setting git.repo
* fixed code to get decrypted CR and corresponding test
2020-03-08 20:08:30 -04:00
Andriy Bulynko
46b40d6011 Optionally keep temporary patches in the config repo after each install/upgrade and use an explicit command to remove these patches later (#193) 2020-03-06 15:08:02 -05:00
Sanat Nayar
7893329ab7 Merge pull request #152 from qlik-oss/context_delete
qliksense delete-context
2020-03-06 13:07:29 -05:00
Sanat Nayar
a127127317 reverted tests 2020-03-06 11:50:26 -05:00
Sanat Nayar
d8f1ab4f30 deleted secrets 2020-03-06 11:46:45 -05:00
Ashwathi Shiva
37bf4eae2b Merge branch 'context_delete' of github.com:qlik-oss/sense-installer into context_delete 2020-03-05 15:29:41 -05:00
Ashwathi Shiva
376f6ae838 Merge branch 'ms-3' into context_delete 2020-03-05 15:15:55 -05:00
Foysal Iqbal
659db113d7 Fix cr (#191) 2020-03-05 14:26:04 -05:00
Foysal Iqbal
19e8eda3a3 Fix install withsecret (#190) 2020-03-05 14:17:57 -05:00
Sanat Nayar
12e511ab04 added tests 2020-03-05 14:17:07 -05:00
Andriy Bulynko
3fec90e50b Pull/push to private image registry include the operator image (#187) 2020-03-05 14:12:27 -05:00
Sanat Nayar
36c32d4ca6 added tests 2020-03-05 13:58:55 -05:00
Sanat Nayar
21d7e63588 added tests 2020-03-05 13:57:16 -05:00
Sanat Nayar
7397fb3b34 added tests 2020-03-05 13:35:29 -05:00
Sanat Nayar
8608a69406 added tests 2020-03-05 13:32:26 -05:00
Sanat Nayar
e530a6a79e added unit tests 2020-03-05 10:27:25 -05:00
Foysal Iqbal
096ba5062b fix secret naming (#189)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-05 09:57:03 -05:00
Ashwathi Shiva
2719da19a5 fixed base64 decode logic in one spot, updated tests (#188)
* fix install with secret
* fixed code and tests
2020-03-04 18:54:16 -05:00
Foysal Iqbal
0d3ba901ef make accept eula mandatory for a context (#186)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-03-04 16:54:32 -05:00
Sanat Nayar
9630453a24 added unit tests 2020-03-04 15:55:01 -05:00
Sanat Nayar
a6d81fa8a5 added unit tests 2020-03-04 15:47:20 -05:00
Ashwathi Shiva
758496cac7 Will not display error message twice (#185)
* fixed a bug that shows error twice
* updated Readme and command help
2020-03-04 12:36:39 -05:00
Ashwathi Shiva
7fadbb8392 Command qliksense config fixed, unit tests added (#184)
qliksense config working and added unit tests
2020-03-04 10:51:21 -05:00
Andriy Bulynko
1c8e4df00a Applying private registry pull secret to the cluster (#181) 2020-03-03 17:51:06 -05:00
Foysal Iqbal
27226568fb Fix namespace (#182) 2020-03-03 15:10:42 -05:00
Sanat Nayar
119e1dee34 fixed help issues 2020-03-03 09:45:50 -05:00
Andriy Bulynko
6ca7db2485 Encrypting push/pull secrets on disk (#171) 2020-02-28 16:28:10 -05:00
Ashwathi Shiva
6994b06180 Base64Decode-Decrypt-Encrypt secret value from a given file (#166)
Fix for panic in ReadFile and a convenience method for decoding and encoding resources to handoff to K8s
2020-02-28 12:43:33 -05:00
Sanat Nayar
c13964b30c help improvements 2020-02-28 10:51:13 -05:00
Sanat Nayar
9e6beeb8b0 removed porter from help 2020-02-28 10:47:28 -05:00
Sanat Nayar
fffa92ed6e removed porter from help 2020-02-28 10:41:51 -05:00
Sanat Nayar
36008ab0dc Merge branch 'ms-3' into context_delete 2020-02-28 09:36:37 -05:00
Sanat Nayar
49eda6fea5 Merge pull request #165 from qlik-oss/helper-levenstein
Levenshtein's Distance use for correct command suggestion
2020-02-28 09:20:55 -05:00
Andriy Bulynko
910b76733e Auth docker registry push (#168) 2020-02-28 01:34:59 -05:00
Sanat Nayar
9758746361 Merge branch 'ms-3' into context_delete 2020-02-27 16:12:04 -05:00
Sanat Nayar
1bbf82a15a prevented default deletion 2020-02-27 15:50:07 -05:00
Sanat Nayar
be9acdd9b2 levenstein's theorem implimentation 2020-02-27 15:29:57 -05:00
Foysal Iqbal
01e2b6923a Print path manifest root (#163) 2020-02-27 14:54:54 -05:00
Sanat Nayar
c65fad8f5c updated docs 2020-02-27 14:37:01 -05:00
Ashwathi Shiva
e0cd07ed94 Support for encrypt secret=true (#162)
support encrypt secret=true
2020-02-27 14:32:33 -05:00
Sanat Nayar
b29c1ec193 updated docs 2020-02-27 14:31:50 -05:00
Sanat Nayar
287ff62507 updated docs 2020-02-27 14:31:00 -05:00
Sanat Nayar
8d9dc3d48b levenstein's theorem implimentation 2020-02-27 14:21:36 -05:00
Andriy Bulynko
b6a42c2031 Adding operations for docker registry secret marshalling/unmarshalling (#153) 2020-02-26 11:35:30 -05:00
Foysal Iqbal
5ba281f93a Initial doc (#156) 2020-02-26 08:22:17 -05:00
Foysal Iqbal
11b037f8e6 fix config apply (#154)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-02-26 00:09:51 -05:00
Sanat Nayar
74d6863acf delete context change 2020-02-25 15:42:53 -05:00
Foysal Iqbal
d31b161fc3 add default mongo for now (#150)
* add default mongo for now

Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-02-25 15:35:04 -05:00
Sanat Nayar
d261be6c13 delete context change 2020-02-25 11:41:41 -05:00
Andriy Bulynko
7fcc1966f8 Automated test for unauthenticated pull/push (#147) 2020-02-25 08:50:20 -05:00
Sanat Nayar
a3a6c47375 delete context change 2020-02-24 16:41:35 -05:00
Foysal Iqbal
97e7336300 Install with git (#146) 2020-02-24 14:47:01 -05:00
Andriy Bulynko
b905bcd41d Blinking "..." on the stdout while the about action is executing kustomize build (#135) 2020-02-24 11:40:47 -05:00
Andriy Bulynko
063c9c97e4 Pull/push CR integration (#134) 2020-02-21 16:43:15 -05:00
Foysal Iqbal
ed67ae3d4c Fix install bug (#133) 2020-02-21 11:34:20 -05:00
Foysal Iqbal
24a0ce3513 Fix root dir (#130) 2020-02-20 16:48:41 -05:00
Sanat Nayar
b4daf52ef5 readme config change 2020-02-20 14:46:49 -05:00
Sanat Nayar
b413e1bca9 init 2020-02-20 14:39:52 -05:00
Sanat Nayar
a7e757e15f added readme for config 2020-02-20 14:30:55 -05:00
Sanat Nayar
c47aabc066 added readme for config 2020-02-20 14:28:20 -05:00
Sanat Nayar
78422af050 added readme for config 2020-02-20 14:23:05 -05:00
Boris Kuschel
cb515f216d Merge pull request #121 from qlik-oss/windows_ansi
Add ansi translator for windows
2020-02-20 08:46:21 -05:00
Boris Kuschel
f6eacefd82 Add ansi translator for windows
Signed-off-by: Boris Kuschel <boris.kuschel@qlik.com>
2020-02-20 07:40:27 -05:00
Sanat Nayar
2dd37ab985 Merge pull request #118 from qlik-oss/context_list
QlikSense config list-contexts
2020-02-19 14:58:50 -05:00
Sanat Nayar
8142bb5fa9 added formatting changes 2020-02-19 11:48:50 -05:00
Sanat Nayar
22ea225b8c added configs sub-command 2020-02-19 09:56:50 -05:00
Andriy Bulynko
2f854fd6e4 Fixing about command output 2020-02-19 09:38:07 -05:00
Andriy Bulynko
47bcc016fc About from current context (#117) 2020-02-19 09:28:21 -05:00
Ashwathi Shiva
4e2083309e Encrypt secret in the case: secret=false (#114)
qliksense config set-secrets <key>=<value> --secrets=false
2020-02-18 16:16:48 -05:00
Sanat Nayar
017aa63726 Merge pull request #98 from qlik-oss/upgrade_commmand
QlikSense Upgrade Command
2020-02-18 11:14:05 -05:00
Sanat Nayar
f4275a47ad added upgrade func 2020-02-14 15:39:06 -05:00
Sanat Nayar
75e4e43f9b added upgrade func 2020-02-14 15:14:48 -05:00
Sanat Nayar
21cbc0b44d added upgrade func 2020-02-14 14:22:54 -05:00
Sanat Nayar
003f7f31fc added upgrade func 2020-02-14 14:22:54 -05:00
Sanat Nayar
c3b8837402 added upgrade func 2020-02-14 14:22:54 -05:00
Sanat Nayar
838ed3069c init 2020-02-14 14:22:54 -05:00
Andriy Bulynko
5668da13a7 Creating uniquely named branches (#94) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
2ed9bcb7bf Placing ejson ENV var mangling behind a check (#93) 2020-02-14 14:22:54 -05:00
Ashwathi Shiva
505bb51f95 Added tests (#90)
* Tests added
2020-02-14 14:22:54 -05:00
Andriy Bulynko
60feff3292 Faster-running kuz test (#89) 2020-02-14 14:22:54 -05:00
Ashwathi Shiva
af1afbef8f Support configuration through multiple args (#81)
* support configuration with multiple args
2020-02-14 14:22:54 -05:00
Boris Kuschel
3c4ada848a Make file paths portable
Signed-off-by: Boris Kuschel <boris.kuschel@qlik.com>
2020-02-14 14:22:54 -05:00
Boris Kuschel
6da6415c44 Make makefile portable:
Signed-off-by: Boris Kuschel <boris.kuschel@qlik.com>
2020-02-14 14:22:54 -05:00
Andriy Bulynko
a3287fc1a9 Storing/referencing ejson keys at path: ${qliksenseHome}/ejson/keys (#77) 2020-02-14 14:22:54 -05:00
Foysal Iqbal
39607652a8 Fix default install (#79) 2020-02-14 14:22:54 -05:00
Ashwathi Shiva
6768f74d40 Add support for rotateKeys and included releaseName as part of CRSpec (#78)
* added support for rotateKeys, and included releaseName as part of CRSpec
2020-02-14 14:22:54 -05:00
Ashwathi Shiva
e159e8bd90 Imperative config through cli (#75)
* fixed a regexp bug and another one where qliksense-context was set as default context every time
* modified file creation permissions
2020-02-14 14:22:54 -05:00
Ashwathi Shiva
65bf3fb185 Imperative config through cli (#70)
* minor fix
2020-02-14 14:22:54 -05:00
Ashwathi Shiva
1f245546cd Imperative config through cli (#69)
* removed unnecessary check
2020-02-14 14:22:54 -05:00
Ashwathi Shiva
e38b66f039 Imperative config through cli (#66)
qliksense config commands implemented
2020-02-14 14:22:54 -05:00
Foysal Iqbal
4fd7f2ecbf Install flags (#68) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
b07995dfb1 Setting kubeConfigPath based on homedir.Dir() for consistency (#65) 2020-02-14 14:22:54 -05:00
Foysal Iqbal
caf318410d Fetch install command (#62) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
78fde72c92 Setting kubeConfigPath based on os.UserHomeDir() (#64) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
4b0543b7b0 upgrading k-apis to v0.0.2 2020-02-14 14:22:54 -05:00
Andriy Bulynko
3ff45e47d7 Using k-apis and underlying go-git for the about command (#56) 2020-02-14 14:22:54 -05:00
Foysal Iqbal
72f7a450cf Config view apply (#55) 2020-02-14 14:22:54 -05:00
Foysal Iqbal
0371fa0d9b Clean porter (#53) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
baf394160f About command (#50) 2020-02-14 14:22:54 -05:00
Foysal Iqbal
e411219da8 bring static files into cli (#49) 2020-02-14 14:22:54 -05:00
Andriy Bulynko
a1cb7eda9f kustomize API in-process (#48) 2020-02-14 14:22:54 -05:00
Boris Kuschel
240b9242fa Pull defaults for no version and no directory 2020-02-14 14:22:54 -05:00
Foysal Iqbal
314ff5a14d About doc (#46) 2020-02-14 14:22:54 -05:00
Boris Kuschel
766a2babc7 Make pull/push Docker engineless (#42) 2020-02-14 14:22:54 -05:00
Sanat Nayar
3c1709dcb5 added upgrade func 2020-02-14 14:22:37 -05:00
Sanat Nayar
48e8c997e4 added upgrade func 2020-02-14 11:11:21 -05:00
Jacob Martin
a9b5599d35 update release to include tars (#92)
* update release to include tars
2020-02-14 06:59:32 -05:00
Sanat Nayar
b092356fba added upgrade func 2020-02-13 14:49:24 -05:00
Foysal Iqbal
488f162dff uninstall command (#97)
Signed-off-by: Foysal Iqbal <mqb@qlik.com>
2020-02-13 14:06:06 -05:00
Sanat Nayar
a29e7acf70 init 2020-02-12 17:03:34 -05:00
Andriy Bulynko
4a6e49f393 Creating uniquely named branches (#94) 2020-02-11 16:32:56 -05:00
Andriy Bulynko
50b2712456 Placing ejson ENV var mangling behind a check (#93) 2020-02-11 15:09:49 -05:00
Ashwathi Shiva
477f049c3e Added tests (#90)
* Tests added
2020-02-11 14:46:26 -05:00
Andriy Bulynko
b2ce12bd62 Faster-running kuz test (#89) 2020-02-10 14:07:07 -05:00
Ashwathi Shiva
ee4352e9d6 Support configuration through multiple args (#81)
* support configuration with multiple args
2020-02-10 09:27:44 -05:00
Boris Kuschel
11822db2cb Merge pull request #82 from qlik-oss/fix_Makefile
Make makefile portable
2020-02-08 08:16:30 -05:00
Boris Kuschel
58b027f361 Merge pull request #83 from qlik-oss/portable_paths
Make file paths portable
2020-02-08 08:16:03 -05:00
Boris Kuschel
25fb2c2407 Make file paths portable
Signed-off-by: Boris Kuschel <boris.kuschel@qlik.com>
2020-02-08 08:13:35 -05:00
Boris Kuschel
05b90314e4 Make makefile portable:
Signed-off-by: Boris Kuschel <boris.kuschel@qlik.com>
2020-02-08 07:40:29 -05:00
Andriy Bulynko
5825ba127b Storing/referencing ejson keys at path: ${qliksenseHome}/ejson/keys (#77) 2020-02-07 21:19:28 -05:00
Foysal Iqbal
156f21fab2 Fix default install (#79) 2020-02-07 16:05:11 -05:00
Ashwathi Shiva
835235a109 Add support for rotateKeys and included releaseName as part of CRSpec (#78)
* added support for rotateKeys, and included releaseName as part of CRSpec
2020-02-07 15:05:11 -05:00
Ashwathi Shiva
53127d00d8 Imperative config through cli (#75)
* fixed a regexp bug and another one where qliksense-context was set as default context every time
* modified file creation permissions
2020-02-07 11:17:23 -05:00
Ashwathi Shiva
4415c8e02b Imperative config through cli (#70)
* minor fix
2020-02-06 14:56:20 -05:00
Ashwathi Shiva
c70e123878 Imperative config through cli (#69)
* removed unnecessary check
2020-02-06 13:41:27 -05:00
Ashwathi Shiva
3595d70b7c Imperative config through cli (#66)
qliksense config commands implemented
2020-02-06 00:39:19 -05:00
Foysal Iqbal
cb2001996c Install flags (#68) 2020-02-05 11:59:48 -05:00
Andriy Bulynko
867106afd3 Setting kubeConfigPath based on homedir.Dir() for consistency (#65) 2020-02-04 17:13:16 -05:00
Foysal Iqbal
6c345c9164 Fetch install command (#62) 2020-02-04 16:50:13 -05:00
Andriy Bulynko
ee0a670018 Setting kubeConfigPath based on os.UserHomeDir() (#64) 2020-02-04 15:26:53 -05:00
99 changed files with 10536 additions and 1060 deletions

View File

@@ -1,43 +0,0 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
docker:
- image: circleci/golang:stretch
working_directory: /go/src/github.com/qlik-oss/sense-installer
steps:
- checkout
- run: make test
- run: make build
build_release:
docker:
- image: circleci/golang:stretch
working_directory: /go/src/github.com/qlik-oss/sense-installer
steps:
- checkout
- run: make test
- run: make xbuild-all
- run:
name: "Publish Release on GitHub"
command: |
go get github.com/tcnksm/ghr
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} /go/src/github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/bin/${CIRCLE_TAG}/
workflows:
version: 2
commit:
jobs:
- build:
filters:
tags:
ignore: /^v.*/
build_release:
jobs:
- build_release:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/

5
.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
# Ignore all files and folders that start with .; .circleci, .github, .git, etc.
# Warning! This will ignore files in subfolders as well.
# If you needs files starting with . then change condition below to be specific
# for each file and folder that needs to be ignored
.* export-ignore

46
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Build Sense installer
on: [pull_request]
jobs:
test:
name: Test
env:
CGO_ENABLED: 0
strategy:
matrix:
go: [1.13.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v2-beta
with:
go-version: ${{ matrix.go }}
- uses: actions/checkout@v2
- name: setup make (Windows)
if: matrix.os == 'windows-latest'
run: choco install make -y
- run: make test
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v2-beta
with:
go-version: 1.13
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- run: make xbuild-all

21
.github/workflows/mkdocs.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- master
paths:
- 'docs/**'
- 'mkdocs.yml'
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@1.11
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

30
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Release Sense installer binaries
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v2-beta
with:
go-version: 1.13
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* # Needed in makefile for versioning
- run: make test
- run: make xbuild-all
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: bin/**/*

9
.gitignore vendored
View File

@@ -1,2 +1,11 @@
bin
.vscode
cmd/qliksense/__debug_bin
pkg/qliksense/crds
pkg/qliksense/packrd
pkg/qliksense/qliksense-packr.go
pkg/qliksense/docker-registry
/pkg/qliksense/tests
.DS_Store
.idea/

191
LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2019 QlikTech International AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

23
MKDOCS.md Normal file
View File

@@ -0,0 +1,23 @@
# Qlik Sense installer documentation
## Local development of documentation
Documentation is built using [mkdocs](https://www.mkdocs.org/) and uses [Material for MKDocs theme](https://squidfunk.github.io/mkdocs-material/)
Requirements: Python and PIP or Docker
```console
pip install mkdocs
pip install mkdocs-material
```
View live changes locally at http://localhost:8000
```console
mkdocs serve
```
### Docker
```console
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material
```

View File

@@ -13,6 +13,7 @@ VERSION ?= $(shell git describe --tags 2> /dev/null || echo v0)
PERMALINK ?= $(shell git describe --tags --exact-match &> /dev/null && echo latest || echo canary)
BUILDTAGS = netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp
CLIENT_PLATFORM ?= $(shell go env GOOS)
CLIENT_ARCH ?= $(shell go env GOARCH)
RUNTIME_PLATFORM ?= linux
@@ -23,8 +24,13 @@ SUPPORTED_ARCHES = amd64
MIXIN = qliksense
DEVNUL := /dev/null
WHICH := which
ifeq ($(CLIENT_PLATFORM),windows)
FILE_EXT=.exe
DEVNUL := NUL
WHICH := where
else ifeq ($(RUNTIME_PLATFORM),windows)
FILE_EXT=.exe
else
@@ -33,47 +39,77 @@ endif
.PHONY: build
build: clean generate
mkdir -p $(BINDIR)
go run _make_support/mkdir_all/do.go $(BINDIR)
go build -ldflags '$(LDFLAGS)' -tags "$(BUILDTAGS)" -o $(BINDIR)/$(MIXIN)$(FILE_EXT) ./cmd/$(MIXIN)
$(MAKE) clean
.PHONY: test
test:
.PHONY: test-setup
test-setup: clean generate
ifeq ($(shell ${WHICH} docker-registry 2>${DEVNUL}),)
$(eval TMP-docker-distribution := $(shell go run _make_support/get_tmp_dir/do.go))
git clone https://github.com/docker/distribution.git "$(TMP-docker-distribution)/docker-distribution"
cd "$(TMP-docker-distribution)/docker-distribution" && git checkout -b v2.7.1 && $(MAKE)
go run _make_support/copy/do.go --src "$(TMP-docker-distribution)/docker-distribution/bin/registry" --dst pkg/qliksense/docker-registry$(FILE_EXT)
go run _make_support/remove_all/do.go "$(TMP-docker-distribution)"
endif
.PHONY: test-short
test-short: test-setup
go test -short -count=1 -tags "$(BUILDTAGS)" -v ./...
$(MAKE) clean
.PHONY: test
test: test-setup
go test -count=1 -tags "$(BUILDTAGS)" -v ./...
$(MAKE) clean
xbuild-all: clean generate
$(foreach OS, $(SUPPORTED_PLATFORMS), \
$(foreach ARCH, $(SUPPORTED_ARCHES), \
$(MAKE) $(MAKE_OPTS) CLIENT_PLATFORM=$(OS) CLIENT_ARCH=$(ARCH) MIXIN=$(MIXIN) xbuild; \
))
$(foreach ARCH, $(SUPPORTED_ARCHES), \
$(MAKE) $(MAKE_OPTS) CLIENT_PLATFORM=$(OS) CLIENT_ARCH=$(ARCH) MIXIN=$(MIXIN) xbuild; \
))
$(MAKE) clean
xbuild: $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
$(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT):
mkdir -p $(dir $@)
GOOS=$(CLIENT_PLATFORM) GOARCH=$(CLIENT_ARCH) $(XBUILD) -o $@ ./cmd/$(MIXIN)
ifeq ($(CLIENT_PLATFORM),windows)
zip $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH).zip $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
else
tar -czvf $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH).tar.gz -C $(BINDIR)/$(VERSION)/ $(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
endif
upx $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
generate: get-crds packr2
go generate ./...
go run _make_support/remove_all/do.go pkg/qliksense/crds
HAS_PACKR2 := $(shell command -v packr2)
packr2:
ifndef HAS_PACKR2
go get -u github.com/gobuffalo/packr/v2/packr2
ifeq ($(shell ${WHICH} packr2 2>${DEVNUL}),)
go get -u github.com/gobuffalo/packr/v2/packr2@v2.7.1
endif
clean: clean-packr
-rm -rf /tmp/operator
-rm -fr pkg/qliksense/crds
go run _make_support/remove_all/do.go pkg/qliksense/crds
clean-packr: packr2
cd pkg/qliksense && packr2 clean
get-crds:
git clone git@github.com:qlik-oss/qliksense-operator.git -b custom_cr /tmp/operator
mkdir -p pkg/qliksense/crds/cr
mkdir -p pkg/qliksense/crds/crd
mkdir -p pkg/qliksense/crds/crd-deploy
cp /tmp/operator/deploy/*.yaml pkg/qliksense/crds/crd-deploy
cp /tmp/operator/deploy/crds/*_crd.yaml pkg/qliksense/crds/crd
cp /tmp/operator/deploy/crds/*_cr.yaml pkg/qliksense/crds/cr
get-crds:
ifeq ($(QLIKSENSE_OPERATOR_DIR),)
$(eval TMP-operator := $(shell go run _make_support/get_tmp_dir/do.go))
git clone https://github.com/qlik-oss/qliksense-operator.git -b master $(TMP-operator)/operator
$(MAKE) QLIKSENSE_OPERATOR_DIR="$(TMP-operator)/operator" get-crds
go run _make_support/remove_all/do.go "$(TMP-operator)"
else
go run _make_support/mkdir_all/do.go pkg/qliksense/crds/cr
go run _make_support/mkdir_all/do.go pkg/qliksense/crds/crd
go run _make_support/mkdir_all/do.go pkg/qliksense/crds/crd-deploy
go run _make_support/copy/do.go --src-pattern "$(QLIKSENSE_OPERATOR_DIR)/deploy/*.yaml" --dst pkg/qliksense/crds/crd-deploy
go run _make_support/copy/do.go --src-pattern "$(QLIKSENSE_OPERATOR_DIR)/deploy/crds/*_crd.yaml" --dst pkg/qliksense/crds/crd
go run _make_support/copy/do.go --src-pattern "$(QLIKSENSE_OPERATOR_DIR)/deploy/crds/*_cr.yaml" --dst pkg/qliksense/crds/cr
endif

167
README.md
View File

@@ -1,166 +1,21 @@
# Qlik Sense installation and operations CLI
# (WIP) Qlik Sense on Kubernetes installation and operations CLI
## Documentation
To learn more about Qlik Sense on Kubernetes CLI go to https://qlik-oss.github.io/sense-installer/
- [Qlik Sense installation and operations CLI](#qlik-sense-installation-and-operations-cli)
- [About](#about)
- [Future Direction](#future-direction)
- [Getting Started](#getting-started)
- [Requirements](#requirements)
- [Download](#download)
- [Porter CLI](#porter-cli)
- [Generate Credentials from published bundle](#generate-credentials-from-published-bundle)
- [Qlik Sense version and image list](#qliksense-version-and-image-list)
- [Optional: Pulling images in manifest locally, "air gap"](#optional-pulling-images-in-manifest-locally-%22air-gap%22)
- [Running Preflight checks](#running-preflight-checks)
- [Installation](#installation)
- [Supported Parameters during install](#supported-parameters-during-install)
- [How To Add Identity Provider Config](#how-to-add-identity-provider-config)
- [Packaging a Custom bundle](#packaging-a-custom-bundle)
## About
The Qlik Sense installer CLI (sense-installer) provides an imperitive interface to many of the configurations that need to be applied against the declaritive structure described in [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s).
The QSEoK CLI (qliksense) provides an imperative interface to many of the configurations that need to be applied against the declarative structure described in [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s).
This is a technology preview that uses [porter](https://porter.sh) to execute "actions" (operations) and bundle versions of the [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) repository.
This is a technology preview that uses Qlik modified [kustomize](https://github.com/qlik-oss/kustomize) to kubernetes manifests of the versions of the [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) repository.
These bundles are posted to [docker hub](https://hub.docker.com/) at the following location: [qliksense-cnab-bundle](https://hub.docker.com/r/qlik/qliksense-cnab-bundle/tags).
For each version of a qliksense sense edge build there should be a corresponding release current posted on docker hub. ex. `qlik/qliksense-cnab-bundle:v1.21.23-edge` for `v1.21.23` edge release of qliksense. The latest version posted will also be labelled as `latest`
For each version of a qliksense edge build there should be a corresponding release in [qliksense-k8s] repository under [releases](https://github.com/qlik-oss/qliksense-k8s/releases)
### Future Direction
- Porter is currently used as a core technology for the CLI. In the future Porter will be moved "up the stack" to allow the CLI to perform the current and expanded operations independently and encapsulate core functionality currently provided by Porter and other dependent tooling.
- More operations:
- Expanded preflight checks
- Expand preflight checks
- backup/restore operations
## Getting Started
### Requirements
- Docker Client connected to a docker engine into which images can built, pulled and pushed.
- (Docker Desktop setup tested for these instructions)
### Download
- Download the appropriate executable for your platform from the [releases page](https://github.com/qlik-oss/sense-installer/releases).
- To allow the CLI to download and initialize dependencies (including porter and it's associated mixins), simply execute `qliksense` with no arguments
- `qliksense`
#### Porter CLI
- *Optional*: If wanting to use porter CLI directly, two environment variables will need to be set so as not to conflict with an existing porter installation:
- _Bash_
```shell
bash# export PORTER_HOME="$HOME\.qliksense"
bash# export PATH="$HOME\.qliksense;$PATH"
```
- _PowerShell_
```shell
PS> $Env:PORTER_HOME="$Env:USERPROFILE\.qliksense"
PS> $Env:PATH="$Env:USERPROFILE\.qliksense;$Env:PATH"
```
### Generate Credentials from published bundle
- Ensure connectivity to the target cluster create a kubeconfig credential for a target bundle.
- generating a file as follows, replace `<credential_name>` with a name of your choosing.
- _Bash_
```shell
bash# CREDENTIAL_NAME=<credential_name>
bash# cat <<EOF > $HOME/.qliksense/credentials/$CREDENTIAL_NAME.yaml
name: $CREDENTIAL_NAME
credentials:
- name: kubeconfig
source:
path: $HOME/.kube/config
EOF
```
- _PowerShell_
```shell
PS> $CREDENTIAL_NAME="<credential_name>"
PS> Add-Content -Value @"
name: $CREDENTIAL_NAME
credentials:
- name: kubeconfig
source:
path: $Env:USERPROFILE\.kube\config
"@ -Path $Env:USERPROFILE\.qliksense\credentials\$CREDENTIAL_NAME".yaml"
```
- credentials can also be created using the [porter](https://porter.sh) CLI *(the correct environmental variable need to have been set up as shown in [Porter CLI](#porter-cli) above)*
- `porter cred generate <credential_name> --tag qlik/qliksense-cnab-bundle:v1.21.23-edge`, replace `<credential_name>` with a name of your choosing.
- Select `file path` and specify full path to a kube config file ex. _Bash_: `/home/user/.kube/config` or _PowerShell_ `C:\Users\user\.kube\config`
### Qlik Sense version and image list
It is possible verify the version of the [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) repository bundled into the `qlik/qliksense-cnab-bundle` image and retreive the list of images included in that release. (This operation can take a minute or so)<https://github.com/qlik-oss/kustomize/issues/13> as the entire manifests needs to be rendered:
- `qliksense about --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
### Optional: Pulling images in manifest locally, "air gap"
If the `dockerRegistry` parameter is specified as the private docker registry to be used by the kubernetes cluster hosting qliksense, it is possible to pull images to the local docker engine for an eventual push during a `qliksense install` or `qliksense upgrade`
- `qliksense pull --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
### Running Preflight checks
You can run preflight checks to ensure that the cluster is in a healthy state before installing Qliksense.
- `qliksense preflight -c <credential_name> --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
The above command runs the checks in the default namespace. If you want to specify the namespace to run preflight checks on:
- `qliksense preflight --param namespace=<value> -c <credential_name> --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
### Installation
- Install the bundle : `qliksense install --param acceptEULA=yes -c <credential_name> --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
#### Supported Parameters during install
| Name | Descriptions | Default |
| ------------- |:-------------:| -----:|
| profile | select a profile i.e docker-desktop, aws-eks, gke | docker-desktop |
| acceptEULA | yes | has to be yes |
| namespace | any kubernetes namespace | default |
| dockerRegistry | A private docker image regitry for pods | not specified (public) |
| rotateKeys | regenerate application PKI keys on upgrade (yes/no) | no |
| mongoDbUri | the mongodb URI to use | URI of development mongodb |
| scName | storage class name | none |
#### How To Add Identity Provider Config
Since idp configs are usually multiline configs it is not conventional to pass to porter during install as a `param`. Rather put the configs in a file and refer to that file during `porter install` command. For example to add `keycloak` IDP create file named `idpconfigs.txt` and put
```shell
idpConfigs=[{"discoveryUrl":"http://keycloak-insecure:8089/keycloak/realms/master22/.well-known/openid-configuration","clientId":"edge-auth","clientSecret":"e15b5075-9399-4b20-a95e-023022aa4aed","realm":"master","hostname":"elastic.example","claimsMapping":{"sub":["sub","client_id"],"name":["name","given_name","family_name","preferred_username"]}}]
```
Then pass that file during install command like this
```shell
qliksense install --param acceptEULA=yes -c <credential_name> --param-file idpconfigs.txt --tag qlik/qliksense-cnab-bundle:<qliksense_version>`
```
## Packaging a Custom bundle
If files need to be added to the [qliksense-k8s repository](https://github.com/qlik-oss/qliksense-k8s) in order to perform advanced configuration outside the scope of the what the operator provides, a custom bundle needs to be built.
Packaging of Qlik Sense on Kubernetes is done through a [Porter](https://porter.sh/) definition in the [Qlik Sense on Kubernetes configuration repository](https://github.com/qlik-oss/qliksense-k8s/blob/master/porter.yaml), the resulting bundle published on DockerHub as a [Cloud Natvie Application Bundle](https://cnab.io/) called [qliksense-cnab-bundle](https://hub.docker.com/r/qlik/qliksense-cnab-bundle)
To start, clone [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and modify the repo as desired, once finished make sure to be in the `qliksense-k8s` directory from which the porter bundle can be built:
```shell
git clone git@github.com:qlik-oss/qliksense-k8s.git
cd qliksense-k8s
qliksense build
```
Once built, all of the `porter` command that were used with `--tag` can be now be used without this flag provided that porter is executed with the `qliksense-k8s` directory. `porter` will automatically use the qliksense-k8s (and the porter.yaml) in the current directory.
## List of Commands
- [qliksense about](action_about.md)
- fully support airgap installation of QSEoK
- restore unwanted deletion of kubernetes resources

109
_make_support/copy/do.go Normal file
View File

@@ -0,0 +1,109 @@
package main
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"github.com/otiai10/copy"
)
func main() {
srcPattern := flag.String("src-pattern", "", "Source file pattern")
src := flag.String("src", "", "Source file or directory")
dst := flag.String("dst", "", "Destination file or directory")
flag.Parse()
if *srcPattern != "" {
if dstInfo, err := os.Lstat(*dst); err != nil {
panic(err)
} else if !dstInfo.IsDir() {
panic(fmt.Errorf("%v must be a directory", *dst))
}
if matches, err := filepath.Glob(*srcPattern); err != nil {
panic(err)
} else {
for _, match := range matches {
srcInfo, err := os.Lstat(match)
if err != nil {
panic(err)
}
if srcInfo.IsDir() {
if err := copy.Copy(match, *dst, copy.Options{
OnSymlink: func(p string) copy.SymlinkAction {
return copy.Skip
},
}); err != nil {
panic(err)
}
} else if srcInfo.Mode().IsRegular() {
if err := fcopy(match, filepath.Join(*dst, filepath.Base(match)), srcInfo); err != nil {
panic(err)
}
}
}
}
} else if *src != "" {
srcInfo, err := os.Lstat(*src)
if err != nil {
panic(err)
}
if srcInfo.IsDir() {
if err := copy.Copy(*src, *dst, copy.Options{
OnSymlink: func(p string) copy.SymlinkAction {
return copy.Skip
},
}); err != nil {
panic(err)
}
} else if srcInfo.Mode().IsRegular() {
finalDestination := *dst
if dstInfo, err := os.Lstat(*dst); err != nil {
if !os.IsNotExist(err) {
panic(err)
}
} else if dstInfo.IsDir() {
finalDestination = filepath.Join(*dst, filepath.Base(*src))
} else if dstInfo.Mode().IsRegular() {
fmt.Println("WARNING: over-writing existing file: ", *dst)
if err := os.Remove(*dst); err != nil {
panic(err)
}
} else {
panic(fmt.Errorf("not sure how to copy to this dst: %v", *dst))
}
if err := fcopy(*src, finalDestination, srcInfo); err != nil {
panic(err)
}
}
}
}
func fcopy(src, dest string, info os.FileInfo) (err error) {
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
return err
}
f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()
if err = os.Chmod(f.Name(), info.Mode()); err != nil {
return err
}
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
_, err = io.Copy(f, s)
return err
}

View File

@@ -0,0 +1,5 @@
module github.com/qlik-oss/sense-installer/_make_support/copy
go 1.13
require github.com/otiai10/copy v1.1.1

View File

@@ -0,0 +1,8 @@
github.com/otiai10/copy v1.1.1 h1:PH7IFlRQ6Fv9vYmuXbDRLdgTHoP1w483kPNUP2bskpo=
github.com/otiai10/copy v1.1.1/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=

View File

@@ -0,0 +1,14 @@
package main
import (
"fmt"
"io/ioutil"
)
func main() {
if tmpDir, err := ioutil.TempDir("", ""); err != nil {
panic(err)
} else {
fmt.Print(tmpDir)
}
}

View File

@@ -0,0 +1,3 @@
module github.com/qlik-oss/sense-installer/_make_support/get_tmp_dir
go 1.13

View File

@@ -0,0 +1,11 @@
package main
import (
"os"
)
func main() {
if err := os.MkdirAll(os.Args[1], os.ModePerm); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,3 @@
module github.com/qlik-oss/sense-installer/_make_support/mkdir_all
go 1.13

View File

@@ -0,0 +1,11 @@
package main
import (
"os"
)
func main() {
if err := os.RemoveAll(os.Args[1]); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,3 @@
module github.com/qlik-oss/sense-installer/_make_support/remove_all
go 1.13

View File

@@ -1,41 +0,0 @@
# qliksense about
About action will display inside information regarding [qliksense-k8](https://github.com/qlik-oss/qliksense-k8s) release.
it will support following flags
- `qliksense about 1.0.0` display default profile for tag `1.0.0`.
- `qliksense about 1.0.0 --profile=docker-desktop`
- `qliksense about`
- assuming current directory has `manifests/docker-desktop`
- or get version information from pull of `qliksense-k8s` `master`
using other supported commands user might have built the CR into the location `~/.qliksense/myqliksense.yaml`
```yaml
apiVersion: qlik.com/v1
kind: QlikSense
metadata:
name: myqliksense
spec:
profile: docker-desktop
manifestsRoot: /Usr/ddd/my-k8-repo/manifests
namespace: myqliksense
storageClassName: efs
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: "mongo://mongo:3307"
- name: messagingPassword
valueFromKey: messagingPassword
```
In that case the command would be
- `qliksense about`
- display from `/Usr/ddd/my-k8-repo/manifests/docker-desktop` location
- pull from `master` if directory invalid/empty

View File

@@ -3,10 +3,11 @@ package main
import (
"errors"
"fmt"
"strings"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"strings"
)
type aboutCommandOptions struct {
@@ -18,7 +19,7 @@ func about(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "about ref",
Short: "About Qlik Sense",
Short: "Displays information pertaining to Qliksense on Kubernetes",
Long: "Gives the version of QLik Sense on Kubernetes and versions of images.",
Example: `
qliksense about 1.0.0
@@ -38,13 +39,13 @@ qliksense about --profile=test
then get version information based on the default profile in the qliksense-k8s repo master
`,
RunE: func(cmd *cobra.Command, args []string) error {
if gitRef, err := getAboutCommandGitRef(args); err != nil {
if gitRef, err := getSingleArg(args); err != nil {
return err
} else if vout, err := q.About(gitRef, opts.Profile); err != nil {
return err
} else if out, err := yaml.Marshal(vout); err != nil {
return err
} else if _, err := fmt.Println(out); err != nil {
} else if _, err := fmt.Println(string(out)); err != nil {
return err
}
return nil
@@ -55,7 +56,7 @@ qliksense about --profile=test
return c
}
func getAboutCommandGitRef(args []string) (string, error) {
func getSingleArg(args []string) (string, error) {
if len(args) > 1 {
return "", errors.New("too many arguments, only 1 expected")
} else if len(args) == 1 {

74
cmd/qliksense/apply.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"bytes"
"errors"
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func applyCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.InstallCommandOptions{}
filePath := ""
keepPatchFiles, pull, push := false, false, false
c := &cobra.Command{
Use: "apply",
Short: "install qliksense based on provided cr file",
Long: `install qliksense based on provided cr file`,
Example: `qliksense apply -f file_name or cat cr_file | qliksense apply -f -`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLoadOrApplyCommandE(cmd, func(crBytes []byte) error {
if cr, crBytesWithEula, err := getCrWithEulaInserted(crBytes); err != nil {
return err
} else if err := validatePullPushFlagsOnApply(cr, pull, push); err != nil {
return err
} else {
return q.ApplyCRFromReader(bytes.NewReader(crBytesWithEula), opts, keepPatchFiles, true, pull, push)
}
})
},
}
f := c.Flags()
f.StringVarP(&filePath, "file", "f", "", "Install from a CR file")
c.MarkFlagRequired("file")
f.StringVarP(&opts.StorageClass, "storageClass", "s", "", "Storage class for qliksense")
f.StringVarP(&opts.MongoDbUri, "mongoDbUri", "m", "", "mongoDbUri for qliksense (i.e. mongodb://qlik-default-mongodb:27017/qliksense?ssl=false)")
f.StringVarP(&opts.RotateKeys, "rotateKeys", "r", "", "Rotate JWT keys for qliksense (yes:rotate keys/ no:use exising keys from cluster/ None: use default EJSON_KEY from env")
f.BoolVar(&keepPatchFiles, keepPatchFilesFlagName, keepPatchFiles, keepPatchFilesFlagUsage)
f.BoolVarP(&pull, pullFlagName, pullFlagShorthand, pull, pullFlagUsage)
f.BoolVarP(&push, pushFlagName, pushFlagShorthand, push, pushFlagUsage)
eulaPreRunHooks.addValidator(fmt.Sprintf("%v %v", rootCommandName, c.Name()), loadOrApplyCommandEulaPreRunHook)
return c
}
func validatePullPushFlagsOnApply(cr *qapi.QliksenseCR, pull, push bool) error {
if pull && !push {
fmt.Printf("WARNING: pulling images without pushing them")
}
if push {
if registry := cr.Spec.GetImageRegistry(); registry == "" {
return errors.New("no image registry set in the CR; to set it use: qliksense config set-image-registry")
}
}
return nil
}
func getCrWithEulaInserted(crBytes []byte) (*qapi.QliksenseCR, []byte, error) {
if cr, err := qapi.CreateCRObjectFromString(string(crBytes)); err != nil {
return nil, nil, err
} else {
cr.SetEULA("yes")
if crBytesWithEula, err := qapi.K8sToYaml(cr); err != nil {
return nil, nil, err
} else {
return cr, crBytesWithEula, nil
}
}
}

View File

@@ -1,24 +1,26 @@
package main
import (
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "do operations on/around CR",
Long: `do operations on/around CR`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Use like: config view or config apply")
},
func configCmd(q *qliksense.Qliksense) *cobra.Command {
var configCmd = &cobra.Command{
Use: "config",
Short: "do operations on/around CR",
Long: `do operations on/around CR`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ConfigViewCR()
},
}
return configCmd
}
func configApplyCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "apply",
Short: "generate the patchs and apply manifests to k8s",
Short: "generate the patches and apply manifests to k8s",
Long: `generate patches based on CR and apply manifests to k8s`,
Example: `qliksense config apply`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -40,3 +42,20 @@ func configViewCmd(q *qliksense.Qliksense) *cobra.Command {
}
return c
}
func configEditCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "edit [context-name]",
Short: "Edit the context cr",
Long: `edit the context cr. if no context name provided default context will be edited
It will open the vim editor unless KUBE_EDITOR is defined`,
Example: `qliksense config edit [context-name]`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
return q.EditCR(args[0])
}
return q.EditCR("")
},
}
return c
}

222
cmd/qliksense/context.go Normal file
View File

@@ -0,0 +1,222 @@
package main
import (
"errors"
"fmt"
"os"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func setContextConfigCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
cmd = &cobra.Command{
Use: "set-context",
Short: "Sets the context in which the Kubernetes cluster and resources live in",
Example: `
qliksense config set-context <context_name>
- The above configuration will be displayed in the CR
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.SetContextConfig(args)
},
}
return cmd
}
func listContextConfigCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
cmd = &cobra.Command{
Use: "list-contexts",
Short: "retrieves the contexts and lists them",
Example: `qliksense config list-contexts`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ListContextConfigs()
},
}
return cmd
}
func setOtherConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
cmd = &cobra.Command{
Use: "set",
Short: "configure a key value pair into the current context",
Example: `
qliksense config set <key>=<value>
- The above configuration will be displayed in the CR
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.SetOtherConfigs(args)
},
}
return cmd
}
func setConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
base64Encoded := false
cmd = &cobra.Command{
Use: "set-configs",
Short: "set configurations into the qliksense context as key-value pairs",
Example: `
qliksense config set-configs <service_name>.<attribute>="<value>"
- The above configuration will be displayed in the CR
qliksense config set-configs <service_name>.<attribute>="<value" --base64
- if the value is base64 encoded
echo "something" | base64 | qliksense config set-configs <service_name>.<attribute> --base64
- value is coming from input pipe as base64 encoded
echo "something" | qliksense config set-configs <service_name>.<attribute>
- value is coming from input pipe
`,
RunE: func(cmd *cobra.Command, args []string) error {
if isInputFromPipe() && len(args) == 1 {
return q.SetConfigFromReader(args[0], os.Stdin, base64Encoded)
}
return q.SetConfigs(args, base64Encoded)
},
}
f := cmd.Flags()
f.BoolVarP(&base64Encoded, "base64", "", false, "if the arguments value is base64 encoded")
return cmd
}
func setSecretsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
secret bool
)
base64Encoded := false
cmd = &cobra.Command{
Use: "set-secrets",
Short: "set secrets configurations into the qliksense context as key-value pairs",
Example: `
qliksense config set-secrets <service_name>.<attribute>="<value>" --secret=true
- Encrypt the secret value into a new Kubernetes secret resource
- The secret resource is placed in the location: <qliksense_home>/<contexts>/<context_name>/secrets/<service_name>.yaml
- Include it's key reference in the current context
qliksense config set-secrets <service_name>.<attribute>="<value>" --secret=false
- Encrypt the secret value and display it in the current context
- No secret resource is created
- The above configuration will be displayed in the CR
qliksense config set-secrets <service_name>.<attribute>="<value>" --base64
- the <value> is base64 encoded
echo "something" | base64 | qliksense config set-secrets <service_name>.<attribute> --base64
- value coming from input pipe as base64 encoded
echo "something" | qliksense config set-secrets <service_name>.<attribute>
- value coming from input pipe`,
RunE: func(cmd *cobra.Command, args []string) error {
if isInputFromPipe() && len(args) == 1 {
return q.SetSecretsFromReader(args[0], os.Stdin, secret, base64Encoded)
}
return q.SetSecrets(args, secret, base64Encoded)
},
}
f := cmd.Flags()
f.BoolVar(&secret, "secret", false, "Whether secrets should be encrypted as a Kubernetes Secret resource")
f.BoolVarP(&base64Encoded, "base64", "", false, "if the arguments value is base64 encoded")
return cmd
}
func deleteContextConfigCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
skipConfirmation := false
cmd = &cobra.Command{
Use: "delete-context",
Short: "deletes a specific context locally (not in-cluster)",
Example: `qliksense config delete-contexts <context_name>`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.DeleteContextConfig(args, skipConfirmation)
},
}
f := cmd.Flags()
f.BoolVar(&skipConfirmation, "yes", skipConfirmation, "skips confirmation")
return cmd
}
func setImageRegistryCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
pushUsername string
pushPassword string
pullUsername string
pullPassword string
username string
password string
)
cmd = &cobra.Command{
Use: "set-image-registry",
Short: "set private image registry",
Example: `
qliksense config set-image-registry https://your.private.registry.example.com:5000 --push-username foo1 --push-password bar1 --pull-username foo2 --pull-password bar2
qliksense config set-image-registry https://your.private.registry.example.com:5000 --username foo --password bar
`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("private docker image registry FQDN is required")
}
registry := args[0]
if username != "" {
pullUsername = username
pushUsername = username
}
if password != "" {
pullPassword = password
pushPassword = password
}
if (pullUsername != "" && pushUsername == "") || (pullUsername == "" && pushUsername != "") {
return errors.New("if you specify pull credentials, you must specify push credentials as well and vise versa")
}
if (pullUsername == "" && pullPassword != "") || (pushUsername == "" && pushPassword != "") {
return errors.New("if you specify passwords, you must specify usernames as well")
}
return q.SetImageRegistry(registry, pushUsername, pushPassword, pullUsername, pullPassword)
},
}
f := cmd.Flags()
f.StringVar(&pushUsername, "push-username", "", "Username used for pushing images")
f.StringVar(&pushPassword, "push-password", "", "Password used for pushing images")
f.StringVar(&pullUsername, "pull-username", "", "Username used for pulling images")
f.StringVar(&pullPassword, "pull-password", "", "Password used for pulling images")
f.StringVar(&username, "username", "", "Username used for both pushing and pulling images")
f.StringVar(&password, "password", "", "Password used for both pushing and pulling images")
return cmd
}
func cleanConfigRepoPatchesCmd(q *qliksense.Qliksense) *cobra.Command {
return &cobra.Command{
Use: "clean-config-repo-patches",
Short: "Clean config repo patch files",
Example: "qliksense config clean-config-repo-patches",
RunE: func(cmd *cobra.Command, args []string) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if err := q.DiscardAllUnstagedChangesFromGitRepo(qConfig); err != nil {
return fmt.Errorf("error removing temporary changes to the config: %v\n", err)
}
fmt.Println("done")
return nil
},
}
}

42
cmd/qliksense/crds.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
var crdsCmd = &cobra.Command{
Use: "crds",
Short: "crds for qliksense and operators",
Long: `crds for qliksense and operators`,
}
func crdsViewCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.CrdCommandOptions{}
c := &cobra.Command{
Use: "view",
Short: "View CRDs for qliksense application. use view --all to see opearator crd as well ",
Long: `View CRDs for qliksense application. use view --all to see opearator crd as well`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ViewCrds(opts)
},
}
f := c.Flags()
f.BoolVarP(&opts.All, "all", "", false, "Include All CRDs")
return c
}
func crdsInstallCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.CrdCommandOptions{}
c := &cobra.Command{
Use: "install",
Short: "Install CRDs fro Qliksense application. Use install --all to include operator crd",
Long: `Install CRDs fro Qliksense application. Use install --all to include operator crd`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.InstallCrds(opts)
},
}
f := c.Flags()
f.BoolVarP(&opts.All, "all", "", false, "Include All CRDs")
return c
}

106
cmd/qliksense/eula.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/mattn/go-tty"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
type eulaPreRunHooksT struct {
validators map[string]func(cmd *cobra.Command, q *qliksense.Qliksense) (bool, error)
postValidationArtifacts map[string]interface{}
}
func (e *eulaPreRunHooksT) addValidator(command string, validator func(cmd *cobra.Command, q *qliksense.Qliksense) (bool, error)) {
e.validators[command] = validator
}
func (e *eulaPreRunHooksT) getValidator(command string) func(cmd *cobra.Command, q *qliksense.Qliksense) (bool, error) {
if validator, ok := e.validators[command]; ok {
return validator
}
return nil
}
func (e *eulaPreRunHooksT) addPostValidationArtifact(artifactName string, artifact interface{}) {
e.postValidationArtifacts[artifactName] = artifact
}
func (e *eulaPreRunHooksT) getPostValidationArtifact(artifactName string) interface{} {
if artifact, ok := e.postValidationArtifacts[artifactName]; ok {
return artifact
}
return nil
}
var eulaEnforced = os.Getenv("QLIKSENSE_EULA_ENFORCE") == "true"
var eulaText = "Please read the end user license agreement at: https://www.qlik.com/us/legal/license-terms"
var eulaPrompt = "Do you accept our EULA? (y/n): "
var eulaErrorInstruction = `You must enter "y" to continue`
var eulaPreRunHooks = eulaPreRunHooksT{
validators: make(map[string]func(cmd *cobra.Command, q *qliksense.Qliksense) (bool, error)),
postValidationArtifacts: make(map[string]interface{}),
}
func commandAlwaysRequiresEulaAcceptance(commandName string) bool {
return commandName == fmt.Sprintf("%v install", rootCommandName) ||
commandName == fmt.Sprintf("%v apply", rootCommandName)
}
func globalEulaPreRun(cmd *cobra.Command, q *qliksense.Qliksense) {
if isEulaEnforced(cmd.CommandPath()) {
if strings.TrimSpace(strings.ToLower(cmd.Flag("acceptEULA").Value.String())) != "yes" {
if eulaPreRunHook := eulaPreRunHooks.getValidator(cmd.CommandPath()); eulaPreRunHook != nil {
if eulaAccepted, err := eulaPreRunHook(cmd, q); err != nil {
panic(err)
} else if !eulaAccepted {
doEnforceEula()
}
} else if qConfig, err := qapi.NewQConfigE(q.QliksenseHome); err != nil {
doEnforceEula()
} else if qcr, err := qConfig.GetCurrentCR(); err != nil || !qcr.IsEULA() {
doEnforceEula()
}
}
}
}
func globalEulaPostRun(cmd *cobra.Command, q *qliksense.Qliksense) {
if isEulaEnforced(cmd.CommandPath()) {
if err := q.SetEulaAccepted(); err != nil {
panic(err)
}
}
}
func isEulaEnforced(commandName string) bool {
return eulaEnforced || commandAlwaysRequiresEulaAcceptance(commandName)
}
func doEnforceEula() {
fmt.Println(eulaText)
fmt.Print(eulaPrompt)
answer := readRuneFromTty()
if strings.ToLower(answer) != "y" {
fmt.Println(eulaErrorInstruction)
os.Exit(1)
}
}
func readRuneFromTty() string {
t, err := tty.Open()
if err != nil {
panic(err)
}
defer t.Close()
answer, err := t.ReadString()
if err != nil {
panic(err)
}
return answer
}

View File

@@ -1,30 +1,30 @@
package main
import (
"errors"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func fetchCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.FetchCommandOptions{}
c := &cobra.Command{
Use: "fetch",
Short: "fetch a release from qliksense-k8s repo",
Long: `fetch a release from qliksense-k8s repo`,
Example: `qliksense fetch <version>`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires a version (i.e. v1.0.0)")
Short: "fetch a release from qliksense-k8s repo, if version not supplied, will use from context",
Long: `fetch a release from qliksense-k8s repo, if version not supplied, will use from context`,
Example: `qliksense fetch [version]`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
opts.Version = args[0]
}
if _, err := semver.NewVersion(args[0]); err != nil {
return errors.New("is it not a valid version. should be something like this v1.0.0")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
q.FetchQK8s(args[0])
return q.FetchK8sWithOpts(opts)
},
}
f := c.Flags()
f.StringVarP(&opts.GitUrl, "url", "", "", "git url from where configuration will be pulled")
f.StringVarP(&opts.AccessToken, "accessToken", "", "", "access token for git url")
f.StringVarP(&opts.SecretName, "secretName", "", "", "kubernetes secret name where a key name accessToken exist")
f.BoolVarP(&opts.Overwrite, "overwrite", "", false, "Ovewrite previously fetched veersion as well as local chagnes")
return c
}

View File

@@ -0,0 +1,29 @@
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
const defaultVersionsLimit = 10
func getInstallableVersionsCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.LsRemoteCmdOptions{
IncludeBranches: false,
Limit: defaultVersionsLimit,
}
c := &cobra.Command{
Use: "get-versions",
Short: "list remote/installable versions",
Long: `list remote/installable versions`,
Example: `qliksense get-versions`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.GetInstallableVersions(opts)
},
}
f := c.Flags()
f.BoolVarP(&opts.IncludeBranches, "include-branches", "", opts.IncludeBranches, "Include branches")
f.IntVarP(&opts.Limit, "limit", "", opts.Limit, "Maximum versions to list (starting with the highest)")
return c
}

View File

@@ -1,30 +1,95 @@
package main
import (
"errors"
"github.com/Masterminds/semver/v3"
"bytes"
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func installCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.InstallCommandOptions{}
filePath := ""
keepPatchFiles, pull, push := false, false, false
c := &cobra.Command{
Use: "install",
Short: "install a qliksense release",
Long: `install a qliksesne release`,
Example: `qliksense install <version>`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires a version (i.e. v1.0.0)")
}
if _, err := semver.NewVersion(args[0]); err != nil {
return errors.New("is it not a valid version. should be something like this v1.0.0")
}
return nil
},
Use: "install",
Short: "install a qliksense release",
Long: `install a qliksense release`,
Example: `qliksense install <version> #if no version provides, expect manifestsRoot is set somewhere in the file system
# qliksense install -f file_name or cat cr_file | qliksense install -f -
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.InstallQK8s(args[0])
if filePath != "" {
return runLoadOrApplyCommandE(cmd, func(crBytes []byte) error {
if cr, crBytesWithEula, err := getCrWithEulaInserted(crBytes); err != nil {
return err
} else if err := validatePullPushFlagsOnApply(cr, pull, push); err != nil {
return err
} else {
return q.ApplyCRFromReader(bytes.NewReader(crBytesWithEula), opts, keepPatchFiles, true, pull, push)
}
})
} else {
version := ""
if len(args) != 0 {
version = args[0]
}
if err := validatePullPushFlagsOnInstall(q, pull, push); err != nil {
return err
}
if pull {
fmt.Println("Pulling images...")
if err := q.PullImages(version, ""); err != nil {
return err
}
}
if push {
fmt.Println("Pushing images...")
if err := q.PushImagesForCurrentCR(); err != nil {
return err
}
}
return q.InstallQK8s(version, opts, keepPatchFiles)
}
},
}
eulaPreRunHooks.addValidator(fmt.Sprintf("%v %v", rootCommandName, c.Name()), func(cmd *cobra.Command, q *qliksense.Qliksense) (b bool, err error) {
if filePath != "" {
return loadOrApplyCommandEulaPreRunHook(cmd, q)
} else if qConfig, err := qapi.NewQConfigE(q.QliksenseHome); err != nil {
return false, err
} else if qcr, err := qConfig.GetCurrentCR(); err != nil {
return false, err
} else {
return qcr.IsEULA(), nil
}
})
f := c.Flags()
f.StringVarP(&opts.StorageClass, "storageClass", "s", "", "Storage class for qliksense")
f.StringVarP(&opts.MongoDbUri, "mongoDbUri", "m", "", "mongoDbUri for qliksense (i.e. mongodb://qlik-default-mongodb:27017/qliksense?ssl=false)")
f.StringVarP(&opts.RotateKeys, "rotateKeys", "r", "", "Rotate JWT keys for qliksense (yes:rotate keys/ no:use exising keys from cluster/ None: use default EJSON_KEY from env")
f.BoolVar(&keepPatchFiles, keepPatchFilesFlagName, keepPatchFiles, keepPatchFilesFlagUsage)
f.StringVarP(&filePath, "file", "f", "", "Install from a CR file")
f.BoolVarP(&pull, pullFlagName, pullFlagShorthand, pull, pullFlagUsage)
f.BoolVarP(&push, pushFlagName, pushFlagShorthand, push, pushFlagUsage)
return c
}
func validatePullPushFlagsOnInstall(q *qliksense.Qliksense, pull, push bool) error {
if pull && !push {
fmt.Printf("WARNING: pulling images without pushing them")
}
if push {
if err := ensureImageRegistrySetInCR(q); err != nil {
return err
}
}
return nil
}

88
cmd/qliksense/load.go Normal file
View File

@@ -0,0 +1,88 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"github.com/pkg/errors"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func loadCrFile(q *qliksense.Qliksense) *cobra.Command {
filePath := ""
overwriteExistingContext := false
c := &cobra.Command{
Use: "load",
Short: "load a CR a file and create necessary structure for future use",
Long: `load a CR a file and create necessary structure for future use`,
Example: `qliksense load -f file_name or cat cr_file | qliksense load -f -`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLoadOrApplyCommandE(cmd, func(buffer []byte) error {
return q.LoadCr(bytes.NewReader(buffer), overwriteExistingContext)
})
},
}
f := c.Flags()
f.StringVarP(&filePath, "file", "f", "", "File to load CR from")
c.MarkFlagRequired("file")
f.BoolVarP(&overwriteExistingContext, "overwrite", "o", overwriteExistingContext, "Overwrite any existing contexts with the same name")
eulaPreRunHooks.addValidator(fmt.Sprintf("%v %v", rootCommandName, c.Name()), loadOrApplyCommandEulaPreRunHook)
return c
}
func getCrFileFromFlag(cmd *cobra.Command, flagName string) (*os.File, error) {
filePath := cmd.Flag(flagName).Value.String()
if filePath == "-" {
if !isInputFromPipe() {
return nil, errors.New("No input pipe present")
}
return os.Stdin, nil
}
file, e := os.Open(filePath)
if e != nil {
return nil, errors.Wrapf(e,
"unable to read the file %s", filePath)
}
return file, nil
}
func isInputFromPipe() bool {
fileInfo, _ := os.Stdin.Stat()
return fileInfo.Mode()&os.ModeCharDevice == 0
}
func loadOrApplyCommandEulaPreRunHook(cmd *cobra.Command, q *qliksense.Qliksense) (bool, error) {
file, err := getCrFileFromFlag(cmd, "file")
if err != nil {
return false, err
}
defer file.Close()
if crBytes, err := ioutil.ReadAll(file); err != nil {
return false, err
} else {
eulaPreRunHooks.addPostValidationArtifact("CR", crBytes)
return q.IsEulaAcceptedInCrFile(bytes.NewBuffer(crBytes))
}
}
func runLoadOrApplyCommandE(cmd *cobra.Command, callBack func(buffer []byte) error) error {
if crBytes := eulaPreRunHooks.getPostValidationArtifact("CR"); crBytes != nil {
return callBack(crBytes.([]byte))
} else {
file, err := getCrFileFromFlag(cmd, "file")
if err != nil {
return err
}
defer file.Close()
if crBytes, err := ioutil.ReadAll(file); err != nil {
return err
} else {
return callBack(crBytes)
}
}
}

View File

@@ -1,7 +1,6 @@
package main
import (
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
@@ -10,18 +9,41 @@ var operatorCmd = &cobra.Command{
Use: "operator",
Short: "Configuration for operator",
Long: `Configuration for operator`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("User like: operator view")
},
}
/*
func operatorViewCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "view",
Short: "View Configuration for operator",
Long: `View Configuration for operator`,
Run: func(cmd *cobra.Command, args []string) {
q.ViewOperatorCrd()
Short: "View CRD for operator",
Long: `View CRD for operator`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ViewOperator()
},
}
return c
}
*/
func operatorCrdCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "crd",
Short: "View CRD for operator",
Long: `View CRD for operator`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ViewOperator()
},
}
return c
}
func operatorControllerCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "controller",
Short: "View manifests for operator controller",
Long: `View manifests for operator controller`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ViewOperatorController()
},
}
return c

438
cmd/qliksense/preflight.go Normal file
View File

@@ -0,0 +1,438 @@
package main
import (
"fmt"
ansi "github.com/mattn/go-colorable"
"github.com/qlik-oss/sense-installer/pkg/preflight"
"github.com/ttacon/chalk"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func preflightCmd(q *qliksense.Qliksense) *cobra.Command {
var preflightCmd = &cobra.Command{
Use: "preflight",
Short: "perform preflight checks on the cluster",
Long: `perform preflight checks on the cluster`,
Example: `qliksense preflight <preflight_check_to_run>`,
}
return preflightCmd
}
func pfDnsCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightDnsCmd = &cobra.Command{
Use: "dns",
Short: "perform preflight dns check",
Long: `perform preflight dns check to check DNS connectivity status in the cluster`,
Example: `qliksense preflight dns`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight DNS check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight DNS check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.CheckDns(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight DNS check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight DNS check PASSED"))
return nil
},
}
f := preflightDnsCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightDnsCmd
}
func pfK8sVersionCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightCheckK8sVersionCmd = &cobra.Command{
Use: "kube-version",
Short: "check kubernetes version",
Long: `check minimum valid kubernetes version on the cluster`,
Example: `qliksense preflight kube-version`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight Kubernetes minimum version check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight kubernetes minimum version check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if err = qp.CheckK8sVersion(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight kubernetes minimum version check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight kubernetes minimum version check PASSED"))
return nil
},
}
f := preflightCheckK8sVersionCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightCheckK8sVersionCmd
}
func pfAllChecksCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightAllChecksCmd = &cobra.Command{
Use: "all",
Short: "perform all checks",
Long: `perform all preflight checks on the target cluster`,
Example: `qliksense preflight all`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight run all checks
fmt.Printf("Running all preflight checks...\n\n")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Unable to run the preflight checks suite"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.RunAllPreflightChecks(kubeConfigContents, namespace, preflightOpts); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("1 or more preflight checks have FAILED"))
fmt.Println("Completed running all preflight checks")
return nil
}
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("All preflight checks have PASSED"))
return nil
},
}
f := preflightAllChecksCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
f.StringVarP(&preflightOpts.MongoOptions.MongodbUrl, "mongodb-url", "", "", "mongodbUrl to try connecting to")
f.StringVarP(&preflightOpts.MongoOptions.Username, "mongodb-username", "", "", "username to connect to mongodb")
f.StringVarP(&preflightOpts.MongoOptions.Password, "mongodb-password", "", "", "password to connect to mongodb")
f.StringVarP(&preflightOpts.MongoOptions.CaCertFile, "mongodb-ca-cert", "", "", "certificate to use for mongodb check")
f.StringVarP(&preflightOpts.MongoOptions.ClientCertFile, "mongodb-client-cert", "", "", "client-certificate to use for mongodb check")
f.BoolVar(&preflightOpts.MongoOptions.Tls, "mongodb-tls", false, "enable tls?")
return preflightAllChecksCmd
}
func pfDeploymentCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var pfDeploymentCheckCmd = &cobra.Command{
Use: "deployment",
Short: "perform preflight deploymwnt check",
Long: `perform preflight deployment check to ensure that we can create deployments in the cluster`,
Example: `qliksense preflight deployment`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight deployments check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight deployment check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.CheckDeployment(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight deployment check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight deployment check PASSED"))
return nil
},
}
f := pfDeploymentCheckCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return pfDeploymentCheckCmd
}
func pfServiceCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var pfServiceCheckCmd = &cobra.Command{
Use: "service",
Short: "perform preflight service check",
Long: `perform preflight service check to ensure that we are able to create services in the cluster`,
Example: `qliksense preflight service`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight service check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight service check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.CheckService(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight service check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight service check PASSED"))
return nil
},
}
f := pfServiceCheckCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return pfServiceCheckCmd
}
func pfPodCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var pfPodCheckCmd = &cobra.Command{
Use: "pod",
Short: "perform preflight pod check",
Long: `perform preflight pod check to ensure we can create pods in the cluster`,
Example: `qliksense preflight pod`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight pod check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight pod check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.CheckPod(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight pod check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight pod check PASSED"))
return nil
},
}
f := pfPodCheckCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return pfPodCheckCmd
}
func pfCreateRoleCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightRoleCmd = &cobra.Command{
Use: "role",
Short: "preflight create role check",
Long: `perform preflight role check to ensure we are able to create a role in the cluster`,
Example: `qliksense preflight createRole`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight role check
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight role check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if err = qp.CheckCreateRole(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight role check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight role check PASSED"))
return nil
},
}
f := preflightRoleCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightRoleCmd
}
func pfCreateRoleBindingCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightRoleBindingCmd = &cobra.Command{
Use: "rolebinding",
Short: "preflight create rolebinding check",
Long: `perform preflight rolebinding check to ensure we are able to create a rolebinding in the cluster`,
Example: `qliksense preflight rolebinding`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight createRoleBinding check
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight rolebinding check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if err = qp.CheckCreateRoleBinding(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight rolebinding check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight rolebinding check PASSED"))
return nil
},
}
f := preflightRoleBindingCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightRoleBindingCmd
}
func pfCreateServiceAccountCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightServiceAccountCmd = &cobra.Command{
Use: "serviceaccount",
Short: "preflight create ServiceAccount check",
Long: `perform preflight serviceaccount check to ensure we are able to create a service account in the cluster`,
Example: `qliksense preflight serviceaccount`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight createServiceAccount check
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight ServiceAccount check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if err = qp.CheckCreateServiceAccount(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight ServiceAccount check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight ServiceAccount check PASSED"))
return nil
},
}
f := preflightServiceAccountCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightServiceAccountCmd
}
func pfCreateAuthCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightCreateAuthCmd = &cobra.Command{
Use: "authcheck",
Short: "preflight authcheck",
Long: `perform preflight authcheck that combines the role, rolebinding and serviceaccount checks`,
Example: `qliksense preflight authcheck`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight authcheck
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight authcheck FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if err = qp.CheckCreateRB(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight authcheck FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight authcheck PASSED"))
return nil
},
}
f := preflightCreateAuthCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
return preflightCreateAuthCmd
}
func pfMongoCheckCmd(q *qliksense.Qliksense) *cobra.Command {
out := ansi.NewColorableStdout()
preflightOpts := &preflight.PreflightOptions{
MongoOptions: &preflight.MongoOptions{},
}
var preflightMongoCmd = &cobra.Command{
Use: "mongo",
Short: "preflight mongo OR preflight mongo --url=<url>",
Long: `perform preflight mongo check to ensure we are able to connect to a mongodb instance in the cluster`,
Example: `qliksense preflight mongo OR preflight mongo --url=<url>`,
RunE: func(cmd *cobra.Command, args []string) error {
qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts}
// Preflight mongo check
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight mongo check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
if namespace == "" {
namespace = "default"
}
if err = qp.CheckMongo(kubeConfigContents, namespace, preflightOpts); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight mongo check FAILED"))
fmt.Printf("Error: %v\n", err)
return nil
}
fmt.Fprintf(out, "%s\n", chalk.Green.Color("Preflight mongo check PASSED"))
return nil
},
}
f := preflightMongoCmd.Flags()
f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode")
f.StringVarP(&preflightOpts.MongoOptions.MongodbUrl, "url", "", "", "mongodbUrl to try connecting to")
f.StringVarP(&preflightOpts.MongoOptions.Username, "username", "", "", "username to connect to mongodb")
f.StringVarP(&preflightOpts.MongoOptions.Password, "password", "", "", "password to connect to mongodb")
f.StringVarP(&preflightOpts.MongoOptions.CaCertFile, "ca-cert", "", "", "ca certificate to use for mongodb check")
f.StringVarP(&preflightOpts.MongoOptions.ClientCertFile, "client-cert", "", "", "client-certificate to use for mongodb check")
f.BoolVar(&preflightOpts.MongoOptions.Tls, "tls", false, "enable tls?")
return preflightMongoCmd
}

View File

@@ -1,27 +0,0 @@
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func pullQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
opts := &aboutCommandOptions{}
cmd := &cobra.Command{
Use: "pull",
Short: "Pull docke images for offline install",
Example: `qliksense pull`,
RunE: func(cmd *cobra.Command, args []string) error {
if gitRef, err := getAboutCommandGitRef(args); err != nil {
return err
} else if err = q.PullImages(gitRef, opts.Profile, false); err != nil {
return err
}
return nil
},
}
f := cmd.Flags()
f.StringVar(&opts.Profile, "profile", "", "Configuration profile")
return cmd
}

View File

@@ -0,0 +1,55 @@
package main
import (
"errors"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func pullQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
opts := &aboutCommandOptions{}
cmd := &cobra.Command{
Use: "pull",
Short: "Pull docker images for offline install",
Example: `qliksense pull`,
RunE: func(cmd *cobra.Command, args []string) error {
version, err := getSingleArg(args)
if err != nil {
return err
}
return q.PullImages(version, opts.Profile)
},
}
f := cmd.Flags()
f.StringVar(&opts.Profile, "profile", "", "Configuration profile")
return cmd
}
func pushQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
cmd := &cobra.Command{
Use: "push",
Short: "Push docker images for offline install",
Example: `qliksense push`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := ensureImageRegistrySetInCR(q); err != nil {
return err
} else {
return q.PushImagesForCurrentCR()
}
},
}
return cmd
}
func ensureImageRegistrySetInCR(q *qliksense.Qliksense) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if qcr, err := qConfig.GetCurrentCR(); err != nil {
return err
} else if registry := qcr.Spec.GetImageRegistry(); registry == "" {
return errors.New("no image registry set in the CR; to set it use: qliksense config set-image-registry")
}
return nil
}

View File

@@ -4,25 +4,36 @@ import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
ansi "github.com/mattn/go-colorable"
"github.com/mitchellh/go-homedir"
"github.com/qlik-oss/sense-installer/pkg"
"github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/ttacon/chalk"
)
// To run this project in ddebug mode, run:
// To run this project in debug mode, run:
// export QLIKSENSE_DEBUG=true
// qliksense <command>
const (
qlikSenseHomeVar = "QLIKSENSE_HOME"
qlikSenseDirVar = ".qliksense"
qlikSenseHomeVar = "QLIKSENSE_HOME"
qlikSenseDirVar = ".qliksense"
keepPatchFilesFlagName = "keep-config-repo-patches"
keepPatchFilesFlagUsage = "Keep config repo patch files (for debugging)"
pullFlagName = "pull"
pullFlagShorthand = "d"
pullFlagUsage = "If using private docker registry, pull (download) all required Qliksense images before install"
pushFlagName = "push"
pushFlagShorthand = "u"
pushFlagUsage = "If using private docker registry, push (upload) all downloaded Qliksense images to that registry before install"
rootCommandName = "qliksense"
)
func initAndExecute() error {
@@ -30,13 +41,18 @@ func initAndExecute() error {
qlikSenseHome string
err error
)
qlikSenseHome, err = setUpPaths()
if err != nil {
log.Fatal(err)
}
// create dirs and appropriate files for setting up contexts
api.LogDebugMessage("QliksenseHomeDir: %s", qlikSenseHome)
if err = rootCmd(qliksense.New(qlikSenseHome)).Execute(); err != nil {
qliksenseClient := qliksense.New(qlikSenseHome)
cmd := rootCmd(qliksenseClient)
if err := cmd.Execute(); err != nil {
//levenstein checks (auto-suggestions)
levenstein(cmd)
return err
}
@@ -58,60 +74,65 @@ func setUpPaths() (string, error) {
}
qlikSenseHome = filepath.Join(homeDir, qlikSenseDirVar)
}
os.Mkdir(qlikSenseHome, os.ModePerm)
if err := os.MkdirAll(qlikSenseHome, os.ModePerm); err != nil {
return "", err
}
return qlikSenseHome, nil
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of qliksense cli",
Long: `All software has versions. This is Hugo's`,
Long: "Print the version number of qliksense cli",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s (%s, %s)\n", pkg.Version, pkg.Commit, pkg.CommitDate)
},
}
func rootCmd(p *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
func commandUsesContext(commandName string) bool {
return commandName != "" &&
commandName != rootCommandName &&
commandName != fmt.Sprintf("%v help", rootCommandName) &&
commandName != fmt.Sprintf("%v version", rootCommandName)
}
cmd = &cobra.Command{
Use: "qliksense",
func getRootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd := &cobra.Command{
Use: rootCommandName,
Short: "Qliksense cli tool",
Long: `qliksense cli tool provides a wrapper around the porter api as well as
provides addition functionality`,
Args: cobra.ArbitraryArgs,
SilenceUsage: true,
Long: `qliksense cli tool provides functionality to perform operations on qliksense-k8s, qliksense operator, and kubernetes cluster`,
Args: cobra.ArbitraryArgs,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if commandUsesContext(cmd.CommandPath()) {
globalEulaPreRun(cmd, p)
if err := p.SetUpQliksenseDefaultContext(); err != nil {
panic(err)
}
pf := api.NewPreflightConfig(p.QliksenseHome)
if err := pf.Initialize(); err != nil {
panic(err)
}
globalEulaPostRun(cmd, p)
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if commandUsesContext(cmd.CommandPath()) {
globalEulaPostRun(cmd, p)
}
},
}
origHelpFunc := cmd.HelpFunc()
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
if !commandUsesContext(cmd.CommandPath()) {
cmd.Flags().MarkHidden("acceptEULA")
}
origHelpFunc(cmd, args)
})
accept := ""
cmd.PersistentFlags().StringVarP(&accept, "acceptEULA", "a", "", "Accept EULA for qliksense")
cmd.Flags().SetInterspersed(false)
cobra.OnInitialize(initConfig)
// For qliksense overrides/commands
cmd.AddCommand(pullQliksenseImages(p))
cmd.AddCommand(about(p))
// add version command
cmd.AddCommand(versionCmd)
// add operator command
cmd.AddCommand(operatorCmd)
operatorCmd.AddCommand(operatorViewCmd(p))
//add fetch command
cmd.AddCommand(fetchCmd(p))
// add install command
cmd.AddCommand(installCmd(p))
// add config command
cmd.AddCommand(configCmd)
configCmd.AddCommand(configApplyCmd(p))
configCmd.AddCommand(configViewCmd(p))
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
return cmd
}
@@ -120,30 +141,89 @@ func initConfig() {
viper.AutomaticEnv()
}
func downloadFile(url string, filepath string) error {
var (
out *os.File
err error
resp *http.Response
)
// Create the file
if out, err = os.Create(filepath); err != nil {
return err
}
defer out.Close()
func rootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd := getRootCmd(p)
cobra.OnInitialize(initConfig)
// Get the data
if resp, err = http.Get(url); err != nil {
return err
}
defer resp.Body.Close()
cmd.AddCommand(getInstallableVersionsCmd(p))
cmd.AddCommand(pullQliksenseImages(p))
cmd.AddCommand(pushQliksenseImages(p))
cmd.AddCommand(about(p))
// add version command
cmd.AddCommand(versionCmd)
// Write the body to file
if _, err = io.Copy(out, resp.Body); err != nil {
return err
}
// add operator command
cmd.AddCommand(operatorCmd)
//operatorCmd.AddCommand(operatorViewCmd(p))
operatorCmd.AddCommand(operatorCrdCmd(p))
operatorCmd.AddCommand(operatorControllerCmd(p))
return nil
//add fetch command
cmd.AddCommand(fetchCmd(p))
// add install command
cmd.AddCommand(installCmd(p))
// add config command
configCmd := configCmd(p)
cmd.AddCommand(configCmd)
configCmd.AddCommand(configApplyCmd(p))
configCmd.AddCommand(configViewCmd(p))
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// add the set-context config command as a sub-command to the app config command
configCmd.AddCommand(setContextConfigCmd(p))
// add the set profile/namespace/storageClassName/git-repository config command as a sub-command to the app config command
configCmd.AddCommand(setOtherConfigsCmd(p))
// add the set ### config command as a sub-command to the app config sub-command
configCmd.AddCommand(setConfigsCmd(p))
// add the set ### config command as a sub-command to the app config sub-command
configCmd.AddCommand(setSecretsCmd(p))
// add the list config command as a sub-command to the app config sub-command
configCmd.AddCommand(listContextConfigCmd(p))
// add the delete-context config command as a sub-command to the app config command
configCmd.AddCommand(deleteContextConfigCmd(p))
// add set-image-registry command as a sub-command to the app config sub-command
configCmd.AddCommand(setImageRegistryCmd(p))
// add clean-config-repo-patches command as a sub-command to the app config sub-command
configCmd.AddCommand(cleanConfigRepoPatchesCmd(p))
// open editor for config
configCmd.AddCommand(configEditCmd(p))
// add uninstall command
cmd.AddCommand(uninstallCmd(p))
// add crds
cmd.AddCommand(crdsCmd)
crdsCmd.AddCommand(crdsViewCmd(p))
crdsCmd.AddCommand(crdsInstallCmd(p))
// add preflight command
preflightCmd := preflightCmd(p)
preflightCmd.AddCommand(pfDnsCheckCmd(p))
preflightCmd.AddCommand(pfK8sVersionCheckCmd(p))
preflightCmd.AddCommand(pfAllChecksCmd(p))
preflightCmd.AddCommand(pfMongoCheckCmd(p))
preflightCmd.AddCommand(pfDeploymentCheckCmd(p))
preflightCmd.AddCommand(pfServiceCheckCmd(p))
preflightCmd.AddCommand(pfPodCheckCmd(p))
preflightCmd.AddCommand(pfCreateRoleCheckCmd(p))
preflightCmd.AddCommand(pfCreateRoleBindingCheckCmd(p))
preflightCmd.AddCommand(pfCreateServiceAccountCheckCmd(p))
preflightCmd.AddCommand(pfCreateAuthCheckCmd(p))
cmd.AddCommand(preflightCmd)
cmd.AddCommand(loadCrFile(p))
cmd.AddCommand((applyCmd(p)))
return cmd
}
func copy(src, dst string) (int64, error) {
@@ -173,3 +253,22 @@ func copy(src, dst string) (int64, error) {
nBytes, err = io.Copy(destination, source)
return nBytes, err
}
func levenstein(cmd *cobra.Command) {
cmd.SuggestionsMinimumDistance = 2
if len(os.Args) > 1 {
args := os.Args[1]
suggest := cmd.SuggestionsFor(args)
if len(suggest) > 0 {
arg := []string{}
for _, cm := range os.Args {
arg = append(arg, cm)
}
if !strings.EqualFold(arg[1], suggest[0]) {
arg[1] = suggest[0]
out := ansi.NewColorableStdout()
fmt.Fprintln(out, chalk.Green.Color("Did you mean: "), chalk.Bold.TextStyle(strings.Join(arg, " ")), "?")
}
}
}
}

View File

@@ -0,0 +1,28 @@
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func uninstallCmd(q *qliksense.Qliksense) *cobra.Command {
skipConfirmation := false
c := &cobra.Command{
Use: "uninstall",
Short: "Uninstall the deployed qliksense.",
Long: `Uninstall the deployed qliksense. By default uninstall the current context`,
Example: `qliksense uninstall <context-name>`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
return q.UninstallQK8s(args[0], skipConfirmation)
}
return q.UninstallQK8s("", skipConfirmation)
},
}
f := c.Flags()
f.BoolVar(&skipConfirmation, "yes", skipConfirmation, "skips confirmation")
return c
}

0
docs/air_gap.md Normal file
View File

145
docs/command_reference.md Normal file
View File

@@ -0,0 +1,145 @@
# CLI reference
### qliksense preflight
Preflight checks provide pre-installation cluster conformance testing and validation before we install qliksense on the cluster. We gather a suite of conformance tests that can be easily written and run on the target cluster to verify that cluster-specific requirements are met.
We support the following tests at the moment as part of preflight checks, and the range of the suite will be expanded in future.
Run the following command to view help about the commands supported by preflight at any moment:
```
qliksense preflight
```
#### Running all checks
Run the following command to execute all preflight checks
```
qliksense preflight all --mongodb-url=<mongo-server url> --mongodb-ca-cert=<path to ca-cert file>
```
#### Running specific check
Run the following command to execute a specific check
```
qliksense preflight dns
```
### qliksense load
`qliksense load` command takes input from a file or from pipe
- `qliksense load -f cr-file.yaml`
- `cat cr-file.yaml | qliksense load -f -`
This will load the Custom Resource (CR) into `${QLIKSENSE_HOME}` folder, create context structure and set the current context to that CR.
This will also encrypt the secrets from CR while writing the CR into the disk.
### qliksense apply
`qliksense apply` command takes input from a file or from pipe
- `qliksense apply -f cr-file.yaml`
- `cat cr-file.yaml | qliksense apply -f -`
The content of `cr-file.yaml` should be something like the following:
```yaml
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-test
labels:
version: v0.0.2
spec:
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: mongodb://qlik-test-mongodb:27017/qliksense?ssl=false
profile: docker-desktop
rotateKeys: "yes"
```
`qliksense apply` does everything `qliksense load` does but will install Qlik Sense into the cluster as well
### qliksense about
`qliksense about` command will display information about [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) release.
For example, running the following command will show information about default profile for `1.0.0` tag
```
qliksense about 1.0.0
```
Run the following command to view options for `about` command:
```
qliksense about --help
```
Using other supported commands user might have built the CR into the location `~/.qliksense/myqliksense.yaml`
```yaml
apiVersion: qlik.com/v1
kind: QlikSense
metadata:
name: myqliksense
spec:
profile: docker-desktop
manifestsRoot: /Usr/xyz/my-k8-repo/manifests
namespace: myqliksense
storageClassName: efs
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: "mongo://mongo:3307"
- name: messagingPassword
valueFromKey: messagingPassword
```
In this case, the result of `qliksense about` command would display information from:
- `/Usr/xyz/my-k8-repo/manifests/docker-desktop` location, or
- Pull and show information from `master` branch if the directory is invalid or empty
### qliksense config
`qliksense config` will perform operations on configurations and contexts regarding the [qliksense-k8](https://github.com/qlik-oss/qliksense-k8s) release.
It supports the following flags:
- `qliksense config apply` - generate the patches and apply manifests to K8s
- `qliksense config list-contexts` - get and list contexts
- `qliksense config set` - configure a key-value pair into the current context
- `qliksense config set-configs` - set configurations into qliksense context as key-value pairs
- `qliksense config set-context` - sets the Kubernetes context where resources are located
- `qliksense config set-secrets <service_name>.<attribute>="<value>" --secret=false` - set secrets configurations into qliksense context as key-value pairs and show encrypted value as part of CR
- `qliksense config set-secrets <service_name>.<attribute>="<value>" --secret=true` - set secrets configurations into qliksense context as key-value pairs and show a key reference to the created Kubernetes secret resource as part of the CR
- `qliksense config view` - view the qliksense operator CR
- `qliksense config delete-context` - deletes a specific context locally (not in-cluster). Deletes context in spec of `config.yaml` and locally deletes entire folder of specified context (does not delete secrets from cluster)
The global file which abstracts all contexts is `~/.qliksense/config.yaml`
```yaml
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: /Users/xyz/.qliksense/contexts/qlik-default/qlik-default.yaml
- name: myqliksense
crFile: /Users/xyz/.qliksense/contexts/myqliksense/myqliksense.yaml
- name: hello
crFile: /Users/xyz/.qliksense/contexts/hello/hello.yaml
currentContext: hello
```

96
docs/concepts.md Normal file
View File

@@ -0,0 +1,96 @@
# How CLI works
At the initialization, `qliksense` cli creates few files in the director `~/.qliksene` and it contains following files:
```console
.qliksense
├── config.yaml
├── contexts
│   └── qlik-default
│   └── qlik-default.yaml
└── ejson
└── keys
```
`qlik-default.yaml` is a default CR created with some default values like:
```yaml
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
profile: docker-desktop
secrets:
qliksense:
- name: mongoDbUri
value: mongodb://qlik-default-mongodb:27017/qliksense?ssl=false
rotateKeys: "yes"
releaseName: qlik-default
```
The `qliksense` cli creates a default qliksense context (different from kubectl context) named `qlik-default` which will be the prefix for all kubernetes resources created by the cli under this context later on.
New context and configuration can be created by the cli, get available commands using:
```console
qliksense config -h
```
---
`qliksense` cli works in two modes
- With a git repo fork/clone of [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s)
- Without git repo
## Without git repo
In this mode `qliksense` CLI downloads the specified version from [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and places it in `~/.qliksense/contexts/<context-name>/qlik-k8s` folder.
The qliksense cli creates a CR for the QlikSense operator and all config operations are performed to edit the CR.
`qliksense install` or `qliksense config apply` will generate patches in local file system (i.e `~/.qliksense/contexts/<context-name>/qlik-k8s`) and
- Install those manifests into the cluster
- Create a custom resource (CR) for the `qliksene operator`.
The operator makes the association to the installed resources so that when `qliksense uninstall` is performed the operator can delete all kubernetes resources related to QSEoK for the current context.
## With a git repo
Create a fork or clone of [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and push it to your git repo/server
To add your repo into CR, perform the following:
```bash
qliksense config set git.repository="https://github.com/my-org/qliksense-k8s"
qliksense config set git.accessToken="<mySecretToken>"
```
When you perform `qliksense install` or `qliksene config apply`, qliksense operator performs these tasks:
- Download corresponding version of manifests from the your git repo
- Generate kustomize patches
- Install kubernetes resources
- Push generated patches into a new branch in the provided git repo. _Gives you ability to merge patches into your master branch_
- Create a CronJob to monitor master branch. Any changes pushed to master branch will be applied into the cluster. _This is a light weight `git-ops` model_
## GitOps
To enable gitops, the following section should be in the CR
```yaml
....
spec:
git:
repository: https://github.com/<OWNER>/<REPO>
accessToken: "<git-token>"
userName: "<git-username>"
gitOps:
enabled: "yes"
schedule: "*/5 * * * *"
watchBranch: <myBranch>
image: qlik-docker-oss.bintray.io/qliksense-repo-watcher
....
```

53
docs/getting_started.md Normal file
View File

@@ -0,0 +1,53 @@
# Getting started
## Requirements
- Kubernetes cluster (Docker Desktop with enabled Kubernetes)
- `kubectl` installed, configured and able to communicate with kubernetes cluster. _`qliksense` CLI uses `kubectl` under the hood to perform operations on cluster_
## Installing `qliksense` CLI
Download the executable for your platform from [releases page](https://github.com/qlik-oss/sense-installer/releases) and rename it to `qliksense`
??? tldr "Linux"
``` bash
curl -Lo qliksense https://github.com/qlik-oss/sense-installer/releases/download/v0.7.0/qliksense-linux-amd64
chmod +x qliksense
sudo mv qliksense /usr/local/bin
```
??? tldr "MacOS"
``` bash
curl -Lo qliksense https://github.com/qlik-oss/sense-installer/releases/download/v0.7.0/qliksense-darwin-amd64
chmod +x qliksense
sudo mv qliksense /usr/local/bin
```
??? tldr "Windows"
Download Windows executable and add it in your `PATH` as `qliksense.exe`
[https://github.com/qlik-oss/sense-installer/releases/download/v0.7.0/qliksense-windows-amd64.exe](https://github.com/qlik-oss/sense-installer/releases/download/v0.7.0/qliksense-windows-amd64.exe)
## Quick start
- To download the version `v0.0.2` from qliksense-k8s [releases](https://github.com/qlik-oss/qliksense-k8s/releases).
```shell
qliksense fetch v0.0.2
```
- To install CRDs for QSEoK and qliksense operator into the kubernetes cluster.
```shell
qliksense crds install --all
```
- To install QSEoK into a namespace in the kubernetes cluster where `kubectl` is pointing to.
```shell
qliksense install --acceptEULA="yes"
```

15
docs/index.md Normal file
View File

@@ -0,0 +1,15 @@
# Overview
The Qlik Sense on Kubernetes CLI (`qliksense`) provides an imperative interface to many of the configurations that need to be applied against the declarative structure described in [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s).
The CLI facilitates:
- Installation of QSEoK
- Installation of qliksense operator to manage QSEoK
- Air gapped installation of QSEoK
!!! info ""
This is a technology preview that uses Qlik modified [kustomize](https://github.com/qlik-oss/kustomize) for Kubernetes manifests on [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) repository
!!! info ""
See QlikSense [edge releases on qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s/releases) repository

256
docs/preflight_checks.md Normal file
View File

@@ -0,0 +1,256 @@
# Preflight checks
Preflight checks provide pre-installation cluster conformance testing and validation before we install qliksense on the cluster. We gather a suite of conformance tests that can be easily written and run on the target cluster to verify that cluster-specific requirements are met.
We support the following tests at the moment as part of preflight checks, and the range of the suite will be expanded in future.
Run the following command to view help about the commands supported by preflight at any moment:
```shell
$ qliksense preflight
perform preflight checks on the cluster
Usage:
qliksense preflight [command]
Examples:
qliksense preflight <preflight_check_to_run>
Available Commands:
all perform all checks
dns perform preflight dns check
k8s-version check k8s version
Flags:
-h, --help help for preflight
```
### DNS check
Run the following command to perform preflight DNS check. We setup a kubernetes deployment and try to reach it as part of establishing DNS connectivity in this check.
The expected output should be similar to the one shown below.
```shell
$ qliksense preflight dns
Preflight DNS check
---------------------
Created deployment "dep-dns-preflight-check"
Created service "svc-dns-pf-check"
Created pod: pf-pod-1
Fetching pod: pf-pod-1
Fetching pod: pf-pod-1
Exec-ing into the container...
Preflight DNS check: PASSED
Completed preflight DNS check
Cleaning up resources...
Deleted pod: pf-pod-1
Deleted service: svc-dns-pf-check
Deleted deployment: dep-dns-preflight-check
```
### Kubernetes version check
We check the version of the target kubernetes cluster and ensure that it falls in the valid range of kubernetes versions that are supported by qliksense.
The command to run this check and the expected similar output are as shown below:
```shell
$ qliksense preflight k8s-version
Preflight kubernetes minimum version check
------------------------------------------
Kubernetes API Server version: v1.15.5
Current K8s Version: 1.15.5
Current 1.15.5 is greater than minimum required version:1.11.0, hence good to go
Preflight minimum kubernetes version check: PASSED
Completed Preflight kubernetes minimum version check
```
### Service check
We use the commmand below to test if we are able to create a service in the cluster.
```shell
$ qliksense preflight service
Preflight service check
-----------------------
Preflight service check:
Created service "svc-pf-check"
Preflight service creation check: PASSED
Cleaning up resources...
Deleted service: svc-pf-check
Completed preflight service check
```
### Deployment check
We use the commmand below to test if we are able to create a deployment in the cluster. After the test exexutes, we wait until the created deployment terminates before we exit the command.
```shell
$ qliksense preflight deployment
Preflight deployment check
-----------------------
Preflight deployment check:
Created deployment "deployment-preflight-check"
Preflight Deployment check: PASSED
Cleaning up resources...
Deleted deployment: deployment-preflight-check
Completed preflight deployment check
```
### Pod check
We use the commmand below to test if we are able to create a pod in the cluster.
```shell
$ qliksense preflight pod
Preflight pod check
--------------------
Preflight pod check:
Created pod: pod-pf-check
Preflight pod creation check: PASSED
Cleaning up resources...
Deleted pod: pod-pf-check
Completed preflight pod check
```
### Create-Role check
We use the command below to test if we are able to create a role in the cluster
```shell
$ qliksense preflight create-role
Preflight create-role check
---------------------------
Preflight create-role check:
Created role: role-preflight-check
Preflight create-role check: PASSED
Cleaning up resources...
Deleted role: role-preflight-check
Completed preflight create-role check
```
### Create-RoleBinding check
We use the command below to test if we are able to create a role binding in the cluster
```shell
$ qliksense preflight createRoleBinding
Preflight create roleBinding check
---------------------------
Preflight createRoleBinding check:
Created RoleBinding: role-binding-preflight-check
Preflight createRoleBinding check: PASSED
Cleaning up resources...
Deleting RoleBinding: role-binding-preflight-check
Deleted RoleBinding: role-binding-preflight-check
Completed preflight createRoleBinding check
```
### Create-ServiceAccount check
We use the command below to test if we are able to create a service account in the cluster
```shell
$ qliksense preflight createServiceAccount
Preflight create ServiceAccount check
-------------------------------------
Preflight createServiceAccount check:
Created Service Account: preflight-check-test-serviceaccount
Preflight createServiceAccount check: PASSED
Cleaning up resources...
Deleting ServiceAccount: preflight-check-test-serviceaccount
Deleted ServiceAccount: preflight-check-test-serviceaccount
Completed preflight createServiceAccount check
```
### CreateRB check
We use the command below to combine creation of role, role binding, and service account tests
```shell
$ qliksense preflight createRB
Preflight createRB check
-------------------------------------
Preflight create-role check:
Created role: role-preflight-check
Preflight create-role check: PASSED
Cleaning up resources...
Deleted role: role-preflight-check
Completed preflight create-role check
Preflight create RoleBinding check:
Created RoleBinding: role-binding-preflight-check
Preflight create RoleBinding check: PASSED
Cleaning up resources...
Deleted RoleBinding: role-binding-preflight-check
Completed preflight create RoleBinding check
Preflight createServiceAccount check:
Created Service Account: preflight-check-test-serviceaccount
Preflight createServiceAccount check: PASSED
Cleaning up resources...
Deleted ServiceAccount: preflight-check-test-serviceaccount
Completed preflight createServiceAccount check
Completed preflight CreateRB check
```
### Mongodb check
We can check if we are able to connect to an instance of mongodb on the cluster by either supplying the mongodbUri as part of the command or infer it from the current context.
```shell
qliksense preflight mongo --url=<url> OR
qliksense preflight mongo
qliksense preflight mongo --url=<mongo-server url> --ca-cert=<path to ca-cert file>
Preflight mongo check
---------------------
Preflight mongodb check:
Created pod: pf-mongo-pod
stdout: MongoDB shell version v4.2.5
connecting to: <url>/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("...") }
MongoDB server version: 4.2.5
bye
stderr:
Preflight mongo check: PASSED
Deleted pod: pf-mongo-pod
Completed preflight mongodb check
```
### Running all checks
Run the command shown below to execute all preflight checks.
```shell
$ qliksense preflight all --mongodb-url=<url> OR
$ qliksense preflight all --mongodb-url=<mongo-server url> --mongodb-ca-cert=<path to ca-cert file>
Running all preflight checks
Preflight DNS check
-------------------
Created deployment "dep-dns-preflight-check"
Created service "svc-dns-pf-check"
Created pod: pf-pod-1
Fetching pod: pf-pod-1
Fetching pod: pf-pod-1
Exec-ing into the container...
Preflight DNS check: PASSED
Completed preflight DNS check
Cleaning up resources...
Deleted pod: pf-pod-1
Deleted service: svc-dns-pf-check
Deleted deployment: dep-dns-preflight-check
Preflight kubernetes minimum version check
------------------------------------------
Kubernetes API Server version: v1.15.5
Current K8s Version: 1.15.5
Current 1.15.5 is greater than minimum required version:1.11.0, hence good to go
Preflight minimum kubernetes version check: PASSED
Completed Preflight kubernetes minimum version check
...
...
All preflight checks have PASSED
Completed running all preflight checks
```

67
go.mod
View File

@@ -3,71 +3,60 @@ module github.com/qlik-oss/sense-installer
go 1.13
replace (
github.com/Sirupsen/logrus v1.0.5 => github.com/sirupsen/logrus v1.0.5
github.com/Sirupsen/logrus v1.3.0 => github.com/Sirupsen/logrus v1.0.6
github.com/Sirupsen/logrus v1.4.0 => github.com/sirupsen/logrus v1.0.6
// github.com/containerd/containerd v1.3.0-0.20190507210959-7c1e88399ec0 => github.com/containerd/containerd v1.3.2
github.com/docker/docker => github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309
// github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
// golang.org/x/crypto v0.0.0-20190129210102-0709b304e793 => golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8
k8s.io/client-go => k8s.io/client-go v0.0.0-20191016111102-bec269661e48
k8s.io/kubectl => k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51
sigs.k8s.io/kustomize/api => github.com/qlik-oss/kustomize/api v0.3.3-0.20200129153315-09eb26c762c8
k8s.io/apimachinery => k8s.io/apimachinery v0.17.0
k8s.io/client-go => k8s.io/client-go v0.17.0
k8s.io/kubectl => k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd
sigs.k8s.io/kustomize/api => github.com/qlik-oss/kustomize/api v0.3.3-0.20200421163508-bc74267cca03
)
require (
cloud.google.com/go v0.52.0 // indirect
cloud.google.com/go/storage v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.0.3
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/Shopify/ejson v1.2.1
github.com/aws/aws-sdk-go v1.28.9 // indirect
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
github.com/bugsnag/bugsnag-go v1.5.3 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cloudflare/cfssl v1.4.1 // indirect
github.com/containerd/containerd v1.3.2 // indirect
github.com/containerd/continuity v0.0.0-20191214063359-1097c8bae83b // indirect
github.com/containers/image/v5 v5.1.0
github.com/docker/cli v0.0.0-20191212191748-ebca1413117a
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7
github.com/docker/go v1.5.1-1 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/go-git/go-git/v5 v5.0.0
github.com/gobuffalo/envy v1.9.0 // indirect
github.com/gobuffalo/logger v1.0.3 // indirect
github.com/gobuffalo/packd v1.0.0 // indirect
github.com/gobuffalo/packr/v2 v2.7.1
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/gorilla/mux v1.7.3 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jinzhu/gorm v1.9.11 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/kyokomi/emoji v2.2.2+incompatible
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-tty v0.0.3
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0 // indirect
github.com/pkg/errors v0.8.1
github.com/qlik-oss/k-apis v0.0.2
github.com/spf13/cobra v0.0.5
github.com/otiai10/copy v1.1.1
github.com/pkg/errors v0.9.1
github.com/qlik-oss/k-apis v0.1.1
github.com/robfig/cron/v3 v3.0.1
github.com/rogpeppe/go-internal v1.5.2 // indirect
github.com/spf13/cobra v0.0.6
github.com/spf13/viper v1.6.1
github.com/theupdateframework/notary v0.6.1 // indirect
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 // indirect
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 // indirect
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/tools v0.0.0-20200312194400-c312e98713c2 // indirect
google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b // indirect
google.golang.org/grpc v1.27.0 // indirect
gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect
gopkg.in/fatih/pool.v2 v2.0.0 // indirect
gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v11.0.0+incompatible
sigs.k8s.io/kustomize/api v0.3.2
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect
sigs.k8s.io/yaml v1.1.0
)
exclude github.com/Azure/go-autorest v12.0.0+incompatible

304
go.sum
View File

@@ -1,9 +1,6 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
@@ -38,13 +35,16 @@ github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dY
github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU=
github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZOMdj5HYo=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v30.1.0+incompatible h1:HyYPft8wXpxMd0kfLtXo6etWcO+XuPbLkcgx9g2cqxU=
github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0=
github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFEY95rZLK0Tj6o=
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v12.1.0+incompatible h1:x0sVyfVo0Qw9jcgVHuKIAiTHGRvQ9PsJP+43TVPV/DM=
github.com/Azure/go-autorest v12.1.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
@@ -56,6 +56,7 @@ github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjW
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
@@ -64,9 +65,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190605020000-c4ba1fdf4d36/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA=
github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
@@ -77,6 +77,7 @@ github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0
github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+BQa3B8=
github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU=
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
@@ -98,25 +99,21 @@ github.com/Shopify/ejson v1.2.1 h1:Dx0Ipn0mUgrZlzIa5oIUrH0rdSmBOyod/UJmQQK1KHo=
github.com/Shopify/ejson v1.2.1/go.mod h1:J8cw5GOA0l/aMOPp+uDfwNYVbeqIaBhzRkv1+76UCvk=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -128,6 +125,9 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -136,13 +136,14 @@ github.com/aws/aws-sdk-go v1.25.36 h1:4+TL/Y2G5hsR1zdfHmjNG1ou1WEqsSWk8v7m1GaDKy
github.com/aws/aws-sdk-go v1.25.36/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0=
github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@@ -154,16 +155,16 @@ github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx2
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/bugsnag-go v1.5.3 h1:yeRUT3mUE13jL1tGwvoQsKdVbAsQx9AJ+fqahKveP04=
github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
@@ -175,11 +176,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw=
github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
@@ -194,6 +191,7 @@ github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191214063359-1097c8bae83b h1:pik3LX++5O3UiNWv45wfP/WT81l7ukBJzd3uUiifbSU=
github.com/containerd/continuity v0.0.0-20191214063359-1097c8bae83b/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
@@ -218,14 +216,16 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -233,28 +233,33 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/deislabs/oras v0.7.0 h1:RnDoFd3tQYODMiUqxgQ8JxlrlWL0/VMKIKRD01MmNYk=
github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c=
github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v0.0.0-20191212191748-ebca1413117a h1:rgpgmLocRiSIM3zdtVgJcyvH7S2cSiIPtL7LvFY8K/0=
github.com/docker/cli v0.0.0-20191212191748-ebca1413117a/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw=
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20170817175659-5f6282db7d65/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k=
github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.0.0-20180212134524-7beb39f0b969/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
@@ -263,8 +268,10 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libkv v0.2.1 h1:PNXYaftMVCFS5CmnDtDWTg3wbBO61Q/cEo3KX1oKxto=
github.com/docker/libkv v0.2.1/go.mod h1:r5hEwHwW8dr0TFBYGCarMNbrQOiwL1xoqDYZ/JqoTK0=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s=
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
@@ -274,14 +281,13 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.11.1+incompatible h1:CjKsv3uWcCMvySPQYKxO8XX3f9zD4FeZRsW4G0B4ffE=
github.com/emicklei/go-restful v2.11.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@@ -289,8 +295,6 @@ github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -313,9 +317,9 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v0.0.0-20161207003320-04f313413ffd/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -326,6 +330,14 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-critic/go-critic v0.3.5-0.20190904082202-d79a9f0c64db/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
@@ -341,6 +353,7 @@ github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70t
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
@@ -360,17 +373,21 @@ github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
@@ -379,7 +396,7 @@ github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tF
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -400,22 +417,28 @@ github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE=
github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc=
github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM=
github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI=
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=
github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v0.0.0-20170815085658-fcdc5011193f/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
@@ -448,7 +471,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
@@ -472,8 +494,6 @@ github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -517,6 +537,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0=
github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
@@ -524,32 +545,39 @@ github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEo
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=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v0.0.0-20170217192616-94e7d24fd285/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/gosuri/uitable v0.0.1 h1:M9sMNgSZPyAu1FJZJLpJ16ofL8q5ko2EDUkICsynvlY=
github.com/gosuri/uitable v0.0.1/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w=
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM=
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hairyhenderson/gomplate/v3 v3.6.0 h1:EryWG7cCxvZ2awoZ957B3AMAd20Zy0uRXeZ7TXXMIp0=
github.com/hairyhenderson/gomplate/v3 v3.6.0/go.mod h1:RbEC6Y14nNTHCtNWpBAkwqDP4ICFUrAH0S8PUFa0qT4=
github.com/hairyhenderson/toml v0.3.1-0.20191004034452-2a4f3b6160f2 h1:Dc4YWWuY02jqhCnErAH++juCTwEPLstAOOVhyPXeE7Q=
@@ -592,6 +620,8 @@ github.com/hashicorp/go-retryablehttp v0.6.3/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
@@ -599,6 +629,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
@@ -643,17 +674,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/johannesboyne/gofakes3 v0.0.0-20191029185751-e238f04965fe h1:9kkgzfTjcHQqS6wGlEhJBJmAMI75lKyHX69w/ii+5So=
github.com/johannesboyne/gofakes3 v0.0.0-20191029185751-e238f04965fe/go.mod h1:cPDudDcSR9fls3ZmrXgt0GU2QpQGQRJc4JBNtKyNr1s=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
@@ -663,6 +687,8 @@ github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBv
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/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 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -677,8 +703,6 @@ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@@ -703,11 +727,14 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kyokomi/emoji v2.2.2+incompatible h1:gaQFbK2+uSxOR4iGZprJAbpmtqTrHhSdgOyIMD6Oidc=
github.com/kyokomi/emoji v2.2.2+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
@@ -721,6 +748,7 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -735,16 +763,18 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -753,8 +783,6 @@ github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08
github.com/miekg/dns v0.0.0-20181005163659-0d29b283ac0f/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.1+incompatible h1:gAMO1HM9xBRONLHHYnu5iFsOJUiJdNZo6oqSENd4eW8=
github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@@ -778,6 +806,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309 h1:cvy4lBOYN3gKfKj8Lzz5Q9TfviP+L7koMHY7SvkyTKs=
@@ -793,34 +822,36 @@ github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mtrmac/gpgme v0.0.0-20170102180018-b2432428689c h1:xa+eQWKuJ9MbB9FBL/eoNvDFvveAkz2LQoz8PzX7Q/4=
github.com/mtrmac/gpgme v0.0.0-20170102180018-b2432428689c/go.mod h1:GhAqVMEWnTcW2dxoD/SO3n2enrgWl3y6Dnx4m59GvcA=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 h1:yN8BPXVwMBAm3Cuvh1L5XE8XpvYRMdsVLd82ILprhUU=
@@ -834,9 +865,16 @@ github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.m
github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
github.com/opencontainers/selinux v1.3.0 h1:xsI95WzPZu5exzA6JzkLSfdr/DilzOhCJOqGe5TgR0g=
github.com/opencontainers/selinux v1.3.0/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913 h1:TnbXhKzrTOyuvWrjI8W6pcoI9XPbLHFXCdN2dtUw7Rw=
github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc=
github.com/otiai10/copy v1.1.1 h1:PH7IFlRQ6Fv9vYmuXbDRLdgTHoP1w483kPNUP2bskpo=
github.com/otiai10/copy v1.1.1/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -848,6 +886,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5 h1:rZQtoozkfsiNs36c7Tdv/gyGNzD1X1XWKO8rptVNZuM=
github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.3.0+incompatible h1:CZzRn4Ut9GbUkHlQ7jqBXeZQV41ZSKWFc302ZU6lUTk=
github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -857,6 +896,8 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7 h1:RcqIXZDN7Vz5lgK7+0
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -865,21 +906,22 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY
github.com/pquerna/ffjson v0.0.0-20181028064349-e517b90714f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9 h1:kyf9snWXHvQc+yxE9imhdI8YAm4oKeZISlaAR+x73zs=
github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -889,9 +931,9 @@ github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkp
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190129233650-316cf8ccfec5/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -900,17 +942,18 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/qlik-oss/k-apis v0.0.1 h1:XgKSi2Lu/Jsc0XeBRCObw8urt9vVjtgr7INq6kjJpV0=
github.com/qlik-oss/k-apis v0.0.1/go.mod h1:FDHuThas5MNw192Sk2IGy9fv8S06BLiuz2vfskZ162Q=
github.com/qlik-oss/k-apis v0.0.2 h1:7Sdz7528of+34NijkhxSc964FTOl+RV3IMMJxiScmd0=
github.com/qlik-oss/k-apis v0.0.2/go.mod h1:KOFzKVIdRqp47ytnHg3+9zb8fTlnrQjO6aKiwcrCJUE=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200129153315-09eb26c762c8 h1:WLVkArXf58T+3SHDvSddE8P6OjkeeUtGEgHk8LDdfeo=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200129153315-09eb26c762c8/go.mod h1:OCt7FTrRVHj4kmR2xLJJUIqu00BTr6GeF09hSmM17Kw=
github.com/qlik-oss/k-apis v0.1.1 h1:aZ4eTMB3mSn03Kuj7+RI0eFLkjK9+0qxADBioRb3qVA=
github.com/qlik-oss/k-apis v0.1.1/go.mod h1:yoYGgPJ/H0t9H3NSq64dWfyQY6QWi2L9c+hCJoVO03U=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200402170547-2e8140160c36 h1:BuT+cnXPQ6mcOWTDS1S8GXy65LAEMdPuNQCC36rMq28=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200402170547-2e8140160c36/go.mod h1:tSQaDZ4Jt9KwYvD7LlMUPi5nkiGOno3PAKl5/XqEfxs=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200421163508-bc74267cca03 h1:xmM3TiZW2o60gkD8V3xMGdX3HDlbR+bhJbWIs3Oo+S0=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200421163508-bc74267cca03/go.mod h1:Yg8bqX8Mq/eSgXfcenxCxhZuSXg+NCsKq6NBdch/oUc=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -918,6 +961,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY=
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -927,6 +972,7 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/securego/gosec v0.0.0-20191002120514-e680875ea14d/go.mod h1:w5+eXa0mYznDkHaMCXA4XYffjlH+cy1oyKbfzJXa2Do=
@@ -940,17 +986,18 @@ github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lz
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/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=
@@ -970,10 +1017,13 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
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=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -999,11 +1049,12 @@ github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia v2.3.0+incompatible h1:GkY4dP3cEfEASBPPkWd+AmjYxhmDkqO9/zg7R0lSQRs=
github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
@@ -1011,25 +1062,24 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/uudashr/gocognit v0.0.0-20190926065955-1655d0de0517/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vbatts/tar-split v0.11.1 h1:0Odu65rhcZ3JZaPHxl7tCI3V/C/Q9Zf82UFravl02dE=
github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
github.com/vbauerster/mpb/v4 v4.11.1 h1:ZOYQSVHgmeanXsbyC44aDg76tBGCS/54Rk8VkL8dJGA=
github.com/vbauerster/mpb/v4 v4.11.1/go.mod h1:vMLa1J/ZKC83G2lB/52XpqT+ZZtFG4aZOdKhmpRL1uM=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU=
github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -1048,36 +1098,36 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek=
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yujunz/go-getter v1.4.1-lite h1:FhvNc94AXMZkfqUwfMKhnQEC9phkphSGdPTL7tIdhOM=
github.com/yujunz/go-getter v1.4.1-lite/go.mod h1:sbmqxXjyLunH1PkF3n7zSlnVeMvmYUuIl9ZVs/7NyCc=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 h1:p7OofyZ509h8DmPLh8Hn+EIIZm/xYhdZHJ9GnXHdr6U=
github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/gorelic v0.0.6 h1:qMJQYPNdtJ7UNYHjX38KXZtltKTqimMuoQjNnSVIuJg=
github.com/yvasiyarov/gorelic v0.0.6/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zealic/xignore v0.3.3 h1:EpLXUgZY/JEzFkTc+Y/VYypzXtNz+MSOMVCGW5Q4CKQ=
github.com/zealic/xignore v0.3.3/go.mod h1:lhS8V7fuSOtJOKsvKI7WfsZE276/7AYEqokv3UiqEAU=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks/UsjeFdn5O5JpLUtzqk9U8xXw=
github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8=
github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb h1:vxqkjztXSaPVDc8FQCdHTaejm2x747f6yPbnu1h2xkg=
github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -1094,22 +1144,27 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1142,7 +1197,10 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1155,7 +1213,6 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -1174,8 +1231,10 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
@@ -1184,6 +1243,10 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCT
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1213,6 +1276,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1227,6 +1291,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
@@ -1246,6 +1311,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -1255,19 +1321,21 @@ golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDq
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c h1:2EA2K0k9bcvvEDlqD8xdlOhCOqq+O/p9Voqi4x9W1YU=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200312194400-c312e98713c2 h1:6TB4+MaZlkcSsJDu+BS5yxSEuZIYhjWz+jhbSLEZylI=
golang.org/x/tools v0.0.0-20200312194400-c312e98713c2/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1277,8 +1345,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -1301,6 +1368,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
@@ -1326,9 +1394,9 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b h1:c8OBoXP3kTbDWWB/oVE3FkR851p4iZ3MPadz7zXEIPU=
google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
@@ -1336,6 +1404,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -1345,20 +1414,19 @@ gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/dancannon/gorethink.v3 v3.0.5 h1:/g7PWP7zUS6vSNmHSDbjCHQh1Rqn8Jy6zSMQxAsBSMQ=
gopkg.in/dancannon/gorethink.v3 v3.0.5/go.mod h1:GXsi1e3N2OcKhcP6nsYABTiUejbWMFO4GY5a4pEaeEc=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg=
gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU=
gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I=
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
@@ -1398,6 +1466,8 @@ gotest.tools/v3 v3.0.0 h1:d+tVGRu6X0ZBQ+kyAR8JKi6AXhTP2gmQaoIYaGFz634=
gotest.tools/v3 v3.0.0/go.mod h1:TUP+/YtXl/dp++T+SZ5v2zUmLVBHmptSb/ajDLCJ+3c=
helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo=
helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
helm.sh/helm/v3 v3.1.2 h1:VpNzaNv2DX4aRnOCcV7v5Of+XT2SZrJ8iOQ25AGKOos=
helm.sh/helm/v3 v3.1.2/go.mod h1:WYsFJuMASa/4XUqLyv54s0U/f3mlAaRErGmyy4z921g=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1407,19 +1477,37 @@ honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXe
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
k8s.io/api v0.0.0-20191016110408-35e52d86657a/go.mod h1:/L5qH+AD540e7Cetbui1tuJeXdmNhO8jM6VkXeDdDhQ=
k8s.io/api v0.0.0-20191214185829-ca1d04f8b0d3/go.mod h1:itOjKREfmUTvcjantxOsyYU5mbFsU7qUnyUuRfF5+5M=
k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM=
k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apiextensions-apiserver v0.0.0-20191016113550-5357c4baaf65 h1:kThoiqgMsSwBdMK/lPgjtYTsEjbUU9nXCA9DyU3feok=
k8s.io/apiextensions-apiserver v0.0.0-20191016113550-5357c4baaf65/go.mod h1:5BINdGqggRXXKnDgpwoJ7PyQH8f+Ypp02fvVNcIFy9s=
k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss=
k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8 h1:Iieh/ZEgT3BWwbLD5qEKcY06jKuPEl6zC7gPSehoLw4=
k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo=
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/apiserver v0.0.0-20191016112112-5190913f932d/go.mod h1:7OqfAolfWxUM/jJ/HBLyE+cdaWFBUoo5Q5pHgJVj2ws=
k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
k8s.io/cli-runtime v0.0.0-20191016114015-74ad18325ed5 h1:8ZfMjkMBzcXEawLsYHg9lDM7aLEVso3NiVKfUTnN56A=
k8s.io/cli-runtime v0.0.0-20191016114015-74ad18325ed5/go.mod h1:sDl6WKSQkDM6zS1u9F49a0VooQ3ycYFBFLqd2jf2Xfo=
k8s.io/cli-runtime v0.0.0-20191214191754-e6dc6d5c8724/go.mod h1:wzlq80lvjgHW9if6MlE4OIGC86MDKsy5jtl9nxz/IYY=
k8s.io/cli-runtime v0.17.2 h1:YH4txSplyGudvxjhAJeHEtXc7Tr/16clKGfN076ydGk=
k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI=
k8s.io/client-go v0.0.0-20191016111102-bec269661e48 h1:C2XVy2z0dV94q9hSSoCuTPp1KOG7IegvbdXuz9VGxoU=
k8s.io/client-go v0.0.0-20191016111102-bec269661e48/go.mod h1:hrwktSwYGI4JK+TJA3dMaFyyvHVi/aLarVHpbs8bgCU=
k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg=
k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=
k8s.io/code-generator v0.0.0-20191004115455-8e001e5d1894/go.mod h1:mJUgkl06XV4kstAnLHAIzJPVCOzVR+ZcfPIv4fUsFCY=
k8s.io/code-generator v0.0.0-20191214185510-0b9b3c99f9f2/go.mod h1:BjGKcoq1MRUmcssvHiSxodCco1T6nVIt4YeCT5CMSao=
k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
k8s.io/component-base v0.0.0-20191016111319-039242c015a9/go.mod h1:SuWowIgd/dtU/m/iv8OD9eOxp3QZBBhTIiWMsBQvKjI=
k8s.io/component-base v0.0.0-20191214190519-d868452632e2/go.mod h1:wupxkh1T/oUDqyTtcIjiEfpbmIHGm8By/vqpSKC6z8c=
k8s.io/component-base v0.17.2 h1:0XHf+cerTvL9I5Xwn9v+0jmqzGAZI7zNydv4tL6Cw6A=
k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
@@ -1433,11 +1521,16 @@ k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLy
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51 h1:RBkTKVMF+xsNsSOVc0+HdC0B5gD1sr6s6Cu5w9qNbuQ=
k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51/go.mod h1:gL826ZTIfD4vXTGlmzgTbliCAT9NGiqpCqK2aNYv5MQ=
k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd h1:nZX5+wEqTu/EBIYjrZlFOA63z4+Zcy96lDkCZPU9a9c=
k8s.io/kubectl v0.0.0-20191219154910-1528d4eea6dd/go.mod h1:9ehGcuUGjXVZh0qbYSB0vvofQw2JQe6c6cO0k4wu/Oo=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/metrics v0.0.0-20191016113814-3b1a734dba6e/go.mod h1:ve7/vMWeY5lEBkZf6Bt5TTbGS3b8wAxwGbdXAsufjRs=
k8s.io/metrics v0.0.0-20191214191643-6b1944c9f765/go.mod h1:5V7rewilItwK0cz4nomU0b3XCcees2Ka5EBYWS1HBeM=
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20191010214722-8d271d903fe4 h1:Gi+/O1saihwDqnlmC8Vhv1M5Sp4+rbOmK9TbsLn8ZEA=
k8s.io/utils v0.0.0-20191010214722-8d271d903fe4/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
@@ -1450,12 +1543,13 @@ pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/letsencrypt v0.0.1 h1:DV0d09Ne9E7UUa9ZqWktZ9L2VmybgTgfq7xlfFR/bbU=
rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ=
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=
vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=

23
mkdocs.yml Normal file
View File

@@ -0,0 +1,23 @@
site_name: Qlik Sense on Kubernetes CLI
repo_url: 'https://github.com/qlik-oss/sense-installer'
strict: true
theme:
name: "material"
palette:
primary: 'green'
accent: 'indigo'
markdown_extensions:
- toc:
permalink: true
- admonition
- codehilite
- pymdownx.inlinehilite
- pymdownx.superfences
- pymdownx.details
nav:
- Overview: index.md
- getting_started.md
- command_reference.md
- concepts.md
- air_gap.md
- Releases ⧉: https://github.com/qlik-oss/sense-installer/releases

BIN
pkg/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -3,23 +3,30 @@ package api
import (
"errors"
"fmt"
"github.com/jinzhu/copier"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/qlik-oss/k-apis/pkg/config"
b64 "encoding/base64"
"github.com/jinzhu/copier"
)
const (
pushSecretFileName = "image-registry-push-secret.yaml"
pullSecretFileName = "image-registry-pull-secret.yaml"
qliksenseContextsDirName = "contexts"
qliksenseSecretsDirName = "secrets"
qliksenseEjsonDirName = "ejson"
QLIK_GIT_REPO = "https://github.com/qlik-oss/qliksense-k8s"
)
// NewQConfig create QliksenseConfig object from file ~/.qliksense/config.yaml
func NewQConfig(qsHome string) *QliksenseConfig {
configFile := filepath.Join(qsHome, "config.yaml")
data, err := ioutil.ReadFile(configFile)
if err != nil {
fmt.Println("Cannot read config file from: "+configFile, err)
os.Exit(1)
}
qc := &QliksenseConfig{}
err = yaml.Unmarshal(data, qc)
qc, err := NewQConfigE(qsHome)
if err != nil {
fmt.Println("yaml unmarshalling error ", err)
os.Exit(1)
@@ -27,20 +34,31 @@ func NewQConfig(qsHome string) *QliksenseConfig {
return qc
}
func NewQConfigE(qsHome string) (*QliksenseConfig, error) {
configFile := filepath.Join(qsHome, "config.yaml")
qc := &QliksenseConfig{}
err := ReadFromFile(qc, configFile)
if err != nil {
return nil, err
}
qc.QliksenseHomePath = qsHome
return qc, nil
}
func NewQConfigEmpty(qsHome string) *QliksenseConfig {
return &QliksenseConfig{
QliksenseHomePath: qsHome,
}
}
// GetCR create a QliksenseCR object for a particular context
// from file ~/.qliksense/contexts/<contx-name>/<contx-name>.yaml
func (qc *QliksenseConfig) GetCR(contextName string) (*QliksenseCR, error) {
crFilePath := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = ctx.CrFile
break
}
}
crFilePath := qc.GetCRFilePath(contextName)
if crFilePath == "" {
return nil, errors.New("context name " + contextName + " not found")
}
return getCRObject(crFilePath)
return qc.GetAndTransformCrObject(crFilePath)
}
// GetCurrentCR create a QliksenseCR object for current context
@@ -49,14 +67,14 @@ func (qc *QliksenseConfig) GetCurrentCR() (*QliksenseCR, error) {
}
// SetCrLocation sets the CR location for a context. Helpful during test
func (qc *QliksenseConfig) SetCrLocation(contextName, filepath string) (*QliksenseConfig, error) {
func (qc *QliksenseConfig) SetCrLocation(contextName, filePath string) (*QliksenseConfig, error) {
tempQc := &QliksenseConfig{}
copier.Copy(tempQc, qc)
found := false
tempQc.Spec.Contexts = []Context{}
for _, c := range qc.Spec.Contexts {
if c.Name == contextName {
c.CrFile = filepath
c.CrFile = filePath
found = true
}
tempQc.Spec.Contexts = append(tempQc.Spec.Contexts, []Context{c}...)
@@ -67,17 +85,488 @@ func (qc *QliksenseConfig) SetCrLocation(contextName, filepath string) (*Qliksen
return nil, errors.New("cannot find the context")
}
func getCRObject(crfile string) (*QliksenseCR, error) {
data, err := ioutil.ReadFile(crfile)
// GetCRObject create a qliksense CR object from file
func GetCRObject(crfile string) (*QliksenseCR, error) {
cr := &QliksenseCR{}
err := ReadFromFile(cr, crfile)
if err != nil {
fmt.Println("Cannot read config file from: "+crfile, err)
fmt.Println("cannot unmarshal cr ", err)
return nil, err
}
return cr, nil
}
func (qc *QliksenseConfig) GetAndTransformCrObject(crfile string) (*QliksenseCR, error) {
cr, err := GetCRObject(crfile)
if err != nil {
return nil, err
}
if cr.Spec.ManifestsRoot != "" && !filepath.IsAbs(cr.Spec.ManifestsRoot) {
cr.Spec.ManifestsRoot = filepath.Join(qc.QliksenseHomePath, cr.Spec.ManifestsRoot)
}
return cr, nil
}
//CreateCRObjectFromString create a QliksenseCR from string content
func CreateCRObjectFromString(crContent string) (*QliksenseCR, error) {
if crContent == "" {
return nil, errors.New("empty string cannot qliksensecr")
}
cr := &QliksenseCR{}
err = yaml.Unmarshal(data, cr)
err := ReadFromStream(cr, strings.NewReader(crContent))
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return nil, err
}
return cr, nil
}
func (qc *QliksenseConfig) GetCRFilePath(contextName string) string {
crFilePath := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = filepath.Join(qc.QliksenseHomePath, ctx.CrFile)
break
}
}
return crFilePath
}
func (cr *QliksenseCR) IsRepoExist() bool {
if cr.Spec.ManifestsRoot == "" {
return false
}
if _, err := os.Lstat(cr.Spec.ManifestsRoot); err != nil {
return false
}
return true
}
func (cr *QliksenseCR) GetFetchUrl() string {
if cr.Spec.FetchSource == nil || cr.Spec.FetchSource.Repository == "" {
return QLIK_GIT_REPO
}
return cr.Spec.FetchSource.Repository
}
func (cr *QliksenseCR) GetFetchAccessToken(encryptionKey string) string {
if cr.Spec.FetchSource == nil {
return ""
}
if tok, err := cr.Spec.FetchSource.GetAccessToken(); err != nil {
fmt.Println(err)
return ""
} else {
by, _ := b64.StdEncoding.DecodeString(tok)
res, err := DecryptData(by, encryptionKey)
if err != nil {
fmt.Println(err)
return ""
}
return string(res)
}
}
func (cr *QliksenseCR) SetFetchUrl(url string) {
if cr.Spec.FetchSource == nil {
cr.Spec.FetchSource = &config.Repo{}
}
cr.Spec.FetchSource.Repository = url
}
func (cr *QliksenseCR) SetFetchAccessToken(token, encryptionKey string) error {
if cr.Spec.FetchSource == nil {
cr.Spec.FetchSource = &config.Repo{}
}
res, err := EncryptData([]byte(token), encryptionKey)
if err != nil {
return err
}
cr.Spec.FetchSource.AccessToken = b64.StdEncoding.EncodeToString(res)
return nil
}
func (cr *QliksenseCR) SetFetchAccessSecretName(sec string) {
if cr.Spec.FetchSource == nil {
cr.Spec.FetchSource = &config.Repo{}
}
cr.Spec.FetchSource.SecretName = sec
}
//DeleteRepo delete the manifest repo and unset manifestsRoot
func (cr *QliksenseCR) DeleteRepo() error {
if err := os.RemoveAll(cr.Spec.ManifestsRoot); err != nil {
return err
}
cr.Spec.ManifestsRoot = ""
return nil
}
func (qc *QliksenseConfig) IsRepoExist(contextName, version string) bool {
if _, err := os.Lstat(qc.BuildRepoPathForContext(contextName, version)); err != nil {
return false
}
return true
}
func (qc *QliksenseConfig) IsRepoExistForCurrent(version string) bool {
if _, err := os.Lstat(qc.BuildRepoPath(version)); err != nil {
return false
}
return true
}
func (qc *QliksenseConfig) DeleteRepoForCurrent(version string) error {
path := qc.BuildRepoPath(version)
return os.RemoveAll(path)
}
func (qc *QliksenseConfig) BuildRepoPath(version string) string {
return qc.BuildRepoPathForContext(qc.Spec.CurrentContext, version)
}
func (qc *QliksenseConfig) BuildRepoPathForContext(contextName, version string) string {
return filepath.Join(qc.GetContextPath(contextName), "qlik-k8s", version)
}
func (qc *QliksenseConfig) BuildCurrentManifestsRoot(version string) string {
return qc.BuildRepoPath(version)
}
func (qc *QliksenseConfig) WriteCR(cr *QliksenseCR) error {
crf := qc.GetCRFilePath(cr.GetName())
if crf == "" {
return errors.New("context name " + cr.GetName() + " not found")
}
return qc.TransformAndWriteCr(cr, crf)
}
//CreateOrWriteCrAndContext create necessary folder structure, update config.yaml and context yaml files
func (qc *QliksenseConfig) CreateOrWriteCrAndContext(cr *QliksenseCR) error {
if qc.QliksenseHomePath == "" {
return errors.New("qliksense home is not set")
}
crf := qc.GetCRFilePath(cr.GetName())
if crf == "" {
// create direcotry structure for context
cDir := filepath.Join(qc.QliksenseHomePath, "contexts", cr.GetName())
if err := os.MkdirAll(cDir, os.ModePerm); err != nil {
return err
}
crf = filepath.Join(cDir, cr.GetName()+".yaml")
ctx := Context{
Name: cr.GetName(),
CrFile: "contexts/" + cr.GetName() + "/" + cr.GetName() + ".yaml", //filepath.Join("contexts", cr.GetName(), cr.GetName()+".yaml"),
}
qc.AddToContexts(ctx)
if err := qc.Write(); err != nil {
return err
}
}
return qc.TransformAndWriteCr(cr, crf)
}
func (qc *QliksenseConfig) TransformAndWriteCr(cr *QliksenseCR, file string) error {
if strings.HasPrefix(cr.Spec.ManifestsRoot, qc.QliksenseHomePath) {
cr.Spec.ManifestsRoot = strings.Replace(cr.Spec.ManifestsRoot, qc.QliksenseHomePath+"/", "", 1)
cr.Spec.ManifestsRoot = strings.Replace(cr.Spec.ManifestsRoot, qc.QliksenseHomePath+"\\", "", 1)
cr.Spec.ManifestsRoot = strings.Replace(cr.Spec.ManifestsRoot, "\\", "/", -1)
}
if err := WriteToFile(cr, file); err != nil {
return err
}
if cr.Spec.ManifestsRoot != "" {
cr.Spec.ManifestsRoot = filepath.Join(qc.QliksenseHomePath, cr.Spec.ManifestsRoot)
}
return nil
}
func (qc *QliksenseConfig) AddToContexts(ctx Context) error {
//TODO: additional duplicate check may be added latter
qc.Spec.Contexts = append(qc.Spec.Contexts, ctx)
return nil
}
func (qc *QliksenseConfig) WriteCurrentContextCR(cr *QliksenseCR) error {
return qc.WriteCR(cr)
}
func (qc *QliksenseConfig) IsContextExist(ctxName string) bool {
for _, ct := range qc.Spec.Contexts {
if ct.Name == ctxName {
return true
}
}
return false
}
func (qc *QliksenseConfig) GetCurrentContextDir() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, qcr.GetObjectMeta().GetName()), nil
}
}
func (qc *QliksenseConfig) GetCurrentContextSecretsDir() (string, error) {
if currentContextDir, err := qc.GetCurrentContextDir(); err != nil {
return "", err
} else {
return filepath.Join(currentContextDir, qliksenseSecretsDirName), nil
}
}
func (qc *QliksenseConfig) setDockerConfigJsonSecret(filename string, dockerConfigJsonSecret *DockerConfigJsonSecret) error {
if secretsDir, err := qc.GetCurrentContextSecretsDir(); err != nil {
return err
} else if encryptionKey, err := qc.GetEncryptionKeyForCurrent(); err != nil {
return err
} else if dockerConfigJsonSecretYaml, err := dockerConfigJsonSecret.ToYaml(encryptionKey); err != nil {
return err
} else if err := os.MkdirAll(secretsDir, os.ModePerm); err != nil {
return err
} else {
return ioutil.WriteFile(filepath.Join(secretsDir, filename), dockerConfigJsonSecretYaml, os.ModePerm)
}
}
func (qc *QliksenseConfig) SetPushDockerConfigJsonSecret(dockerConfigJsonSecret *DockerConfigJsonSecret) error {
return qc.setDockerConfigJsonSecret(pushSecretFileName, dockerConfigJsonSecret)
}
func (qc *QliksenseConfig) SetPullDockerConfigJsonSecret(dockerConfigJsonSecret *DockerConfigJsonSecret) error {
return qc.setDockerConfigJsonSecret(pullSecretFileName, dockerConfigJsonSecret)
}
func (qc *QliksenseConfig) GetPushDockerConfigJsonSecret() (*DockerConfigJsonSecret, error) {
return qc.getDockerConfigJsonSecret(pushSecretFileName)
}
func (qc *QliksenseConfig) GetPullDockerConfigJsonSecret() (*DockerConfigJsonSecret, error) {
return qc.getDockerConfigJsonSecret(pullSecretFileName)
}
func (qc *QliksenseConfig) DeletePushDockerConfigJsonSecret() error {
return qc.deleteDockerConfigJsonSecret(pushSecretFileName)
}
func (qc *QliksenseConfig) DeletePullDockerConfigJsonSecret() error {
return qc.deleteDockerConfigJsonSecret(pullSecretFileName)
}
func (qc *QliksenseConfig) deleteDockerConfigJsonSecret(name string) error {
if secretsDir, err := qc.GetCurrentContextSecretsDir(); err != nil {
return err
} else {
return os.Remove(filepath.Join(secretsDir, name))
}
}
func (qc *QliksenseConfig) getDockerConfigJsonSecret(name string) (*DockerConfigJsonSecret, error) {
dockerConfigJsonSecret := &DockerConfigJsonSecret{}
if secretsDir, err := qc.GetCurrentContextSecretsDir(); err != nil {
return nil, err
} else if dockerConfigJsonSecretYaml, err := ioutil.ReadFile(filepath.Join(secretsDir, name)); err != nil {
return nil, err
} else if encryptionKey, err := qc.GetEncryptionKeyForCurrent(); err != nil {
return nil, err
} else if err := dockerConfigJsonSecret.FromYaml(dockerConfigJsonSecretYaml, encryptionKey); err != nil {
return nil, err
}
return dockerConfigJsonSecret, nil
}
func (qc *QliksenseConfig) getCurrentContextEncryptionKeyPairLocation() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
return qc.getContextEncryptionKeyLocation(qcr.GetName())
}
}
func (qc *QliksenseConfig) getContextEncryptionKeyLocation(contextName string) (string, error) {
// Check env var: QLIKSENSE_KEY_LOCATION to determine location to store keypair
var secretKeyPairLocation string
if os.Getenv("QLIKSENSE_KEY_LOCATION") != "" {
LogDebugMessage("Env variable: QLIKSENSE_KEY_LOCATION= %s", os.Getenv("QLIKSENSE_KEY_LOCATION"))
secretKeyPairLocation = os.Getenv("QLIKSENSE_KEY_LOCATION")
} else {
// QLIKSENSE_KEY_LOCATION has not been set, hence storing key pair in default location:
// /.qliksense/secrets/contexts/<current-context>/secrets/
secretKeyPairLocation = filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, contextName, qliksenseSecretsDirName)
}
return secretKeyPairLocation, os.MkdirAll(secretKeyPairLocation, os.ModePerm)
}
func (qc *QliksenseConfig) GetCurrentContextEjsonKeyDir() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
ejsonKeyDir := filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, qcr.GetObjectMeta().GetName(), qliksenseEjsonDirName)
if err := os.MkdirAll(ejsonKeyDir, os.ModePerm); err != nil {
return "", err
}
return ejsonKeyDir, nil
}
}
func (qc *QliksenseConfig) GetEncryptionKeyForCurrent() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
return qc.GetEncryptionKeyFor(qcr.GetName())
}
}
func (qc *QliksenseConfig) GetEncryptionKeyFor(contextName string) (string, error) {
secretKeyLocation, err := qc.getContextEncryptionKeyLocation(contextName)
if err != nil {
return "", err
}
key, err := LoadSecretKey(secretKeyLocation)
if key != "" {
return key, nil
}
fmt.Println("Generating new encryption key for the context: " + contextName)
return GenerateAndStoreSecretKey(secretKeyLocation)
}
func (cr *QliksenseCR) AddLabelToCr(key, value string) {
m := cr.GetObjectMeta().GetLabels()
if m == nil {
m = make(map[string]string)
}
m[key] = value
cr.GetObjectMeta().SetLabels(m)
}
func (cr *QliksenseCR) GetLabelFromCr(key string) string {
return cr.GetObjectMeta().GetLabels()[key]
}
func (cr *QliksenseCR) GetString() (string, error) {
out, err := K8sToYaml(cr)
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return "", err
}
return string(out), nil
}
func (cr *QliksenseCR) GetK8sSecretsFolder(qlikSenseHomeDir string) string {
return filepath.Join(qlikSenseHomeDir, qliksenseContextsDirName, cr.GetName(), qliksenseSecretsDirName)
}
func (cr *QliksenseCR) IsEULA() bool {
for k, nvs := range cr.Spec.Configs {
if k == "qliksense" {
for _, nv := range nvs {
if nv.Name == "acceptEULA" {
return nv.Value == "yes"
}
}
}
}
return false
}
func (cr *QliksenseCR) SetEULA(value string) {
cr.Spec.AddToConfigs("qliksense", "acceptEULA", value)
}
// GetCustomCrdsPath get crds path if exist in the profile dir
func (cr *QliksenseCR) GetCustomCrdsPath() string {
if cr.Spec.ManifestsRoot == "" || cr.Spec.Profile == "" {
return ""
}
crdsPath := filepath.Join(cr.Spec.GetManifestsRoot(), "manifests", cr.Spec.Profile, "crds")
if _, err := os.Lstat(crdsPath); err != nil {
return ""
}
return crdsPath
}
// GetDecryptedCr it decrypts all the encrypted value and return a new CR
func (qc *QliksenseConfig) GetDecryptedCr(cr *QliksenseCR) (*QliksenseCR, error) {
newCr := &QliksenseCR{}
copier.Copy(newCr, cr)
encryptionKey, err := qc.GetEncryptionKeyFor(cr.GetName())
if err != nil {
return nil, err
}
finalSecrets := map[string]config.NameValues{}
for k, nvs := range newCr.Spec.Secrets {
newNvs := config.NameValues{}
for _, nv := range nvs {
if nv.Value != "" {
b, err := b64.StdEncoding.DecodeString(strings.TrimSpace(nv.Value))
if err != nil {
return nil, err
}
db, err := DecryptData(b, encryptionKey)
if err != nil {
return nil, err
}
newNvs = append(newNvs, config.NameValue{
Name: nv.Name,
Value: string(db),
})
}
}
finalSecrets[k] = newNvs
}
newCr.Spec.Secrets = finalSecrets
if newCr.Spec.FetchSource != nil && newCr.Spec.FetchSource.AccessToken != "" {
decData := cr.GetFetchAccessToken(encryptionKey)
newCr.Spec.FetchSource.AccessToken = decData
}
return newCr, nil
}
//Validate validate CR
func (cr *QliksenseCR) Validate() bool {
return true
}
//CreateContextDirs create context dir structure ~/.qliksense/contexts/contextName
func (qc *QliksenseConfig) CreateContextDirs(contextName string) error {
return os.MkdirAll(qc.GetContextPath(contextName), os.ModePerm)
}
func (qc *QliksenseConfig) GetContextPath(contextName string) string {
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, contextName)
}
//BuildCrFileAbsolutePath build absolute path for a cr ie. ~/.qliksense/contexts/qlik-defautl/qlik-default.yaml
func (qc *QliksenseConfig) BuildCrFileAbsolutePath(contextName string) string {
return filepath.Join(qc.GetContextPath(contextName), contextName+".yaml")
}
//BuildCrFilePath build cr file path i.e. contexts/qlik-default/qlik-default.yaml
func (qc *QliksenseConfig) BuildCrFilePath(contextName string) string {
return filepath.Join(qc.GetContextPath(contextName), contextName+".yaml")
}
//AddToContexts add the context into qc.Spec.Contexts
func (qc *QliksenseConfig) AddToContextsRaw(crName, crFile string) {
qc.Spec.Contexts = append(qc.Spec.Contexts, []Context{
{CrFile: crFile,
Name: crName},
}...)
}
//SetCurrentContextName set the qc.Spec.CurrentContext
func (qc *QliksenseConfig) SetCurrentContextName(name string) {
qc.Spec.CurrentContext = name
}
//Write write QliksenseConfig into config.yaml
func (qc *QliksenseConfig) Write() error {
return WriteToFile(qc, filepath.Join(qc.QliksenseHomePath, "config.yaml"))
}

View File

@@ -1,7 +1,10 @@
package api
import (
b64 "encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
@@ -45,7 +48,6 @@ metadata:
spec:
profile: docker-desktop
manifestsRoot: /Users/mqb/.qliksense/contexts/contx1/qlik-k8s/v0.0.1/manifests
namespace: myqliksense
storageClassName: efs
configs:
qliksense:
@@ -67,7 +69,7 @@ func TestGetCR(t *testing.T) {
// create CR
createCRFile(dir)
crFile := filepath.Join(dir, "contexts", "contx1", "contx1.yaml")
crFile := filepath.Join("contexts", "contx1", "contx1.yaml")
qct, e := qc.SetCrLocation("contx1", crFile)
if e != nil {
t.Fail()
@@ -83,3 +85,88 @@ func TestGetCR(t *testing.T) {
}
td()
}
func TestGetDecryptedCr(t *testing.T) {
td, dir := setup()
qc := NewQConfig(dir)
if qc.Spec.CurrentContext != "contx1" {
t.Fail()
}
// create CR
createCRFile(dir)
crFile := filepath.Join("contexts", "contx1", "contx1.yaml")
qct, e := qc.SetCrLocation("contx1", crFile)
if e != nil {
t.Fail()
t.Log(e)
}
qcr, err := qct.GetCurrentCR()
key, _ := setupGenerateKey(dir)
ecn, _ := EncryptData([]byte("mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"), key)
b := b64.StdEncoding.EncodeToString(ecn)
qcr.Spec.AddToSecrets("qliksense", "mongoDbUri", b, "")
qcr.SetFetchAccessToken("mytoken", key)
newCr, err := qct.GetDecryptedCr(qcr)
if err != nil {
t.Fail()
t.Log(err)
}
decryptedValue := newCr.Spec.GetFromSecrets("qliksense", "mongoDbUri")
orignalValue := qcr.Spec.GetFromSecrets("qliksense", "mongoDbUri")
if decryptedValue != "mongodb://qlik-default-mongodb:27017/qliksense?ssl=false" {
t.Fail()
b, _ := K8sToYaml(newCr)
t.Log(b)
}
if decryptedValue == orignalValue {
t.Fail()
}
if newCr.Spec.FetchSource.AccessToken != "mytoken" {
t.Fail()
}
td()
}
func setupGenerateKey(homeDir string) (string, error) {
secretKeyPairDir := filepath.Join(homeDir, "secrets", "contexts", "contx1", "secrets")
if err := os.MkdirAll(secretKeyPairDir, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
os.Setenv("QLIKSENSE_KEY_LOCATION", secretKeyPairDir)
key, _ := LoadSecretKey(secretKeyPairDir)
if key == "" {
return GenerateAndStoreSecretKey(secretKeyPairDir)
}
return key, nil
}
func Test_set_and_get_fetch_access_token(t *testing.T) {
td, homeDir := setup()
defer td()
createCRFile(homeDir)
crFile := filepath.Join("contexts", "contx1", "contx1.yaml")
qConfig := NewQConfig(homeDir)
newQ, _ := qConfig.SetCrLocation("contx1", crFile)
newQ.Write()
qConfig = NewQConfig(homeDir)
qcr, _ := qConfig.GetCurrentCR()
key, _ := qConfig.GetEncryptionKeyFor(qcr.GetName())
if err := qcr.SetFetchAccessToken("mytokenbeforeencryption", key); err != nil {
t.Log(err)
t.FailNow()
}
tok := qcr.GetFetchAccessToken(key)
if tok != "mytokenbeforeencryption" {
t.Log("Expected: mytokenbeforeencryption, got: " + tok)
t.Fail()
}
}

127
pkg/api/context_apis.go Normal file
View File

@@ -0,0 +1,127 @@
package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"github.com/qlik-oss/k-apis/pkg/config"
"k8s.io/apimachinery/pkg/runtime/schema"
machine_yaml "k8s.io/apimachinery/pkg/util/yaml"
)
const (
QliksenseConfigApiVersion = "v1"
QliksenseConfigApiGroup = "config.qlik.com"
QliksenseConfigKind = "QliksenseConfig"
QliksenseApiVersion = "v1"
QliksenseKind = "Qliksense"
QliksenseGroup = "qlik.com"
QliksenseDefaultProfile = "docker-desktop"
DefaultRotateKeys = "yes"
QliksenseMetadataName = "QliksenseConfigMetadata"
DefaultMongoDbUri = "mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"
DefaultMongoDbUriKey = "mongoDbUri"
)
// AddCommonConfig adds common configs into CRs
func (qliksenseCR *QliksenseCR) AddCommonConfig(contextName string) {
qliksenseCR.SetGroupVersionKind(schema.GroupVersionKind{
Group: QliksenseGroup,
Kind: QliksenseKind,
Version: QliksenseApiVersion,
})
qliksenseCR.SetName(contextName)
qliksenseCR.Spec = &config.CRSpec{
Profile: QliksenseDefaultProfile,
RotateKeys: DefaultRotateKeys,
}
qliksenseCR.Spec.AddToSecrets("qliksense", DefaultMongoDbUriKey, DefaultMongoDbUri, "")
}
// AddBaseQliksenseConfigs adds configs into config.yaml
func (qliksenseConfig *QliksenseConfig) AddBaseQliksenseConfigs(defaultQliksenseContext string) {
qliksenseConfig.SetGroupVersionKind(schema.GroupVersionKind{
Group: QliksenseConfigApiGroup,
Kind: QliksenseConfigKind,
Version: QliksenseConfigApiVersion,
})
qliksenseConfig.SetName(QliksenseMetadataName)
if defaultQliksenseContext != "" {
if qliksenseConfig.Spec == nil {
qliksenseConfig.Spec = &ContextSpec{}
}
qliksenseConfig.Spec.CurrentContext = defaultQliksenseContext
}
}
func (qliksenseConfig *QliksenseConfig) SwitchCurrentCRToVersionAndProfile(version, profile string) error {
if qcr, err := qliksenseConfig.GetCurrentCR(); err != nil {
return err
} else {
versionManifestRoot := qliksenseConfig.BuildCurrentManifestsRoot(version)
if (qcr.Spec.ManifestsRoot != versionManifestRoot) || (profile != "" && qcr.Spec.Profile != profile) || (qcr.GetLabelFromCr("version") != version) {
qcr.Spec.ManifestsRoot = versionManifestRoot
if profile != "" {
qcr.Spec.Profile = profile
}
qcr.AddLabelToCr("version", version)
if err := qliksenseConfig.WriteCurrentContextCR(qcr); err != nil {
return err
}
}
}
return nil
}
// WriteToFile (content, targetFile) writes content into specified file
func WriteToFile(content interface{}, targetFile string) error {
if content == nil || targetFile == "" {
return nil
}
x, err := K8sToYaml(content)
if err != nil {
err = fmt.Errorf("An error occurred during marshalling CR: %v", err)
log.Println(err)
return err
}
// Writing content
err = ioutil.WriteFile(targetFile, x, 0644)
if err != nil {
log.Println(err)
return err
}
LogDebugMessage("Wrote content into %s", targetFile)
return nil
}
// ReadFromFile (content, targetFile) reads content from specified sourcefile
func ReadFromFile(content interface{}, sourceFile string) error {
if content == nil || sourceFile == "" {
return nil
}
file, e := os.Open(sourceFile)
if e != nil {
return e
}
defer file.Close()
return ReadFromStream(content, file)
}
// ReadFromStream reads from input stream and creat yaml struct of type content
func ReadFromStream(content interface{}, reader io.Reader) error {
contents, err := ioutil.ReadAll(reader)
if err != nil {
err = fmt.Errorf("There was an error reading from reader: %v", err)
return err
}
// reading k8s style object
// https://stackoverflow.com/questions/44306554/how-to-deserialize-kubernetes-yaml-file
dec := machine_yaml.NewYAMLOrJSONDecoder(bytes.NewReader(contents), 10000)
return dec.Decode(content)
}

View File

@@ -0,0 +1,103 @@
package api
import (
"reflect"
"testing"
"github.com/qlik-oss/k-apis/pkg/config"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
testDir = "./tests"
)
func TestAddCommonConfig(t *testing.T) {
gvk := schema.GroupVersionKind{
Group: QliksenseGroup,
Kind: QliksenseKind,
Version: QliksenseApiVersion,
}
q := &QliksenseCR{}
q.SetName("myqliksense")
q.SetGroupVersionKind(gvk)
q.Spec = &config.CRSpec{
Profile: QliksenseDefaultProfile,
RotateKeys: DefaultRotateKeys,
Secrets: map[string]config.NameValues{
"qliksense": []config.NameValue{{
Name: DefaultMongoDbUriKey,
Value: DefaultMongoDbUri,
},
},
},
}
type args struct {
qliksenseCR *QliksenseCR
contextName string
}
tests := []struct {
name string
args args
want *QliksenseCR
}{
{
name: "valid case",
args: args{
qliksenseCR: &QliksenseCR{},
contextName: "myqliksense",
},
want: q,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.qliksenseCR.AddCommonConfig(tt.args.contextName)
if !reflect.DeepEqual(tt.args.qliksenseCR, tt.want) {
t.Errorf("AddCommonConfig() = %+v, want %+v", tt.args.qliksenseCR, tt.want)
}
})
}
}
func TestAddBaseQliksenseConfigs(t *testing.T) {
gvk := schema.GroupVersionKind{
Group: QliksenseConfigApiGroup,
Kind: QliksenseConfigKind,
Version: QliksenseConfigApiVersion,
}
qc := &QliksenseConfig{}
qc.SetGroupVersionKind(gvk)
qc.SetName(QliksenseMetadataName)
qc.Spec = &ContextSpec{
CurrentContext: "qlik-default",
}
type args struct {
qliksenseConfig *QliksenseConfig
defaultQliksenseContext string
}
tests := []struct {
name string
args args
want *QliksenseConfig
}{
{
name: "valid case",
args: args{
qliksenseConfig: &QliksenseConfig{},
defaultQliksenseContext: "qlik-default",
},
want: qc,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.qliksenseConfig.AddBaseQliksenseConfigs(tt.args.defaultQliksenseContext)
if !reflect.DeepEqual(tt.args.qliksenseConfig, tt.want) {
t.Errorf("AddBaseQliksenseConfigs() = %+v, want %+v", tt.args.qliksenseConfig, tt.want)
}
})
}
}

8
pkg/api/copy.go Normal file
View File

@@ -0,0 +1,8 @@
package api
import "github.com/otiai10/copy"
//copy source directory to destination
func CopyDirectory(source string, dest string) error {
return copy.Copy(source, dest)
}

101
pkg/api/copy_test.go Normal file
View File

@@ -0,0 +1,101 @@
package api
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/types"
)
func TestCopyDirectory(t *testing.T) {
src, _ := ioutil.TempDir("", "")
f1, _ := ioutil.TempFile(src, "")
ioutil.TempFile(src, "")
dest, _ := ioutil.TempDir("", "")
CopyDirectory(src, dest)
if _, err := os.Lstat(filepath.Join(dest, filepath.Base(f1.Name()))); err != nil {
t.Log(err)
t.Fail()
}
}
func TestCopyDirectory_withGit_withKuz(t *testing.T) {
if testing.Short() {
t.Skip("Skipping in short test mode")
}
tmpDir1, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpDir1)
tmpDir2, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpDir2)
repoPath1 := path.Join(tmpDir1, "repo")
repo1, err := kapis_git.CloneRepository(repoPath1, "https://github.com/qlik-oss/qliksense-k8s", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := CopyDirectory(repoPath1, tmpDir2); err != nil {
t.Fatalf("unexpected error: %v", err)
}
repoPath2 := tmpDir2
repo2, err := kapis_git.OpenRepository(repoPath2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := kapis_git.Checkout(repo2, "v0.0.2", "", nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
repo2Manifest, err := kuz(path.Join(repoPath2, "manifests", "docker-desktop"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := kapis_git.Checkout(repo1, "v0.0.2", "", nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
repo1Manifest, err := kuz(path.Join(repoPath1, "manifests", "docker-desktop"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(repo2Manifest) != string(repo1Manifest) {
t.Logf("manifest generated on the original config:\n%v", string(repo1Manifest))
t.Logf("manifest generated on the copied config:\n%v", string(repo2Manifest))
t.Fatal("expected manifests to be equal, but they were not")
}
}
func kuz(directory string) ([]byte, error) {
options := &krusty.Options{
DoLegacyResourceSort: false,
LoadRestrictions: types.LoadRestrictionsNone,
DoPrune: false,
PluginConfig: konfig.DisabledPluginConfig(),
}
k := krusty.MakeKustomizer(filesys.MakeFsOnDisk(), options)
resMap, err := k.Run(directory)
if err != nil {
return nil, err
}
return resMap.AsYaml()
}

View File

@@ -0,0 +1,103 @@
package api
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type k8sDockerConfigJsonMapType struct {
Auths map[string]k8sDockerConfigJsonType `json:"auths"`
}
type k8sDockerConfigJsonType struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email,omitempty"`
Auth string `json:"auth"`
}
func (kdcjt *k8sDockerConfigJsonType) GenerateAuth() {
kdcjt.Auth = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", kdcjt.Username, kdcjt.Password)))
}
type DockerConfigJsonSecret struct {
Name string
Uri string
Username string
Password string
Email string
}
func (d *DockerConfigJsonSecret) ToYaml(encryptionKey string) ([]byte, error) {
k8sDockerConfigJson := k8sDockerConfigJsonType{
Username: d.Username,
Password: d.Password,
Email: d.Email,
}
k8sDockerConfigJson.GenerateAuth()
k8sDockerConfigJsonMap := k8sDockerConfigJsonMapType{
Auths: map[string]k8sDockerConfigJsonType{
d.Uri: k8sDockerConfigJson,
},
}
k8sDockerConfigJsonMapBytes, err := json.Marshal(k8sDockerConfigJsonMap)
if err != nil {
return nil, err
}
var k8sDockerConfigJsonMapMaybeEncryptedBytes []byte
if encryptionKey != "" {
if k8sDockerConfigJsonMapMaybeEncryptedBytes, err = EncryptData(k8sDockerConfigJsonMapBytes, encryptionKey); err != nil {
return nil, err
}
} else {
k8sDockerConfigJsonMapMaybeEncryptedBytes = k8sDockerConfigJsonMapBytes
}
k8sSecret := v1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: d.Name,
},
Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
".dockerconfigjson": k8sDockerConfigJsonMapMaybeEncryptedBytes,
},
}
return K8sSecretToYaml(k8sSecret)
}
func (d *DockerConfigJsonSecret) FromYaml(secretBytes []byte, decryptionKey string) error {
k8sDockerConfigJsonMap := k8sDockerConfigJsonMapType{}
if k8sSecret, err := K8sSecretFromYaml(secretBytes); err != nil {
return err
} else if k8sSecret.TypeMeta.Kind != "Secret" {
return errors.New("not a Secret kind")
} else if k8sSecret.Type != v1.SecretTypeDockerConfigJson {
return errors.New("not a kubernetes.io/dockerconfigjson type")
} else if k8sDockerConfigJsonMapEncryptedBytes, ok := k8sSecret.Data[".dockerconfigjson"]; !ok {
return errors.New("secret data is missing a value for the .dockerconfigjson key")
} else if k8sDockerConfigJsonMapBytes, err := DecryptData(k8sDockerConfigJsonMapEncryptedBytes, decryptionKey); err != nil {
return errors.New("secret data is missing a value for the .dockerconfigjson key")
} else if err := json.Unmarshal(k8sDockerConfigJsonMapBytes, &k8sDockerConfigJsonMap); err != nil {
return err
} else {
d.Name = k8sSecret.ObjectMeta.Name
for registry, k8sDockerConfigJson := range k8sDockerConfigJsonMap.Auths {
d.Uri = registry
d.Username = k8sDockerConfigJson.Username
d.Password = k8sDockerConfigJson.Password
d.Email = k8sDockerConfigJson.Email
break
}
return nil
}
}

View File

@@ -0,0 +1,69 @@
package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"testing"
"gopkg.in/yaml.v3"
)
func TestDockerConfigJsonSecret(t *testing.T) {
dockerConfigJsonSecret := DockerConfigJsonSecret{
Name: "some-name",
Uri: "some-uri",
Username: "some-username",
Password: "some-password",
Email: "some-email",
}
dockerConfigJsonSecretFromYaml := DockerConfigJsonSecret{}
validYamlMap := map[string]interface{}{}
encryptionKey, err := GenerateKey()
if err != nil {
t.Fatalf("error generating RSA private key: %v\n", err)
}
dockerConfigJsonSecretYamlBytes, err := dockerConfigJsonSecret.ToYaml(encryptionKey)
dockerConfigJsonMap := map[string]interface{}{}
if err != nil {
t.Fatalf("error converting secret to yaml: %v", err)
} else if err := yaml.Unmarshal(dockerConfigJsonSecretYamlBytes, &validYamlMap); err != nil {
t.Fatalf("error unmarshalling yaml string: %v, error: %v", string(dockerConfigJsonSecretYamlBytes), err)
} else if validYamlMap["apiVersion"] != "v1" ||
validYamlMap["kind"] != "Secret" ||
validYamlMap["metadata"].(map[string]interface{})["name"] != dockerConfigJsonSecret.Name ||
validYamlMap["type"] != "kubernetes.io/dockerconfigjson" {
t.Fatalf("error verifying validity of secret yaml: %v", string(dockerConfigJsonSecretYamlBytes))
} else if dockerConfigJsonBytesBase64, ok := validYamlMap["data"].(map[string]interface{})[".dockerconfigjson"]; !ok {
t.Fatalf("no .dockerconfigjson data key in the secret yaml: %v", string(dockerConfigJsonSecretYamlBytes))
} else if dockerConfigJsonEncryptedBytes, err := base64.StdEncoding.DecodeString(dockerConfigJsonBytesBase64.(string)); err != nil {
t.Fatalf("error decoding dockerConfigJsonBytes from base64: %v", err)
} else if dockerConfigJsonBytes, err := DecryptData(dockerConfigJsonEncryptedBytes, encryptionKey); err != nil {
t.Fatalf("error decrypting dockerConfigJsonBytes: %v", err)
} else if err := json.Unmarshal(dockerConfigJsonBytes, &dockerConfigJsonMap); err != nil {
t.Fatalf("error unmarshalling dockerConfigJson from json: %v", err)
} else if dockerConfigJson, ok := dockerConfigJsonMap["auths"].(map[string]interface{})[dockerConfigJsonSecret.Uri]; !ok {
t.Fatalf("dockerConfigJson map does not contain data for the registry: %v", dockerConfigJsonSecret.Uri)
} else if dockerConfigJson.(map[string]interface{})["username"] != dockerConfigJsonSecret.Username ||
dockerConfigJson.(map[string]interface{})["password"] != dockerConfigJsonSecret.Password ||
dockerConfigJson.(map[string]interface{})["email"] != dockerConfigJsonSecret.Email {
t.Fatal("dockerConfigJson map does not contain expected values")
} else {
authBase64 := dockerConfigJson.(map[string]interface{})["auth"]
if auth, err := base64.StdEncoding.DecodeString(authBase64.(string)); err != nil {
t.Fatal("error base64 decoding auth value")
} else if string(auth) != fmt.Sprintf("%s:%s", dockerConfigJsonSecret.Username, dockerConfigJsonSecret.Password) {
t.Fatal("auth value was not what we expected")
}
}
t.Logf("dockerConfigJsonSecretYaml: \n%v\n", string(dockerConfigJsonSecretYamlBytes))
if err := dockerConfigJsonSecretFromYaml.FromYaml(dockerConfigJsonSecretYamlBytes, encryptionKey); err != nil {
t.Fatalf("error reading secret in from yaml: %v", err)
} else if !reflect.DeepEqual(dockerConfigJsonSecret, dockerConfigJsonSecretFromYaml) {
t.Fatalf("secret: %v does not equal secret: %v", dockerConfigJsonSecret, dockerConfigJsonSecretFromYaml)
}
}

102
pkg/api/encryption.go Normal file
View File

@@ -0,0 +1,102 @@
package api
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"path/filepath"
)
const (
key_file_name = "user_secret_key"
)
// GenerateAndStoreSecretKey generates and stores key
func GenerateAndStoreSecretKey(secretsDir string) (string, error) {
// creating contexts/qlik-default/secrets/user_secret_key
keyFile := filepath.Join(secretsDir, key_file_name)
key, err := GenerateKey()
if err != nil {
return "", err
}
if err := writeContentToFile([]byte(key), keyFile); err != nil {
return "", err
}
return key, nil
}
func LoadSecretKey(secretsDir string) (string, error) {
keyFile := filepath.Join(secretsDir, key_file_name)
by, err := ioutil.ReadFile(keyFile)
if err != nil {
return "", err
}
return string(by), nil
}
// writeContentToFile writes keys to a file
func writeContentToFile(keyData []byte, fileName string) error {
err := ioutil.WriteFile(fileName, keyData, 0600)
if err != nil {
log.Printf("error writing to file (%s): %v", fileName, err)
return err
}
return nil
}
func GenerateKey() (string, error) {
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return "", err
}
s := fmt.Sprintf("%x", salt)
return s, nil
}
func EncryptData(plaintext []byte, userKey string) ([]byte, error) {
key, _ := hex.DecodeString(userKey)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return aesgcm.Seal(nonce, nonce, plaintext, nil), nil
}
func DecryptData(ciphertext []byte, userKey string) ([]byte, error) {
key, _ := hex.DecodeString(userKey)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := aesgcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}

View File

@@ -0,0 +1,29 @@
package api
import (
"testing"
)
func Test_encrypt_decrypt(t *testing.T) {
key, err := GenerateKey()
if err != nil {
t.Log(err)
t.FailNow()
}
testData := "this is a secret value"
enc, err := EncryptData([]byte(testData), key)
if err != nil {
t.Log(err)
t.FailNow()
}
dec, err := DecryptData(enc, key)
if err != nil {
t.Log(err)
t.FailNow()
}
if testData != string(dec) {
t.Log("expected: " + testData)
t.Log("actual: " + string(dec))
t.Fail()
}
}

View File

@@ -0,0 +1,30 @@
package api
import (
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
func K8sSecretToYaml(k8sSecret v1.Secret) ([]byte, error) {
return K8sToYaml(k8sSecret)
}
func K8sSecretFromYaml(k8sSecretBytes []byte) (v1.Secret, error) {
k8sSecret := v1.Secret{}
if err := yaml.UnmarshalStrict(k8sSecretBytes, &k8sSecret); err != nil {
return k8sSecret, err
}
return k8sSecret, nil
}
func K8sToYaml(k8sObj interface{}) ([]byte, error) {
k8sSecretYamlMap := map[string]interface{}{}
if k8sSecretYamlBytes, err := yaml.Marshal(k8sObj); err != nil {
return nil, err
} else if err := yaml.Unmarshal(k8sSecretYamlBytes, &k8sSecretYamlMap); err != nil {
return nil, err
} else {
delete(k8sSecretYamlMap["metadata"].(map[string]interface{}), "creationTimestamp")
return yaml.Marshal(k8sSecretYamlMap)
}
}

View File

@@ -1,13 +1,78 @@
package api
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
)
func KubectlApply(manifests string) error {
// KubectlApply create resoruces in the provided namespace,
// if namespace="" then use whatever the kubectl default is
func KubectlApply(manifests, namespace string) error {
return kubectlOperation(manifests, "apply", namespace)
}
func KubectlApplyVerbose(manifests, namespace string, verbose bool) error {
return kubectlOperationVerbose(manifests, "apply", namespace, verbose)
}
// KubectlDelete delete resoruces in the provided namespace,
// if namespace="" then use whatever the kubectl default is
func KubectlDelete(manifests, namespace string) error {
return kubectlOperation(manifests, "delete", namespace)
}
func KubectlDeleteVerbose(manifests, namespace string, verbose bool) error {
return kubectlOperationVerbose(manifests, "delete", namespace, verbose)
}
func GetKubectlNamespace() string {
namespace := ""
cmd := exec.Command("kubectl", "config", "current-context")
var out, out2 bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("kubectl config current-context %q\n", err)
return namespace
}
if out.String() == "" {
fmt.Println("kubectl config current-context does not return anything")
return namespace
}
cmd = exec.Command("kubectl", "config", "view", "-o", `jsonpath={.contexts[?(@.name == "`+strings.TrimSpace(out.String())+`")].context.namespace}`)
cmd.Stdout = &out2
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Printf("kubectl config view failed with %q\n", err)
return namespace
}
namespace = out2.String()
return namespace
}
func SetKubectlNamespace(ns string) {
cmd := exec.Command("kubectl", "config", "set-context", "--namespace="+ns, "--current")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("kubectl config set-context --namespace failed with %q\n", err)
}
}
func kubectlOperation(manifests string, oprName string, namespace string) error {
return kubectlOperationVerbose(manifests, oprName, namespace, true)
}
func kubectlOperationVerbose(manifests string, oprName string, namespace string, verbose bool) error {
tempYaml, err := ioutil.TempFile("", "")
if err != nil {
fmt.Println("cannot create file ", err)
@@ -15,15 +80,55 @@ func KubectlApply(manifests string) error {
}
tempYaml.WriteString(manifests)
cmd := exec.Command("kubectl", "apply", "-f", tempYaml.Name(), "--validate=false")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
arguments := make([]string, 0)
arguments = append(arguments, oprName)
arguments = append(arguments, "-f")
arguments = append(arguments, tempYaml.Name())
if oprName == "apply" {
arguments = append(arguments, "--validate=false")
}
if namespace != "" {
arguments = append(arguments, "-n")
arguments = append(arguments, namespace)
}
var cmd *exec.Cmd
if oprName == "apply" {
cmd = exec.Command("kubectl", arguments...)
} else {
cmd = exec.Command("kubectl", arguments...)
}
sterrBuffer := &bytes.Buffer{}
stoutBuffer := &bytes.Buffer{}
cmd.Stdout = stoutBuffer
cmd.Stderr = sterrBuffer
err = cmd.Run()
if err != nil {
fmt.Printf("kubectl apply failed with %s\n", err)
fmt.Println("temp CRD file: " + tempYaml.Name())
return err
return fmt.Errorf("kubectl %v failed with: %v, %v, temp k8s yaml file:%v\n", oprName, err, sterrBuffer.String(), tempYaml.Name())
}
if verbose {
fmt.Println(stoutBuffer.String())
}
os.Remove(tempYaml.Name())
return nil
}
func KubectlDirectOps(opr []string, namespace string) (string, error) {
arguments := []string{}
if namespace != "" {
arguments = append(arguments, "-n", namespace)
}
arguments = append(arguments, opr...)
var out bytes.Buffer
cmd := exec.Command("kubectl", arguments...)
LogDebugMessage("Kubectl command: %s %v\n", "kubectl", arguments)
sterrBuffer := &bytes.Buffer{}
cmd.Stderr = sterrBuffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("kubectl %v failed with: %v, %v\n", opr, err, sterrBuffer.String())
}
s := out.String()
return s, nil
}

32
pkg/api/kubectl_test.go Normal file
View File

@@ -0,0 +1,32 @@
package api
import (
"fmt"
"strings"
"testing"
)
func TestGetKubectlNamespace(t *testing.T) {
t.Skip()
ns := GetKubectlNamespace()
SetKubectlNamespace("tada")
got := GetKubectlNamespace()
if got != "tada" {
t.Log(got)
t.Fail()
}
SetKubectlNamespace(ns)
}
func TestKubectlDirectOps(t *testing.T) {
t.Skip()
SetKubectlNamespace("test")
ns := GetKubectlNamespace()
opr := fmt.Sprintf("version")
opr1 := strings.Fields(opr)
_, err := KubectlDirectOps(opr1, ns)
if err != nil {
t.Log(err)
t.Fail()
}
}

124
pkg/api/preflight_apis.go Normal file
View File

@@ -0,0 +1,124 @@
package api
import (
"os"
"path"
"path/filepath"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PreflightConfig struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Spec *PreflightSpec `json:"spec" yaml:"spec"`
QliksenseHomePath string `json:"-" yaml:"-"`
}
type PreflightSpec struct {
MinK8sVersion string `json:"minK8sVersion,omitempty" yaml:"minK8sVersion,omitempty"`
Images map[string]string `json:"images,omitempty" yaml:"images,omitempty"`
}
//NewPreflightConfigEmpty create empty PreflightConfig object
func NewPreflightConfigEmpty(qHome string) *PreflightConfig {
p := &PreflightConfig{
QliksenseHomePath: qHome,
TypeMeta: metav1.TypeMeta{
APIVersion: "config.qlik.com/v1",
Kind: "PreflightConfig",
},
ObjectMeta: metav1.ObjectMeta{
Name: "PreflightConfigMetadata",
},
Spec: &PreflightSpec{},
}
return p
}
//NewPreflightConfig create empty PreflightConfig object if preflit/preflight-config.yaml not exist
func NewPreflightConfig(qHome string) *PreflightConfig {
p := NewPreflightConfigEmpty(qHome)
conFile := p.GetConfigFilePath()
if _, err := os.Lstat(conFile); err != nil {
return p
}
p = &PreflightConfig{
QliksenseHomePath: qHome,
}
if err := ReadFromFile(p, conFile); err != nil {
return nil
}
return p
}
//GetConfigFilePath return preflight-config.yaml file path
func (p *PreflightConfig) GetConfigFilePath() string {
return filepath.Join(p.QliksenseHomePath, "preflight", "preflight-config.yaml")
}
//Write write PreflightConfig object into the ~/.qliksense/preflight/preflight-config.yaml file
func (p *PreflightConfig) Write() error {
pDir := filepath.Join(p.QliksenseHomePath, "preflight")
if err := os.MkdirAll(pDir, os.ModePerm); err != nil {
return err
}
return WriteToFile(p, p.GetConfigFilePath())
}
func (p *PreflightConfig) AddMinK8sV(version string) {
if p.Spec == nil {
p.Spec = &PreflightSpec{}
}
p.Spec.MinK8sVersion = version
}
func (p *PreflightConfig) AddImage(imageFor, imageName string) {
if p.Spec.Images == nil {
p.Spec.Images = make(map[string]string)
}
p.Spec.Images[imageFor] = imageName
}
func (p *PreflightConfig) GetImageName(imageFor string, accountForImageRegistry bool) (string, error) {
if p.Spec.Images == nil {
return "", nil
}
image := p.Spec.Images[imageFor]
if accountForImageRegistry {
qConfig := NewQConfig(p.QliksenseHomePath)
if currentCR, err := qConfig.GetCurrentCR(); err != nil {
return "", err
} else if imageRegistry := currentCR.Spec.GetImageRegistry(); imageRegistry != "" {
imageSegments := strings.Split(image, "/")
imageNameAndTag := imageSegments[len(imageSegments)-1]
return path.Join(imageRegistry, imageNameAndTag), nil
}
}
return image, nil
}
func (p *PreflightConfig) GetMinK8sVersion() string {
return p.Spec.MinK8sVersion
}
func (p *PreflightConfig) IsExistOnDisk() bool {
if _, err := os.Lstat(p.GetConfigFilePath()); err != nil {
return false
}
return true
}
func (p *PreflightConfig) GetImageMap() map[string]string {
return p.Spec.Images
}
func (p *PreflightConfig) Initialize() error {
if p.IsExistOnDisk() {
return nil
}
p.AddMinK8sV("1.15")
p.AddImage("nginx", "nginx")
p.AddImage("netcat", "subfuzion/netcat")
p.AddImage("mongo", "mongo")
return p.Write()
}

View File

@@ -0,0 +1,138 @@
package api
import (
"fmt"
"io/ioutil"
"os"
"path"
"testing"
)
func Test_Initalize(t *testing.T) {
testCases := []struct {
name string
validate func(t *testing.T, tempDir string)
}{
{
name: "without account for imageRegistry",
validate: func(t *testing.T, tempDir string) {
preflightConfig := NewPreflightConfig(tempDir)
imageName, err := preflightConfig.GetImageName("test", false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if imageName != "testimage" {
t.Fatalf("expected image name: testimage, got: %v", imageName)
}
},
},
{
name: "with account for configured imageRegistry",
validate: func(t *testing.T, tempDir string) {
registry := "registryFoo"
setupQliksenseTestDefaultContext(t, tempDir, fmt.Sprintf(`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
configs:
qliksense:
- name: imageRegistry
value: %v
`, registry))
preflightConfig := NewPreflightConfig(tempDir)
imageName, err := preflightConfig.GetImageName("test", true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedImageName := fmt.Sprintf("%v/testimage", registry)
if imageName != expectedImageName {
t.Fatalf("expected image name: %v, got: %v", expectedImageName, imageName)
}
},
},
{
name: "with account for un-configured imageRegistry",
validate: func(t *testing.T, tempDir string) {
setupQliksenseTestDefaultContext(t, tempDir, `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
configs:
qliksense:
- name: something
value: other
`)
preflightConfig := NewPreflightConfig(tempDir)
imageName, err := preflightConfig.GetImageName("test", true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedImageName := "testimage"
if imageName != expectedImageName {
t.Fatalf("expected image name: %v, got: %v", expectedImageName, imageName)
}
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
setupPreflightConfig(t, tempDir)
testCase.validate(t, tempDir)
})
}
}
func setupPreflightConfig(t *testing.T, tempDir string) {
pf := NewPreflightConfig(tempDir)
if err := pf.Initialize(); err != nil {
t.Fatal(err)
}
p := &PreflightConfig{
QliksenseHomePath: tempDir,
}
if err := ReadFromFile(p, pf.GetConfigFilePath()); err != nil {
t.Fatal(err)
}
if p.GetMinK8sVersion() != "1.15" {
t.Fatalf("expected k8 version: 1.15, but got " + p.GetMinK8sVersion())
}
p.AddImage("test", "testimage")
if err := p.Write(); err != nil {
t.Fatal(err)
}
}
func setupQliksenseTestDefaultContext(t *testing.T, tmpQlikSenseHome, CR string) {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`), os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
defaultContextDir := path.Join(tmpQlikSenseHome, "contexts", "qlik-default")
if err := os.MkdirAll(defaultContextDir, os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := ioutil.WriteFile(path.Join(defaultContextDir, "qlik-default.yaml"), []byte(CR), os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -1,24 +1,26 @@
package api
import "github.com/qlik-oss/k-apis/pkg/config"
// CommonConfig is exported
type CommonConfig struct {
ApiVersion string `json:"apiVersion" yaml:"apiVersion"`
Kind string `json:"kind" yaml:"kind"`
Metadata *Metadata `json:"metadata" yaml:"metadata"`
}
import (
kapi_config "github.com/qlik-oss/k-apis/pkg/config"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// QliksenseConfig is exported
type QliksenseConfig struct {
CommonConfig `json:",inline" yaml:",inline"`
Spec *ContextSpec `json:"spec" yaml:"spec"`
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Spec *ContextSpec `json:"spec" yaml:"spec"`
QliksenseHomePath string `json:"-" yaml:"-"`
}
/*type CommonConfig struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
*/
// QliksenseCR is exported
type QliksenseCR struct {
CommonConfig `json:",inline" yaml:",inline"`
Spec *config.CRSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
kapi_config.KApiCr `json:",inline" yaml:",inline"`
}
// ContextSpec is exported
@@ -38,3 +40,10 @@ type Metadata struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}
// ServiceKeyValue holds the combination of service, key and value
type ServiceKeyValue struct {
SvcName string
Key string
Value string
}

278
pkg/api/utils.go Normal file
View File

@@ -0,0 +1,278 @@
package api
import (
"archive/tar"
"archive/zip"
"compress/gzip"
b64 "encoding/base64"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/pkg/errors"
)
func checkExists(filename string) os.FileInfo {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return nil
}
LogDebugMessage("File exists")
return info
}
// FileExists checks if a file exists
func FileExists(filename string) bool {
if fe := checkExists(filename); fe != nil && !fe.IsDir() {
return true
}
return false
}
// DirExists checks if a directory exists
func DirExists(dirname string) bool {
if fe := checkExists(dirname); fe != nil && fe.IsDir() {
return true
}
return false
}
// LogDebugMessage logs a debug message
func LogDebugMessage(strMessage string, args ...interface{}) {
if os.Getenv("QLIKSENSE_DEBUG") == "true" {
fmt.Printf(strMessage, args...)
}
}
// ReadKeys reads key file from disk
func ReadKeys(keyFile string) ([]byte, error) {
keybyteArray, err := ioutil.ReadFile(keyFile)
if err != nil {
err = fmt.Errorf("There was an error reading from file: %s, %v", keyFile, err)
log.Println(err)
return nil, err
}
return keybyteArray, nil
}
// ProcessConfigArgs processes args and returns an service, key, value slice
func ProcessConfigArgs(args []string, base64Encoded bool) ([]*ServiceKeyValue, error) {
// prepare received args
// split args[0] into key and value
if len(args) == 0 {
err := fmt.Errorf("No args were provided. Please provide args to configure the current context")
return nil, err
}
notValidErr := fmt.Errorf("Please provide valid args for this command")
resultSvcKV := make([]*ServiceKeyValue, len(args))
// qliksense.mongodb=somethig
for i, arg := range args {
LogDebugMessage("Arg received: %s", arg)
first := strings.SplitN(arg, "=", 2)
if len(first) != 2 {
return nil, notValidErr
}
second := strings.SplitN(first[0], ".", 2)
if len(second) != 2 {
return nil, notValidErr
}
resultValue := strings.Trim(first[1], "\"")
if base64Encoded {
if decodeByte, err := b64.StdEncoding.DecodeString(resultValue); err != nil {
return nil, err
} else {
resultValue = strings.Trim(string(decodeByte), "\n ")
}
}
resultSvcKV[i] = &ServiceKeyValue{
SvcName: second[0],
Key: second[1],
Value: resultValue,
}
}
return resultSvcKV, nil
}
func ExecuteTaskWithBlinkingStdoutFeedback(task func() (interface{}, error), feedback string) (result interface{}, err error) {
taskDone := make(chan bool)
go func() {
result, err = task()
taskDone <- true
}()
progressOnTicker := time.NewTicker(500 * time.Millisecond)
progressOffTicker := time.NewTicker(1000 * time.Millisecond)
printProgress := func(on bool) {
if on {
fmt.Printf("%s\r", feedback)
} else {
fmt.Printf("%s\r", strings.Repeat(" ", len(feedback)))
}
}
for {
select {
case <-taskDone:
progressOnTicker.Stop()
progressOffTicker.Stop()
printProgress(false)
return result, err
case <-progressOnTicker.C:
printProgress(true)
case <-progressOffTicker.C:
printProgress(false)
}
}
}
func DownloadFile(url, baseFolder, installerName string) error {
var (
out *os.File
err error
resp *http.Response
)
// Create the file
fileName := filepath.Join(baseFolder, installerName)
LogDebugMessage("Installer Filename: %s\n", fileName)
if out, err = os.Create(fileName); err != nil {
return err
}
defer out.Close()
// Get the data
if resp, err = http.Get(url); err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("unable to download the file from URL: %s, status: %s", url, resp.Status)
log.Println(err)
return err
}
// Write the body to file
if _, err = io.Copy(out, resp.Body); err != nil {
return err
}
err = os.Chmod(fileName, os.ModePerm)
if err != nil {
log.Println(err)
}
return nil
}
func ExplodePackage(destination, fileToUntar string) error {
LogDebugMessage("Destination: %s\n", destination)
LogDebugMessage("fileToUntar: %s\n", fileToUntar)
if strings.HasSuffix(fileToUntar, "zip") {
LogDebugMessage("This is a windows file : %s", fileToUntar)
err := UnZipFile(destination, fileToUntar)
if err != nil {
return nil
}
} else if strings.HasSuffix(fileToUntar, "tar.gz") {
LogDebugMessage("This is a mac/linux file: %s", fileToUntar)
err := UntarGzFile(destination, fileToUntar)
if err != nil {
return nil
}
}
return nil
}
func UntarGzFile(destination, fileToUntar string) error {
lFile, err := os.Open(fileToUntar)
if err != nil {
err = errors.Wrapf(err, "unable to read the local file %s", fileToUntar)
log.Fatal(err)
return err
}
gzReader, err := gzip.NewReader(lFile)
if err != nil {
err = errors.Wrap(err, "unable to load the file into a gz reader")
log.Fatal(err)
return err
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
switch {
case err == io.EOF:
return nil
case err != nil:
err = errors.Wrap(err, "error during untar")
log.Fatal(err)
return err
case header == nil:
continue
}
fileInLoop := filepath.Join(destination, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(fileInLoop); err != nil {
if err := os.MkdirAll(fileInLoop, 0755); err != nil {
err = errors.Wrapf(err, "error creating directory %s", fileInLoop)
log.Fatal(err)
return err
}
}
case tar.TypeReg:
fileAtLoc, err := os.OpenFile(fileInLoop, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
err = errors.Wrapf(err, "error opening file %s", fileInLoop)
log.Fatal(err)
return err
}
if _, err := io.Copy(fileAtLoc, tarReader); err != nil {
err = errors.Wrapf(err, "error writing file %s", fileInLoop)
log.Fatal(err)
return err
}
fileAtLoc.Close()
fileAtLoc.Chmod(os.ModePerm)
}
}
}
func UnZipFile(destination, fileToUnzip string) error {
zipReader, _ := zip.OpenReader(fileToUnzip)
for _, file := range zipReader.Reader.File {
zippedFile, err := file.Open()
if err != nil {
log.Fatal(err)
}
defer zippedFile.Close()
extractedFilePath := filepath.Join(
destination,
file.Name,
)
outputFile, err := os.OpenFile(
extractedFilePath,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
file.Mode(),
)
if err != nil {
log.Fatal(err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, zippedFile)
if err != nil {
log.Fatal(err)
}
LogDebugMessage("File extracted: %s, Extracted file path: %s\n", file.Name, extractedFilePath)
}
return nil
}

47
pkg/api/utils_test.go Normal file
View File

@@ -0,0 +1,47 @@
package api
import (
"testing"
)
func TestProcessConfigArgs(t *testing.T) {
args := []string{
"qliksense.mongodb=mongouri://something?ffall",
"test_under.test=value_under",
"test-dash.dash-key=value-dash",
"test-dot.dot-key=127.0.0.1",
"test123.key123=value123",
"test-equal.keyequal=newvalue=@hj",
}
expectedKeys := []string{"mongodb", "test", "dash-key", "dot-key", "key123", "keyequal"}
expectedValue := []string{"mongouri://something?ffall", "value_under", "value-dash", "127.0.0.1", "value123", "newvalue=@hj"}
exppectedSvc := []string{"qliksense", "test_under", "test-dash", "test-dot", "test123", "test-equal"}
sv, err := ProcessConfigArgs(args, false)
if err != nil {
t.Log(err)
t.FailNow()
}
for _, v := range sv {
if !contains(expectedKeys, v.Key) {
t.Fail()
t.Log("expectd key " + v.Key + " not found")
}
if !contains(expectedValue, v.Value) {
t.Fail()
t.Log("expectd Value " + v.Value + " not found")
}
if !contains(exppectedSvc, v.SvcName) {
t.Fail()
t.Log("expectd service " + v.SvcName + " not found")
}
}
}
func contains(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}

111
pkg/preflight/all_checks.go Normal file
View File

@@ -0,0 +1,111 @@
package preflight
import (
"fmt"
ansi "github.com/mattn/go-colorable"
"github.com/pkg/errors"
"github.com/ttacon/chalk"
)
func (qp *QliksensePreflight) RunAllPreflightChecks(kubeConfigContents []byte, namespace string, preflightOpts *PreflightOptions) error {
checkCount := 0
totalCount := 0
out := ansi.NewColorableStdout()
// Preflight minimum kuberenetes version check
if err := qp.CheckK8sVersion(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight kubernetes minimum version check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight kubernetes minimum version check PASSED"))
checkCount++
}
totalCount++
// Preflight deployment check
if err := qp.CheckDeployment(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight deployment check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight deployment check PASSED"))
checkCount++
}
totalCount++
// Preflight service check
if err := qp.CheckService(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight service check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight service check PASSED"))
checkCount++
}
totalCount++
// Preflight pod check
if err := qp.CheckPod(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight pod check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight pod check PASSED"))
checkCount++
}
totalCount++
// Preflight role check
if err := qp.CheckCreateRole(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color("Preflight role check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight role check PASSED"))
checkCount++
}
totalCount++
// Preflight rolebinding check
if err := qp.CheckCreateRoleBinding(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color(" Preflight rolebinding check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight rolebinding check PASSED"))
checkCount++
}
totalCount++
// Preflight serviceaccount check
if err := qp.CheckCreateServiceAccount(namespace); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color(" Preflight serviceaccount check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight serviceaccount check PASSED"))
checkCount++
}
totalCount++
// Preflight mongo check
if err := qp.CheckMongo(kubeConfigContents, namespace, preflightOpts); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color(" Preflight mongo check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight mongo check PASSED"))
checkCount++
}
totalCount++
// Preflight DNS check
if err := qp.CheckDns(namespace, kubeConfigContents); err != nil {
fmt.Fprintf(out, "%s\n", chalk.Red.Color(" Preflight DNS check FAILED"))
fmt.Printf("Error: %v\n\n", err)
} else {
fmt.Fprintf(out, "%s\n\n", chalk.Green.Color("Preflight DNS check PASSED"))
checkCount++
}
totalCount++
if checkCount == totalCount {
// All preflight checks were successful
return nil
}
return errors.New("1 or more preflight checks have FAILED")
}

View File

@@ -0,0 +1,127 @@
package preflight
import (
"fmt"
"k8s.io/client-go/kubernetes"
)
func (qp *QliksensePreflight) CheckDeployment(namespace string, kubeConfigContents []byte) error {
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("Kube config error: %v\n", err)
return err
}
// Deployment check
qp.P.LogVerboseMessage("Preflight deployment check: \n")
qp.P.LogVerboseMessage("--------------------------- \n")
err = qp.checkPfDeployment(clientset, namespace, "deployment-preflight-check")
if err != nil {
qp.P.LogVerboseMessage("Preflight Deployment check: FAILED\n")
return err
}
qp.P.LogVerboseMessage("Completed preflight deployment check\n")
return nil
}
func (qp *QliksensePreflight) CheckService(namespace string, kubeConfigContents []byte) error {
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("unable to create a kubernetes client: %v\n", err)
return err
}
// Service check
qp.P.LogVerboseMessage("Preflight service check: \n")
qp.P.LogVerboseMessage("------------------------ \n")
err = qp.checkPfService(clientset, namespace)
if err != nil {
qp.P.LogVerboseMessage("Preflight Service check: FAILED\n")
return err
}
qp.P.LogVerboseMessage("Completed preflight service check\n")
return nil
}
func (qp *QliksensePreflight) CheckPod(namespace string, kubeConfigContents []byte) error {
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("error: unable to create a kubernetes client: %v\n", err)
return err
}
// Pod check
qp.P.LogVerboseMessage("Preflight pod check: \n")
qp.P.LogVerboseMessage("-------------------- \n")
err = qp.checkPfPod(clientset, namespace)
if err != nil {
qp.P.LogVerboseMessage("Preflight Pod check: FAILED\n")
return err
}
qp.P.LogVerboseMessage("Completed preflight pod check\n")
return nil
}
func (qp *QliksensePreflight) checkPfPod(clientset *kubernetes.Clientset, namespace string) error {
// create a pod
podName := "pod-pf-check"
commandToRun := []string{}
imageName, err := qp.GetPreflightConfigObj().GetImageName(nginx, true)
if err != nil {
return err
}
pod, err := qp.createPreflightTestPod(clientset, namespace, podName, imageName, nil, commandToRun)
if err != nil {
err = fmt.Errorf("unable to create pod - %v\n", err)
return err
}
defer qp.deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, pod); err != nil {
return err
}
qp.P.LogVerboseMessage("Preflight pod creation check: PASSED\n")
qp.P.LogVerboseMessage("Cleaning up resources...\n")
return nil
}
func (qp *QliksensePreflight) checkPfService(clientset *kubernetes.Clientset, namespace string) error {
// creating service
serviceName := "svc-pf-check"
pfService, err := qp.createPreflightTestService(clientset, namespace, serviceName)
if err != nil {
err = fmt.Errorf("unable to create service - %v\n", err)
return err
}
defer qp.deleteService(clientset, namespace, serviceName)
_, err = getService(clientset, namespace, pfService.GetName())
if err != nil {
err = fmt.Errorf("unable to retrieve service - %v\n", err)
return err
}
qp.P.LogVerboseMessage("Preflight service creation check: PASSED\n")
qp.P.LogVerboseMessage("Cleaning up resources...\n")
return nil
}
func (qp *QliksensePreflight) checkPfDeployment(clientset *kubernetes.Clientset, namespace, depName string) error {
// check if we are able to create a deployment
imageName, err := qp.GetPreflightConfigObj().GetImageName(nginx, true)
if err != nil {
return err
}
pfDeployment, err := qp.createPreflightTestDeployment(clientset, namespace, depName, imageName)
if err != nil {
err = fmt.Errorf("unable to create deployment - %v\n", err)
return err
}
defer qp.deleteDeployment(clientset, namespace, depName)
if err := waitForDeployment(clientset, namespace, pfDeployment); err != nil {
return err
}
qp.P.LogVerboseMessage("Preflight Deployment check: PASSED\n")
qp.P.LogVerboseMessage("Cleaning up resources...\n")
return nil
}

View File

@@ -0,0 +1,91 @@
package preflight
import (
"fmt"
"strings"
)
const (
nginx = "nginx"
netcat = "netcat"
)
func (qp *QliksensePreflight) CheckDns(namespace string, kubeConfigContents []byte) error {
qp.P.LogVerboseMessage("Preflight DNS check: \n")
qp.P.LogVerboseMessage("------------------- \n")
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("unable to create a kubernetes client: %v\n", err)
return err
}
// creating deployment
depName := "dep-dns-preflight-check"
nginxImageName, err := qp.GetPreflightConfigObj().GetImageName(nginx, true)
if err != nil {
return err
}
dnsDeployment, err := qp.createPreflightTestDeployment(clientset, namespace, depName, nginxImageName)
if err != nil {
err = fmt.Errorf("unable to create deployment: %v\n", err)
return err
}
defer qp.deleteDeployment(clientset, namespace, depName)
if err := waitForDeployment(clientset, namespace, dnsDeployment); err != nil {
return err
}
// creating service
serviceName := "svc-dns-pf-check"
dnsService, err := qp.createPreflightTestService(clientset, namespace, serviceName)
if err != nil {
err = fmt.Errorf("unable to create service : %s, %s\n", serviceName, err)
return err
}
defer qp.deleteService(clientset, namespace, serviceName)
// create a pod
podName := "pf-pod-1"
commandToRun := []string{"sh", "-c", "sleep 10; nc -z -v -w 1 " + dnsService.Name + " 80"}
netcatImageName, err := qp.GetPreflightConfigObj().GetImageName(netcat, true)
if err != nil {
err = fmt.Errorf("unable to retrieve image : %v\n", err)
return err
}
dnsPod, err := qp.createPreflightTestPod(clientset, namespace, podName, netcatImageName, nil, commandToRun)
if err != nil {
err = fmt.Errorf("unable to create pod : %s, %s\n", podName, err)
return err
}
defer qp.deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, dnsPod); err != nil {
return err
}
if len(dnsPod.Spec.Containers) == 0 {
err := fmt.Errorf("there are no containers in the pod")
return err
}
waitForPodToDie(clientset, namespace, dnsPod)
logStr, err := getPodLogs(clientset, dnsPod)
if err != nil {
err = fmt.Errorf("unable to execute dns check in the cluster: %v", err)
return err
}
if strings.HasSuffix(strings.TrimSpace(logStr), "succeeded!") {
qp.P.LogVerboseMessage("Preflight DNS check: PASSED\n")
} else {
err = fmt.Errorf("Expected response not found\n")
return err
}
qp.P.LogVerboseMessage("Completed preflight DNS check\n")
qp.P.LogVerboseMessage("Cleaning up resources...\n")
return nil
}

View File

@@ -0,0 +1,159 @@
package preflight
import (
"fmt"
"io/ioutil"
"strings"
"github.com/qlik-oss/sense-installer/pkg/api"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
apiv1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
const (
mongo = "mongo"
)
func (qp *QliksensePreflight) CheckMongo(kubeConfigContents []byte, namespace string, preflightOpts *PreflightOptions) error {
qp.P.LogVerboseMessage("Preflight mongodb check: \n")
qp.P.LogVerboseMessage("------------------------ \n")
if preflightOpts.MongoOptions.MongodbUrl == "" {
// infer mongoDbUrl from currentCR
qp.P.LogVerboseMessage("MongoDbUri is empty, infer from CR\n")
qConfig := qapi.NewQConfig(qp.Q.QliksenseHome)
var currentCR *qapi.QliksenseCR
var err error
qConfig.SetNamespace(namespace)
currentCR, err = qConfig.GetCurrentCR()
if err != nil {
qp.P.LogVerboseMessage("Unable to retrieve current CR: %v\n", err)
return err
}
decryptedCR, err := qConfig.GetDecryptedCr(currentCR)
if err != nil {
qp.P.LogVerboseMessage("An error occurred while retrieving mongodbUrl from current CR: %v\n", err)
return err
}
preflightOpts.MongoOptions.MongodbUrl = decryptedCR.Spec.GetFromSecrets("qliksense", "mongoDbUri")
}
qp.P.LogVerboseMessage("MongodbUrl: %s\n", preflightOpts.MongoOptions.MongodbUrl)
if err := qp.mongoConnCheck(kubeConfigContents, namespace, preflightOpts); err != nil {
return err
}
qp.P.LogVerboseMessage("Completed preflight mongodb check\n")
return nil
}
func (qp *QliksensePreflight) mongoConnCheck(kubeConfigContents []byte, namespace string, preflightOpts *PreflightOptions) error {
var caCertSecretName, clientCertSecretName string
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("unable to create a kubernetes client: %v\n", err)
return err
}
var secrets []string
if preflightOpts.MongoOptions.CaCertFile != "" {
caCertSecretName = "preflight-mongo-test-cacert"
caCertSecret, err := qp.createSecret(clientset, namespace, preflightOpts.MongoOptions.CaCertFile, caCertSecretName)
if err != nil {
err = fmt.Errorf("unable to create a ca cert kubernetes secret: %v\n", err)
return err
}
defer qp.deleteK8sSecret(clientset, namespace, caCertSecret)
secrets = append(secrets, caCertSecretName)
}
if preflightOpts.MongoOptions.ClientCertFile != "" {
clientCertSecretName = "preflight-mongo-test-clientcert"
clientCertSecret, err := qp.createSecret(clientset, namespace, preflightOpts.MongoOptions.ClientCertFile, clientCertSecretName)
if err != nil {
err = fmt.Errorf("unable to create a client cert kubernetes secret: %v\n", err)
return err
}
defer qp.deleteK8sSecret(clientset, namespace, clientCertSecret)
secrets = append(secrets, clientCertSecretName)
}
mongoCommand := strings.Builder{}
mongoCommand.WriteString(fmt.Sprintf("sleep 10;mongo %s", preflightOpts.MongoOptions.MongodbUrl))
if preflightOpts.MongoOptions.Username != "" {
mongoCommand.WriteString(fmt.Sprintf(" --username %s", preflightOpts.MongoOptions.Username))
api.LogDebugMessage("Adding username: Mongo command: %s\n", mongoCommand.String())
}
if preflightOpts.MongoOptions.Password != "" {
mongoCommand.WriteString(fmt.Sprintf(" --password %s", preflightOpts.MongoOptions.Password))
api.LogDebugMessage("Adding username and password\n")
}
if preflightOpts.MongoOptions.Tls || preflightOpts.MongoOptions.CaCertFile != "" || preflightOpts.MongoOptions.ClientCertFile != "" {
mongoCommand.WriteString(" --tls")
api.LogDebugMessage("Adding --tls: Mongo command: %s\n", mongoCommand.String())
}
if preflightOpts.MongoOptions.CaCertFile != "" {
mongoCommand.WriteString(fmt.Sprintf(" --tlsCAFile=/etc/ssl/%s/%[1]s", caCertSecretName))
api.LogDebugMessage("Adding caCertFile: Mongo command: %s\n", mongoCommand.String())
}
if preflightOpts.MongoOptions.ClientCertFile != "" {
mongoCommand.WriteString(fmt.Sprintf(" --tlsCertificateKeyFile=/etc/ssl/%s/%[1]s", clientCertSecretName))
api.LogDebugMessage("Adding clientCertFile: Mongo command: %s\n", mongoCommand.String())
}
mongoCommand.WriteString(` --eval "print(\"connected to mongo\")"`)
commandToRun := []string{"sh", "-c", mongoCommand.String()}
api.LogDebugMessage("Mongo command: %s\n", strings.Join(commandToRun, " "))
// create a pod
podName := "pf-mongo-pod"
imageName, err := qp.GetPreflightConfigObj().GetImageName(mongo, true)
if err != nil {
err = fmt.Errorf("unable to retrieve image : %v\n", err)
return err
}
mongoPod, err := qp.createPreflightTestPod(clientset, namespace, podName, imageName, secrets, commandToRun)
if err != nil {
err = fmt.Errorf("unable to create pod : %v\n", err)
return err
}
defer qp.deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, mongoPod); err != nil {
return err
}
if len(mongoPod.Spec.Containers) == 0 {
err := fmt.Errorf("there are no containers in the pod- %v\n", err)
return err
}
waitForPodToDie(clientset, namespace, mongoPod)
logStr, err := getPodLogs(clientset, mongoPod)
if err != nil {
err = fmt.Errorf("unable to execute mongo check in the cluster: %v\n", err)
return err
}
stringToCheck := "Implicit session:"
if strings.Contains(logStr, stringToCheck) {
qp.P.LogVerboseMessage("Preflight mongo check: PASSED\n")
} else {
err = fmt.Errorf("Connection failed: %s\n", logStr)
return err
}
return nil
}
func (qp *QliksensePreflight) createSecret(clientset *kubernetes.Clientset, namespace, certFile, certSecretName string) (*apiv1.Secret, error) {
certBytes, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
certSecret, err := qp.createPreflightTestSecret(clientset, namespace, certSecretName, certBytes)
if err != nil {
err = fmt.Errorf("unable to create secret with ca cert : %v\n", err)
return nil, err
}
return certSecret, nil
}

View File

@@ -0,0 +1,714 @@
package preflight
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
"k8s.io/api/rbac/v1beta1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/retry"
)
type PreflightOptions struct {
Verbose bool
MongoOptions *MongoOptions
}
// LogVerboseMessage logs a verbose message
func (p *PreflightOptions) LogVerboseMessage(strMessage string, args ...interface{}) {
if p.Verbose || os.Getenv("QLIKSENSE_DEBUG") == "true" {
fmt.Printf(strMessage, args...)
}
}
type MongoOptions struct {
MongodbUrl string
Username string
Password string
CaCertFile string
ClientCertFile string
Tls bool
}
var gracePeriod int64 = 0
type QliksensePreflight struct {
Q *qliksense.Qliksense
P *PreflightOptions
}
func (qp *QliksensePreflight) GetPreflightConfigObj() *api.PreflightConfig {
return api.NewPreflightConfig(qp.Q.QliksenseHome)
}
func InitPreflight() (string, []byte, error) {
api.LogDebugMessage("Reading .kube/config file...")
homeDir, err := homedir.Dir()
if err != nil {
err = fmt.Errorf("Unable to deduce home dir\n")
return "", nil, err
}
api.LogDebugMessage("Kube config location: %s\n\n", filepath.Join(homeDir, ".kube", "config"))
kubeConfig := filepath.Join(homeDir, ".kube", "config")
kubeConfigContents, err := ioutil.ReadFile(kubeConfig)
if err != nil {
err = fmt.Errorf("Unable to deduce home dir\n")
return "", nil, err
}
// retrieve namespace
namespace := api.GetKubectlNamespace()
// if namespace comes back empty, we will run checks in the default namespace
if namespace == "" {
namespace = "default"
}
api.LogDebugMessage("Namespace: %s\n", namespace)
return namespace, kubeConfigContents, nil
}
func initiateK8sOps(opr, namespace string) error {
opr1 := strings.Fields(opr)
_, err := api.KubectlDirectOps(opr1, namespace)
if err != nil {
fmt.Println(err)
return err
}
return nil
}
func int32Ptr(i int32) *int32 { return &i }
func retryOnError(mf func() error) error {
return retry.OnError(wait.Backoff{
Duration: 1 * time.Second,
Factor: 1,
Jitter: 0.1,
Steps: 5,
}, func(err error) bool {
return k8serrors.IsConflict(err) || k8serrors.IsGone(err) || k8serrors.IsServerTimeout(err) ||
k8serrors.IsServiceUnavailable(err) || k8serrors.IsTimeout(err) || k8serrors.IsTooManyRequests(err)
}, mf)
}
func getK8SClientSet(kubeconfig []byte, contextName string) (*kubernetes.Clientset, *rest.Config, error) {
var clientConfig *rest.Config
var err error
if len(kubeconfig) == 0 {
clientConfig, err = rest.InClusterConfig()
if err != nil {
err = errors.Wrap(err, "Unable to load in-cluster kubeconfig")
return nil, nil, err
}
} else {
config, err := clientcmd.Load(kubeconfig)
if err != nil {
err = errors.Wrap(err, "Unable to load kubeconfig")
return nil, nil, err
}
if contextName != "" {
config.CurrentContext = contextName
}
clientConfig, err = clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
err = errors.Wrap(err, "Unable to create client config from config")
return nil, nil, err
}
}
clientset, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
err = errors.Wrap(err, "Unable to create clientset")
return nil, nil, err
}
return clientset, clientConfig, nil
}
func (qp *QliksensePreflight) createPreflightTestDeployment(clientset *kubernetes.Clientset, namespace string, depName string, imageName string) (*appsv1.Deployment, error) {
deploymentsClient := clientset.AppsV1().Deployments(namespace)
deployment := &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{
Name: depName,
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(1),
Selector: &v1.LabelSelector{
MatchLabels: map[string]string{
"app": "preflight-check",
},
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: v1.ObjectMeta{
Labels: map[string]string{
"app": "preflight-check",
"label": "preflight-check-label",
},
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{
{
Name: "dep",
Image: imageName,
Ports: []apiv1.ContainerPort{
{
Name: "http",
Protocol: apiv1.ProtocolTCP,
ContainerPort: 80,
},
},
},
},
},
},
},
}
// Create Deployment
var result *appsv1.Deployment
if err := retryOnError(func() (err error) {
result, err = deploymentsClient.Create(deployment)
return err
}); err != nil {
err = errors.Wrapf(err, "unable to create deployments in the %s namespace", namespace)
return nil, err
}
qp.P.LogVerboseMessage("Created deployment %q\n", result.GetObjectMeta().GetName())
return deployment, nil
}
func getDeployment(clientset *kubernetes.Clientset, namespace, depName string) (*appsv1.Deployment, error) {
deploymentsClient := clientset.AppsV1().Deployments(namespace)
var deployment *appsv1.Deployment
if err := retryOnError(func() (err error) {
deployment, err = deploymentsClient.Get(depName, v1.GetOptions{})
return err
}); err != nil {
err = errors.Wrapf(err, "unable to get deployments in the %s namespace", namespace)
api.LogDebugMessage("%v\n", err)
return nil, err
}
return deployment, nil
}
func (qp *QliksensePreflight) deleteDeployment(clientset *kubernetes.Clientset, namespace, name string) error {
deploymentsClient := clientset.AppsV1().Deployments(namespace)
// Create Deployment
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
GracePeriodSeconds: &gracePeriod,
}
if err := retryOnError(func() (err error) {
return deploymentsClient.Delete(name, &deleteOptions)
}); err != nil {
return err
}
if err := waitForDeploymentToDelete(clientset, namespace, name); err != nil {
return err
}
qp.P.LogVerboseMessage("Deleted deployment: %s\n", name)
return nil
}
func (qp *QliksensePreflight) createPreflightTestService(clientset *kubernetes.Clientset, namespace string, svcName string) (*apiv1.Service, error) {
iptr := int32Ptr(80)
servicesClient := clientset.CoreV1().Services(namespace)
service := &apiv1.Service{
ObjectMeta: v1.ObjectMeta{
Name: svcName,
Namespace: namespace,
Labels: map[string]string{
"app": "preflight-check",
},
},
Spec: apiv1.ServiceSpec{
Ports: []apiv1.ServicePort{
{Name: "port1",
Port: *iptr,
},
},
Selector: map[string]string{
"app": "preflight-check",
},
ClusterIP: "",
},
}
var result *apiv1.Service
if err := retryOnError(func() (err error) {
result, err = servicesClient.Create(service)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created service %q\n", result.GetObjectMeta().GetName())
return service, nil
}
func getService(clientset *kubernetes.Clientset, namespace, svcName string) (*apiv1.Service, error) {
servicesClient := clientset.CoreV1().Services(namespace)
var svc *apiv1.Service
if err := retryOnError(func() (err error) {
svc, err = servicesClient.Get(svcName, v1.GetOptions{})
return err
}); err != nil {
err = errors.Wrapf(err, "unable to get services in the %s namespace", namespace)
return nil, err
}
return svc, nil
}
func (qp *QliksensePreflight) deleteService(clientset *kubernetes.Clientset, namespace, name string) error {
servicesClient := clientset.CoreV1().Services(namespace)
// Create Deployment
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
if err := retryOnError(func() (err error) {
return servicesClient.Delete(name, &deleteOptions)
}); err != nil {
fmt.Println(err)
return err
}
qp.P.LogVerboseMessage("Deleted service: %s\n", name)
return nil
}
func (qp *QliksensePreflight) deletePod(clientset *kubernetes.Clientset, namespace, name string) error {
podsClient := clientset.CoreV1().Pods(namespace)
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
GracePeriodSeconds: &gracePeriod,
}
if err := retryOnError(func() (err error) {
return podsClient.Delete(name, &deleteOptions)
}); err != nil {
return err
}
if err := waitForPodToDelete(clientset, namespace, name); err != nil {
return err
}
qp.P.LogVerboseMessage("Deleted pod: %s\n", name)
return nil
}
func (qp *QliksensePreflight) createPreflightTestPod(clientset *kubernetes.Clientset, namespace, podName, imageName string, secretNames []string, commandToRun []string) (*apiv1.Pod, error) {
// build the pod definition we want to deploy
pod := &apiv1.Pod{
ObjectMeta: v1.ObjectMeta{
Name: podName,
Namespace: namespace,
Labels: map[string]string{
"app": "preflight",
},
},
Spec: apiv1.PodSpec{
RestartPolicy: apiv1.RestartPolicyNever,
Containers: []apiv1.Container{
{
Name: "cnt",
Image: imageName,
ImagePullPolicy: apiv1.PullIfNotPresent,
Command: commandToRun,
},
},
},
}
if len(secretNames) > 0 {
for _, secretName := range secretNames {
pod.Spec.Volumes = append(pod.Spec.Volumes, apiv1.Volume{
Name: secretName,
VolumeSource: apiv1.VolumeSource{
Secret: &apiv1.SecretVolumeSource{
SecretName: secretName,
Items: []apiv1.KeyToPath{
{
Key: secretName,
Path: secretName,
},
},
},
},
})
if len(pod.Spec.Containers) > 0 {
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, apiv1.VolumeMount{
Name: secretName,
MountPath: "/etc/ssl/" + secretName,
ReadOnly: true,
})
}
}
}
// now create the pod in kubernetes cluster using the clientset
if err := retryOnError(func() (err error) {
pod, err = clientset.CoreV1().Pods(namespace).Create(pod)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created pod: %s\n", pod.Name)
return pod, nil
}
func getPod(clientset *kubernetes.Clientset, namespace, podName string) (*apiv1.Pod, error) {
api.LogDebugMessage("Fetching pod: %s\n", podName)
var pod *apiv1.Pod
if err := retryOnError(func() (err error) {
pod, err = clientset.CoreV1().Pods(namespace).Get(podName, v1.GetOptions{})
return err
}); err != nil {
api.LogDebugMessage("%v\n", err)
return nil, err
}
return pod, nil
}
func getPodLogs(clientset *kubernetes.Clientset, pod *apiv1.Pod) (string, error) {
podLogOpts := apiv1.PodLogOptions{}
api.LogDebugMessage("Retrieving logs for pod: %s namespace: %s\n", pod.GetName(), pod.Namespace)
req := clientset.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts)
podLogs, err := req.Stream()
if err != nil {
return "", err
}
defer podLogs.Close()
time.Sleep(15 * time.Second)
buf := new(bytes.Buffer)
_, err = io.Copy(buf, podLogs)
if err != nil {
return "", err
}
api.LogDebugMessage("Log from pod: %s\n", buf.String())
return buf.String(), nil
}
func waitForResource(checkFunc func() (interface{}, error), validateFunc func(interface{}) bool) error {
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
OUT:
for {
r, err := checkFunc()
if err != nil {
return err
}
select {
case <-timeout.C:
break OUT
default:
if validateFunc(r) {
break OUT
}
}
time.Sleep(5 * time.Second)
}
return nil
}
func waitForDeployment(clientset *kubernetes.Clientset, namespace string, pfDeployment *appsv1.Deployment) error {
var err error
depName := pfDeployment.GetName()
checkFunc := func() (interface{}, error) {
pfDeployment, err = getDeployment(clientset, namespace, depName)
if err != nil {
err = fmt.Errorf("unable to retrieve deployment: %s\n", depName)
return nil, err
}
return pfDeployment, nil
}
validateFunc := func(data interface{}) bool {
d := data.(*appsv1.Deployment)
return int(d.Status.ReadyReplicas) > 0
}
if err := waitForResource(checkFunc, validateFunc); err != nil {
return err
}
if int(pfDeployment.Status.ReadyReplicas) == 0 {
err = fmt.Errorf("deployment took longer than expected to spin up pods")
return err
}
return nil
}
func waitForPod(clientset *kubernetes.Clientset, namespace string, pod *apiv1.Pod) error {
var err error
if len(pod.Spec.Containers) == 0 {
err = fmt.Errorf("there are no containers in the pod")
return err
}
podName := pod.Name
checkFunc := func() (interface{}, error) {
pod, err = getPod(clientset, namespace, podName)
if err != nil {
err = fmt.Errorf("unable to retrieve %s pod by name", podName)
return nil, err
}
return pod, nil
}
validateFunc := func(data interface{}) bool {
po := data.(*apiv1.Pod)
return len(po.Status.ContainerStatuses) > 0 && po.Status.ContainerStatuses[0].Ready
}
if err := waitForResource(checkFunc, validateFunc); err != nil {
return err
}
if len(pod.Status.ContainerStatuses) == 0 || !pod.Status.ContainerStatuses[0].Ready {
err = fmt.Errorf("container is taking much longer than expected")
return err
}
return nil
}
func waitForPodToDie(clientset *kubernetes.Clientset, namespace string, pod *apiv1.Pod) error {
podName := pod.Name
checkFunc := func() (interface{}, error) {
po, err := getPod(clientset, namespace, podName)
if err != nil {
err = fmt.Errorf("unable to retrieve %s pod by name", podName)
return nil, err
}
api.LogDebugMessage("pod status: %v\n", po.Status.Phase)
return po, nil
}
validateFunc := func(r interface{}) bool {
po := r.(*apiv1.Pod)
return po.Status.Phase == apiv1.PodFailed || po.Status.Phase == apiv1.PodSucceeded
}
if err := waitForResource(checkFunc, validateFunc); err != nil {
return err
}
return nil
}
func waitForPodToDelete(clientset *kubernetes.Clientset, namespace, podName string) error {
checkFunc := func() (interface{}, error) {
po, err := getPod(clientset, namespace, podName)
if err != nil {
return nil, err
}
return po, nil
}
validateFunc := func(po interface{}) bool {
return false
}
if err := waitForResource(checkFunc, validateFunc); err != nil {
return nil
}
err := fmt.Errorf("delete pod is taking unusually long")
return err
}
func waitForDeploymentToDelete(clientset *kubernetes.Clientset, namespace, deploymentName string) error {
checkFunc := func() (interface{}, error) {
dep, err := getDeployment(clientset, namespace, deploymentName)
if err != nil {
return nil, err
}
return dep, nil
}
validateFunc := func(po interface{}) bool {
return false
}
if err := waitForResource(checkFunc, validateFunc); err != nil {
return nil
}
err := fmt.Errorf("delete deployment is taking unusually long")
return err
}
func (qp *QliksensePreflight) createPfRole(clientset *kubernetes.Clientset, namespace, roleName string) (*v1beta1.Role, error) {
// build the role defination we want to create
var role *v1beta1.Role
roleSpec := &v1beta1.Role{
ObjectMeta: v1.ObjectMeta{
Name: roleName,
Namespace: namespace,
Labels: map[string]string{
"app": "preflight",
},
},
Rules: []v1beta1.PolicyRule{},
}
// now create the role in kubernetes cluster using the clientset
if err := retryOnError(func() (err error) {
role, err = clientset.RbacV1beta1().Roles(namespace).Create(roleSpec)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created role: %s\n", role.Name)
return role, nil
}
func (qp *QliksensePreflight) deleteRole(clientset *kubernetes.Clientset, namespace string, role *v1beta1.Role) {
rolesClient := clientset.RbacV1beta1().Roles(namespace)
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
err := rolesClient.Delete(role.GetName(), &deleteOptions)
if err != nil {
log.Fatal(err)
}
qp.P.LogVerboseMessage("Deleted role: %s\n\n", role.Name)
}
func (qp *QliksensePreflight) createPfRoleBinding(clientset *kubernetes.Clientset, namespace, roleBindingName string) (*v1beta1.RoleBinding, error) {
var roleBinding *v1beta1.RoleBinding
// build the rolebinding defination we want to create
roleBindingSpec := &v1beta1.RoleBinding{
ObjectMeta: v1.ObjectMeta{
Name: roleBindingName,
Namespace: namespace,
Labels: map[string]string{
"app": "preflight",
},
},
Subjects: []v1beta1.Subject{
{
Kind: "ServiceAccount",
APIGroup: "",
Name: "preflight-check-subject",
Namespace: namespace,
},
},
RoleRef: v1beta1.RoleRef{
APIGroup: "",
Kind: "Role",
Name: "preflight-check-roleref",
},
}
// now create the roleBinding in kubernetes cluster using the clientset
if err := retryOnError(func() (err error) {
roleBinding, err = clientset.RbacV1beta1().RoleBindings(namespace).Create(roleBindingSpec)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created RoleBinding: %s\n", roleBindingSpec.Name)
return roleBinding, nil
}
func (qp *QliksensePreflight) deleteRoleBinding(clientset *kubernetes.Clientset, namespace string, roleBinding *v1beta1.RoleBinding) {
roleBindingClient := clientset.RbacV1beta1().RoleBindings(namespace)
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
err := roleBindingClient.Delete(roleBinding.GetName(), &deleteOptions)
if err != nil {
log.Fatal(err)
}
qp.P.LogVerboseMessage("Deleted RoleBinding: %s\n\n", roleBinding.Name)
}
func (qp *QliksensePreflight) createPfServiceAccount(clientset *kubernetes.Clientset, namespace, serviceAccountName string) (*apiv1.ServiceAccount, error) {
var serviceAccount *apiv1.ServiceAccount
// build the serviceAccount defination we want to create
serviceAccountSpec := &apiv1.ServiceAccount{
ObjectMeta: v1.ObjectMeta{
Name: "preflight-check-test-serviceaccount",
Namespace: namespace,
Labels: map[string]string{
"app": "preflight",
},
},
}
// now create the serviceAccount in kubernetes cluster using the clientset
if err := retryOnError(func() (err error) {
serviceAccount, err = clientset.CoreV1().ServiceAccounts(namespace).Create(serviceAccountSpec)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created Service Account: %s\n", serviceAccountSpec.Name)
return serviceAccount, nil
}
func (qp *QliksensePreflight) deleteServiceAccount(clientset *kubernetes.Clientset, namespace string, serviceAccount *apiv1.ServiceAccount) {
serviceAccountClient := clientset.CoreV1().ServiceAccounts(namespace)
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
err := serviceAccountClient.Delete(serviceAccount.GetName(), &deleteOptions)
if err != nil {
log.Fatal(err)
}
qp.P.LogVerboseMessage("Deleted ServiceAccount: %s\n\n", serviceAccount.Name)
}
func (qp *QliksensePreflight) createPreflightTestSecret(clientset *kubernetes.Clientset, namespace, secretName string, secretData []byte) (*apiv1.Secret, error) {
var secret *apiv1.Secret
var err error
// build the secret defination we want to create
secretSpec := &apiv1.Secret{
ObjectMeta: v1.ObjectMeta{
Name: secretName,
Namespace: namespace,
Labels: map[string]string{
"app": "preflight",
},
},
Data: map[string][]byte{
secretName: secretData,
},
}
// now create the secret in kubernetes cluster using the clientset
if err = retryOnError(func() (err error) {
secret, err = clientset.CoreV1().Secrets(namespace).Create(secretSpec)
return err
}); err != nil {
return nil, err
}
qp.P.LogVerboseMessage("Created Secret: %s\n", secret.Name)
return secret, nil
}
func (qp *QliksensePreflight) deleteK8sSecret(clientset *kubernetes.Clientset, namespace string, k8sSecret *apiv1.Secret) {
secretClient := clientset.CoreV1().Secrets(namespace)
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
err := secretClient.Delete(k8sSecret.GetName(), &deleteOptions)
if err != nil {
log.Fatal(err)
}
qp.P.LogVerboseMessage("Deleted Secret: %s\n", k8sSecret.Name)
}

View File

@@ -0,0 +1,43 @@
package preflight
import (
"fmt"
"testing"
)
func Test_initiateK8sOps(t *testing.T) {
t.Skip()
type args struct {
opr string
namespace string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
opr: fmt.Sprintf("version"),
namespace: "test-ns",
},
wantErr: false,
},
{
name: "invalid case",
args: args{
opr: fmt.Sprintf("versions"),
namespace: "test-ns",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := initiateK8sOps(tt.args.opr, tt.args.namespace); (err != nil) != tt.wantErr {
t.Errorf("initiateK8sOps() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

157
pkg/preflight/role_check.go Normal file
View File

@@ -0,0 +1,157 @@
package preflight
import (
"fmt"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/qlik-oss/sense-installer/pkg/api"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
)
var resultYamlBytes = []byte("")
func (qp *QliksensePreflight) CheckCreateRole(namespace string) error {
// create a Role
qp.P.LogVerboseMessage("Preflight role check: \n")
qp.P.LogVerboseMessage("--------------------- \n")
err := qp.checkCreateEntity(namespace, "Role")
if err != nil {
return err
}
qp.P.LogVerboseMessage("Completed preflight role check\n")
return nil
}
func (qp *QliksensePreflight) CheckCreateRoleBinding(namespace string) error {
// create a RoleBinding
qp.P.LogVerboseMessage("Preflight rolebinding check: \n")
qp.P.LogVerboseMessage("---------------------------- \n")
err := qp.checkCreateEntity(namespace, "RoleBinding")
if err != nil {
return err
}
qp.P.LogVerboseMessage("Completed preflight rolebinding check\n")
return nil
}
func (qp *QliksensePreflight) CheckCreateServiceAccount(namespace string) error {
// create a service account
qp.P.LogVerboseMessage("Preflight serviceaccount check: \n")
qp.P.LogVerboseMessage("------------------------------- \n")
err := qp.checkCreateEntity(namespace, "ServiceAccount")
if err != nil {
return err
}
qp.P.LogVerboseMessage("Completed preflight serviceaccount check\n")
return nil
}
func (qp *QliksensePreflight) checkCreateEntity(namespace, entityToTest string) error {
qConfig := qapi.NewQConfig(qp.Q.QliksenseHome)
var currentCR *qapi.QliksenseCR
mfroot := ""
kusDir := ""
var err error
currentCR, err = qConfig.GetCurrentCR()
if err != nil {
qp.P.LogVerboseMessage("Unable to retrieve current CR: %v\n", err)
return err
}
if currentCR.IsRepoExist() {
mfroot = currentCR.Spec.GetManifestsRoot()
} else if tempDownloadedDir, err := qliksense.DownloadFromGitRepoToTmpDir(qliksense.QLIK_GIT_REPO, "master"); err != nil {
qp.P.LogVerboseMessage("Unable to Download from git repo to tmp dir: %v\n", err)
return err
} else {
mfroot = tempDownloadedDir
}
if currentCR.Spec.Profile == "" {
kusDir = filepath.Join(mfroot, "manifests", "docker-desktop")
} else {
kusDir = filepath.Join(mfroot, "manifests", currentCR.Spec.Profile)
}
if len(resultYamlBytes) == 0 {
resultYamlBytes, err = qliksense.ExecuteKustomizeBuild(kusDir)
if err != nil {
err := fmt.Errorf("Unable to retrieve manifests from executing kustomize: %s, error: %v", kusDir, err)
return err
}
}
sa := qliksense.GetYamlsFromMultiDoc(string(resultYamlBytes), entityToTest)
if sa != "" {
sa = strings.Replace(sa, "name: qliksense", "name: preflight", -1)
} else {
err := fmt.Errorf("Unable to retrieve yamls to apply on cluster from dir: %s, error: %v", kusDir, err)
return err
}
namespace = "" // namespace is handled when generating the manifests
defer func() {
qp.P.LogVerboseMessage("Cleaning up resources...\n")
err := api.KubectlDeleteVerbose(sa, namespace, qp.P.Verbose)
if err != nil {
qp.P.LogVerboseMessage("Preflight cleanup failed!\n")
}
}()
err = api.KubectlApplyVerbose(sa, namespace, qp.P.Verbose)
if err != nil {
err := fmt.Errorf("Failed to create entity on the cluster: %v", err)
return err
}
qp.P.LogVerboseMessage("Preflight %s check: PASSED\n", entityToTest)
return nil
}
func (qp *QliksensePreflight) CheckCreateRB(namespace string, kubeConfigContents []byte) error {
// create a role
qp.P.LogVerboseMessage("Preflight createRole check: \n")
qp.P.LogVerboseMessage("--------------------------- \n")
errStr := strings.Builder{}
err1 := qp.checkCreateEntity(namespace, "Role")
if err1 != nil {
errStr.WriteString(err1.Error())
errStr.WriteString("\n")
qp.P.LogVerboseMessage("%v\n", err1)
qp.P.LogVerboseMessage("Preflight role check: FAILED\n")
}
qp.P.LogVerboseMessage("Completed preflight role check\n\n")
// create a roleBinding
qp.P.LogVerboseMessage("Preflight rolebinding check: \n")
qp.P.LogVerboseMessage("---------------------------- \n")
err2 := qp.checkCreateEntity(namespace, "RoleBinding")
if err2 != nil {
errStr.WriteString(err2.Error())
errStr.WriteString("\n")
qp.P.LogVerboseMessage("%v\n", err2)
qp.P.LogVerboseMessage("Preflight rolebinding check: FAILED\n")
}
qp.P.LogVerboseMessage("Completed preflight rolebinding check\n\n")
// create a service account
qp.P.LogVerboseMessage("Preflight serviceaccount check: \n")
qp.P.LogVerboseMessage("------------------------------- \n")
err3 := qp.checkCreateEntity(namespace, "ServiceAccount")
if err3 != nil {
errStr.WriteString(err3.Error())
errStr.WriteString("\n")
qp.P.LogVerboseMessage("%v\n", err3)
qp.P.LogVerboseMessage("Preflight serviceaccount check: FAILED\n")
}
qp.P.LogVerboseMessage("Completed preflight serviceaccount check\n\n")
if err1 != nil || err2 != nil || err3 != nil {
qp.P.LogVerboseMessage("Preflight authcheck: FAILED\n")
qp.P.LogVerboseMessage("Completed preflight authcheck\n")
return errors.New(errStr.String())
}
qp.P.LogVerboseMessage("Preflight authcheck: PASSED\n")
qp.P.LogVerboseMessage("Completed preflight authcheck\n")
return nil
}

View File

@@ -0,0 +1,52 @@
package preflight
import (
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/sense-installer/pkg/api"
"k8s.io/apimachinery/pkg/version"
)
func (qp *QliksensePreflight) CheckK8sVersion(namespace string, kubeConfigContents []byte) error {
qp.P.LogVerboseMessage("Preflight kubernetes version check: \n")
qp.P.LogVerboseMessage("----------------------------------- \n")
var currentVersion *semver.Version
clientset, _, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("Unable to create clientset: %v\n", err)
return err
}
var serverVersion *version.Info
if err := retryOnError(func() (err error) {
serverVersion, err = clientset.ServerVersion()
return err
}); err != nil {
err = fmt.Errorf("Unable to get server version: %v\n", err)
return err
}
qp.P.LogVerboseMessage("Kubernetes API Server version: %s\n", serverVersion.String())
// Compare K8s version on the cluster with minimum supported k8s version
currentVersion, err = semver.NewVersion(serverVersion.String())
if err != nil {
err = fmt.Errorf("Unable to convert server version into semver version: %v\n", err)
return err
}
api.LogDebugMessage("Current Kubernetes Version: %v\n", currentVersion)
minK8sVersionSemver, err := semver.NewVersion(qp.GetPreflightConfigObj().GetMinK8sVersion())
if err != nil {
err = fmt.Errorf("Unable to convert minimum Kubernetes version into semver version:%v\n", err)
return err
}
if currentVersion.GreaterThan(minK8sVersionSemver) {
qp.P.LogVerboseMessage("Current Kubernetes API Server version %s is greater than or equal to minimum required version: %s\n", currentVersion, minK8sVersionSemver)
} else {
err = fmt.Errorf("Current Kubernetes API Server version %s is less than minimum required version: %s", currentVersion, minK8sVersionSemver)
return err
}
return nil
}

BIN
pkg/qliksense/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -11,6 +11,7 @@ import (
"sort"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"gopkg.in/yaml.v2"
)
@@ -57,48 +58,43 @@ func (nw *nullWriter) Write(p []byte) (n int, err error) {
}
const (
defaultProfile = "docker-desktop"
defaultGitUrl = "https://github.com/qlik-oss/qliksense-k8s"
defaultProfile = "docker-desktop"
defaultConfigRepoGitUrl = "https://github.com/qlik-oss/qliksense-k8s"
)
func (p *Qliksense) About(gitRef, profile string) (*VersionOutput, error) {
configDirectory, isTemporary, profile, err := getConfigDirectory(defaultGitUrl, gitRef, profile)
func (q *Qliksense) About(gitRef, profile string) (*VersionOutput, error) {
configDirectory, isTemporary, profile, err := q.getConfigDirectory(defaultConfigRepoGitUrl, gitRef, profile)
if err != nil {
return nil, err
}
if isTemporary {
} else if isTemporary {
defer os.RemoveAll(configDirectory)
}
chartVersion, err := getChartVersion(filepath.Join(configDirectory, "transformers", "qseokversion.yaml"), "qliksense")
if err != nil {
return nil, err
}
kuzManifest, err := executeKustomizeBuild(filepath.Join(configDirectory, "manifests", profile))
if err != nil {
return nil, err
}
images, err := getImageList(kuzManifest)
if err != nil {
return nil, err
}
return &VersionOutput{
QliksenseVersion: chartVersion,
Images: images,
}, nil
return q.AboutDir(configDirectory, profile)
}
func getConfigDirectory(gitUrl, gitRef, profileEntered string) (dir string, isTemporary bool, profile string, err error) {
func (q *Qliksense) AboutDir(configDirectory, profile string) (*VersionOutput, error) {
if chartVersion, err := getChartVersion(filepath.Join(configDirectory, "transformers", "qseokversion.yaml"), "qliksense"); err != nil {
return nil, err
} else if kuzManifest, err := executeKustomizeBuildWithStdoutProgress(filepath.Join(configDirectory, "manifests", profile)); err != nil {
return nil, err
} else if images, err := getImageList(kuzManifest); err != nil {
return nil, err
} else {
return &VersionOutput{
QliksenseVersion: chartVersion,
Images: images,
}, nil
}
}
func (q *Qliksense) getConfigDirectory(gitUrl, gitRef, profileEntered string) (dir string, isTemporary bool, profile string, err error) {
profile = profileEntered
if profile == "" {
profile = defaultProfile
}
if gitRef != "" {
if dir, err = downloadFromGitRepoToTmpDir(gitUrl, gitRef); err != nil {
if dir, err = DownloadFromGitRepoToTmpDir(gitUrl, gitRef); err != nil {
return "", false, "", err
} else {
return dir, true, profile, nil
@@ -113,25 +109,26 @@ func getConfigDirectory(gitUrl, gitRef, profileEntered string) (dir string, isTe
return dir, false, profile, nil
}
var profileFromCR string
exists, dir, profileFromCR, err = configExistsInCR()
var profileFromCurrentContext string
exists, dir, profileFromCurrentContext, err = q.configExistsInCurrentContext()
if err != nil {
return "", false, "", err
} else if exists {
if profileEntered == "" {
profile = profileFromCR
profile = profileFromCurrentContext
}
return dir, false, profile, nil
}
if dir, err = downloadFromGitRepoToTmpDir(gitUrl, "master"); err != nil {
if dir, err = DownloadFromGitRepoToTmpDir(gitUrl, "master"); err != nil {
return "", false, "", err
} else {
return dir, true, profile, nil
}
}
func downloadFromGitRepoToTmpDir(gitUrl, gitRef string) (string, error) {
//DownloadFromGitRepoToTmpDir download git repo to a temporary directory
func DownloadFromGitRepoToTmpDir(gitUrl, gitRef string) (string, error) {
if tmpDir, err := ioutil.TempDir("", ""); err != nil {
return "", err
} else {
@@ -164,8 +161,15 @@ func configExistsInCurrentDirectory(profile string) (exists bool, currentDirecto
return exists, currentDirectory, err
}
func configExistsInCR() (exists bool, directory string, profile string, err error) {
return exists, directory, profile, err
func (q *Qliksense) configExistsInCurrentContext() (exists bool, directory string, profile string, err error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if currentCr, err := qConfig.GetCurrentCR(); err != nil {
return false, "", "", err
} else if currentCr.Spec.ManifestsRoot == "" {
return false, "", "", nil
} else {
return true, currentCr.Spec.GetManifestsRoot(), currentCr.Spec.Profile, nil
}
}
func getImageList(yamlContent []byte) ([]string, error) {
@@ -190,8 +194,10 @@ func getImageList(yamlContent []byte) ([]string, error) {
})
}
var sortedImageList []string
for image, _ := range imageMap {
for image, v := range imageMap {
sortedImageList = append(sortedImageList, image)
// a warning "simplify range expression" if written like this 'for image _ :=range imageMap'
_ = v
}
sort.Strings(sortedImageList)
return sortedImageList, nil

View File

@@ -2,8 +2,11 @@ package qliksense
import (
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"testing"
)
@@ -227,15 +230,24 @@ spec:
}
}
func Test_foo(t *testing.T) {
configDir := "C:\\Users\\abulynko\\AppData\\Local\\Temp\\03994898/repo"
fmt.Printf("--AB: 1: %v\n", path.Dir(configDir))
sub := path.Dir(configDir)
fmt.Printf("--AB: 2: %v\n", path.Dir(sub))
}
func Test_About_getConfigDirectory(t *testing.T) {
verifyAsdBranch := func(configDir string) (ok bool, reason string) {
tmpDir := os.TempDir()
if (path.Clean(path.Dir(path.Dir(configDir))) != path.Clean(tmpDir)) || (path.Base(configDir) != "repo") {
configParentDir := filepath.Dir(configDir)
if (filepath.Clean(filepath.Dir(configParentDir)) != filepath.Clean(tmpDir)) || (filepath.Base(configDir) != "repo") {
return false, fmt.Sprintf("expected config directory path: %v to be under: %v and terminate with repo", configDir, tmpDir)
}
if info, err := os.Stat(path.Join(configDir, "asdczxc")); err != nil || !info.Mode().IsRegular() {
if info, err := os.Stat(filepath.Join(configDir, "asdczxc")); err != nil || !info.Mode().IsRegular() {
return false, fmt.Sprintf(`expected to find file: "asdczxc" under directory: %v`, configDir)
}
return true, ""
@@ -244,15 +256,16 @@ func Test_About_getConfigDirectory(t *testing.T) {
verifyMasterBranch := func(configDir string) (ok bool, reason string) {
tmpDir := os.TempDir()
if (path.Clean(path.Dir(path.Dir(configDir))) != path.Clean(tmpDir)) || (path.Base(configDir) != "repo") {
configParentDir := filepath.Dir(configDir)
if (filepath.Clean(filepath.Dir(configParentDir)) != filepath.Clean(tmpDir)) || (filepath.Base(configDir) != "repo") {
return false, fmt.Sprintf("expected config directory path: %v to be under: %v and terminate with repo", configDir, tmpDir)
}
if _, err := os.Stat(path.Join(configDir, "asdczxc")); err == nil || !os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(configDir, "asdczxc")); err == nil || !os.IsNotExist(err) {
return false, fmt.Sprintf(`expected to NOT find file: "asdczxc"" under directory: %v`, configDir)
}
if info, err := os.Stat(path.Join(configDir, "sad")); err != nil || !info.Mode().IsRegular() {
if info, err := os.Stat(filepath.Join(configDir, "sad")); err != nil || !info.Mode().IsRegular() {
return false, fmt.Sprintf(`expected to find file: "sad"" under directory: %v`, configDir)
}
return true, ""
@@ -260,13 +273,13 @@ func Test_About_getConfigDirectory(t *testing.T) {
var testCases = []struct {
name string
setup func(t *testing.T) (gitUrl, gitRef, profileEntered string)
verify func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error)
cleanup func(configDir string) error
setup func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string)
verify func(q *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error)
cleanup func(q *Qliksense, configDir string) error
}{
{
name: "config in current directory and default profile",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
currentDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("error obtaining current directory: %v\n", err)
@@ -277,9 +290,9 @@ func Test_About_getConfigDirectory(t *testing.T) {
if err != nil {
t.Fatalf("error making path: %v, err: %v\n", defaultProfilePath, err)
}
return "no-clone-for-you", "", ""
return &Qliksense{}, "no-clone-for-you", "", ""
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
currentDirectory, err := os.Getwd()
if err != nil {
return false, "", err
@@ -299,10 +312,10 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(_ *Qliksense, configDir string) error {
if currentDirectory, err := os.Getwd(); err != nil {
return err
} else if err := os.RemoveAll(path.Join(currentDirectory, "manifests")); err != nil {
} else if err := os.RemoveAll(filepath.Join(currentDirectory, "manifests")); err != nil {
return err
}
return nil
@@ -310,21 +323,21 @@ func Test_About_getConfigDirectory(t *testing.T) {
},
{
name: "config in current directory and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
currentDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("error obtaining current directory: %v\n", err)
}
profileEntered = "foo"
defaultProfilePath := path.Join(currentDirectory, "manifests", profileEntered)
defaultProfilePath := filepath.Join(currentDirectory, "manifests", profileEntered)
err = os.MkdirAll(defaultProfilePath, os.ModePerm)
if err != nil {
t.Fatalf("error making path: %v, err: %v\n", defaultProfilePath, err)
}
return "no-clone-for-you", "", profileEntered
return &Qliksense{}, "no-clone-for-you", "", profileEntered
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
currentDirectory, err := os.Getwd()
if err != nil {
return false, "", err
@@ -344,10 +357,10 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(_ *Qliksense, configDir string) error {
if currentDirectory, err := os.Getwd(); err != nil {
return err
} else if err := os.RemoveAll(path.Join(currentDirectory, "manifests")); err != nil {
} else if err := os.RemoveAll(filepath.Join(currentDirectory, "manifests")); err != nil {
return err
}
return nil
@@ -355,10 +368,10 @@ func Test_About_getConfigDirectory(t *testing.T) {
},
{
name: "config downloaded from git based on specific git ref and default profile used",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "asd", ""
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
return &Qliksense{}, "https://github.com/test/HelloWorld", "asd", ""
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyAsdBranch(configDir)
if !ok {
return ok, reason, nil
@@ -374,11 +387,11 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(_ *Qliksense, configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
tmpTmpDir := filepath.Dir(configDir)
if filepath.Clean(filepath.Dir(tmpTmpDir)) == filepath.Clean(tmpDir) && filepath.Base(configDir) == "repo" {
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
@@ -388,10 +401,10 @@ func Test_About_getConfigDirectory(t *testing.T) {
},
{
name: "config downloaded from git based on specific git ref and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "asd", "foo"
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
return &Qliksense{}, "https://github.com/test/HelloWorld", "asd", "foo"
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyAsdBranch(configDir)
if !ok {
return ok, reason, nil
@@ -407,11 +420,11 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(_ *Qliksense, configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
tmpTmpDir := filepath.Dir(configDir)
if filepath.Clean(filepath.Dir(tmpTmpDir)) == filepath.Clean(tmpDir) && filepath.Base(configDir) == "repo" {
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
@@ -421,10 +434,21 @@ func Test_About_getConfigDirectory(t *testing.T) {
},
{
name: "config downloaded from git from master branch and default profile used",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "", ""
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
if qliksenseHome, err := ioutil.TempDir("", ""); err != nil {
t.Fatalf("error creating tmp qliksenseHome directory: %v\n", err)
return nil, "", "", ""
} else {
q := &Qliksense{QliksenseHome: qliksenseHome}
if err := q.SetUpQliksenseDefaultContext(); err != nil {
t.Fatalf("error setting up default context in the tmp dir: %v\n", err)
return nil, "", "", ""
} else {
return q, "https://github.com/test/HelloWorld", "", ""
}
}
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyMasterBranch(configDir)
if !ok {
return ok, reason, nil
@@ -440,24 +464,38 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(q *Qliksense, configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
tmpTmpDir := filepath.Dir(configDir)
if filepath.Clean(filepath.Dir(tmpTmpDir)) == filepath.Clean(tmpDir) && filepath.Base(configDir) == "repo" {
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
if err := os.RemoveAll(q.QliksenseHome); err != nil {
return err
}
return nil
},
},
{
name: "config downloaded from git from master branch and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "", "foo"
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
if qliksenseHome, err := ioutil.TempDir("", ""); err != nil {
t.Fatalf("error creating tmp qliksenseHome directory: %v\n", err)
return nil, "", "", ""
} else {
q := &Qliksense{QliksenseHome: qliksenseHome}
if err := q.SetUpQliksenseDefaultContext(); err != nil {
t.Fatalf("error setting up default context in the tmp dir: %v\n", err)
return nil, "", "", ""
} else {
return q, "https://github.com/test/HelloWorld", "", "foo"
}
}
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
verify: func(_ *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyMasterBranch(configDir)
if !ok {
return ok, reason, nil
@@ -473,31 +511,83 @@ func Test_About_getConfigDirectory(t *testing.T) {
return true, "", nil
},
cleanup: func(configDir string) error {
cleanup: func(q *Qliksense, configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
tmpTmpDir := filepath.Dir(configDir)
if filepath.Clean(filepath.Dir(tmpTmpDir)) == filepath.Clean(tmpDir) && filepath.Base(configDir) == "repo" {
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
if err := os.RemoveAll(q.QliksenseHome); err != nil {
return err
}
return nil
},
},
{
name: "config loaded from current context",
setup: func(t *testing.T) (q *Qliksense, gitUrl, gitRef, profileEntered string) {
if qliksenseHome, err := ioutil.TempDir("", ""); err != nil {
t.Fatalf("error creating tmp qliksenseHome directory: %v\n", err)
return nil, "", "", ""
} else {
q := &Qliksense{QliksenseHome: qliksenseHome}
if err := q.SetUpQliksenseDefaultContext(); err != nil {
t.Fatalf("error setting up default context in the tmp dir: %v\n", err)
return nil, "", "", ""
} else if qConfig, err := qapi.NewQConfigE(q.QliksenseHome); err != nil {
t.Fatalf("cannot initiallize qConfig: %v\n", err)
return nil, "", "", ""
} else if !qConfig.IsRepoExistForCurrent("master") {
if err := q.FetchQK8s("master"); err != nil {
t.Fatalf("error fetching master config to the tmp dir: %v\n", err)
return nil, "", "", ""
}
}
return q, "no-git-clone-for-you", "", ""
}
},
verify: func(q *Qliksense, configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
expectedConfigDir := qConfig.BuildRepoPath("master")
if configDir != expectedConfigDir {
return false, fmt.Sprintf("expected configDir to be %v", expectedConfigDir), nil
}
if isTemporary {
return false, "expected isTemporary to be false", nil
}
if profile != "docker-desktop" {
return false, fmt.Sprintf("expected profile to be: docker-desktop, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(q *Qliksense, configDir string) error {
if err := os.RemoveAll(q.QliksenseHome); err != nil {
return err
}
return nil
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
configDirectory, isTemporary, profile, err := getConfigDirectory(testCase.setup(t))
q, gitUrl, gitRef, profileEntered := testCase.setup(t)
configDirectory, isTemporary, profile, err := q.getConfigDirectory(gitUrl, gitRef, profileEntered)
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
if ok, reason, err := testCase.verify(configDirectory, isTemporary, profile); err != nil {
if ok, reason, err := testCase.verify(q, configDirectory, isTemporary, profile); err != nil {
t.Fatalf("unexpected verification error: %v\n", err)
} else if !ok {
t.Fatalf("verification failed: %v\n", reason)
} else if err := testCase.cleanup(configDirectory); err != nil {
} else if err := testCase.cleanup(q, configDirectory); err != nil {
t.Fatalf("unexpected cleanup error: %v\n", err)
}
})

61
pkg/qliksense/apply.go Normal file
View File

@@ -0,0 +1,61 @@
package qliksense
import (
"fmt"
"io"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) ApplyCRFromReader(r io.Reader, opts *InstallCommandOptions, keepPatchFiles, overwriteExistingContext, pull, push bool) error {
if err := q.LoadCr(r, overwriteExistingContext); err != nil {
return err
}
qConfig := qapi.NewQConfig(q.QliksenseHome)
cr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
version := cr.GetLabelFromCr("version")
if pull {
fmt.Println("Pulling images...")
if err := q.PullImages(version, ""); err != nil {
return err
}
}
if push {
fmt.Println("Pushing images...")
if err := q.PushImagesForCurrentCR(); err != nil {
return err
}
}
if IsQliksenseInstalled(cr.GetName()) {
// it is needed in case want to upgrade from one version to another
if cr.Spec.ManifestsRoot == "" && cr.Spec.Git == nil {
if !qConfig.IsRepoExistForCurrent(version) {
if err := q.FetchQK8s(version); err != nil {
return err
}
}
}
return q.UpgradeQK8s(keepPatchFiles)
}
return q.InstallQK8s(version, opts, keepPatchFiles)
}
func IsQliksenseInstalled(crName string) bool {
args := []string{
"get",
"qliksense",
crName,
"-ogo-template",
`--template='{{ .metadata.name}}'`,
}
_, err := qapi.KubectlDirectOps(args, "")
if err != nil {
return false
}
return true
}

View File

@@ -1,16 +1,28 @@
package qliksense
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/mitchellh/go-homedir"
"github.com/qlik-oss/k-apis/pkg/cr"
"github.com/qlik-oss/sense-installer/pkg/api"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"gopkg.in/yaml.v2"
)
const (
Q_INIT_CRD_PATH = "manifests/base/manifests/qliksense-init"
Q_INIT_CRD_PATH = "manifests/base/crds"
agreementTempalte = `
Please read the agreement at https://www.qlik.com/us/legal/license-terms
Accept the end user license agreement by providing acceptEULA=yes
`
)
func (q *Qliksense) ConfigApplyQK8s() error {
@@ -22,33 +34,67 @@ func (q *Qliksense) ConfigApplyQK8s() error {
fmt.Println("cannot get the current-context cr", err)
return err
}
return applyConfigToK8s(qcr)
// check if acceptEULA is yes or not
if !qcr.IsEULA() {
return errors.New(agreementTempalte + "\nPlease do $ qliksense config set-configs qliksense.acceptEULA=yes\n")
}
// create patch dependent resoruces
fmt.Println("Installing resoruces used kuztomize patch")
if err := q.createK8sResoruceBeforePatch(qcr); err != nil {
return err
}
if qcr.Spec.Git.Repository != "" {
// fetching and applying manifest will be in the operator controller
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
return q.applyCR(dcr)
}
}
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
return q.applyConfigToK8s(dcr)
}
}
func applyConfigToK8s(qcr *qapi.QliksenseCR) error {
// apply qliksense-init crd first
mroot := qcr.Spec.GetManifestsRoot()
qInitMsPath := filepath.Join(mroot, Q_INIT_CRD_PATH)
func (q *Qliksense) configEjson() error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if ejsonKeyDir, err := qConfig.GetCurrentContextEjsonKeyDir(); err != nil {
return err
} else if err := os.Unsetenv("EJSON_KEY"); err != nil {
return err
} else if err := os.Setenv("EJSON_KEYDIR", ejsonKeyDir); err != nil {
return err
}
return nil
}
qInitByte, err := executeKustomizeBuild(qInitMsPath)
func (q *Qliksense) applyConfigToK8s(qcr *qapi.QliksenseCR) error {
if qcr.Spec.RotateKeys != "None" {
if err := q.configEjson(); err != nil {
return err
}
}
userHomeDir, err := homedir.Dir()
if err != nil {
fmt.Println("cannot generate crds for qliksense-init", err)
fmt.Printf(`error fetching user's home directory: %v\n`, err)
return err
}
if err = qapi.KubectlApply(string(qInitByte)); err != nil {
return err
}
fmt.Println("Manifests root: " + qcr.Spec.GetManifestsRoot())
qcr.SetNamespace(qapi.GetKubectlNamespace())
// generate patches
cr.GeneratePatches(qcr.Spec)
cr.GeneratePatches(&qcr.KApiCr, path.Join(userHomeDir, ".kube", "config"))
// apply generated manifests
profilePath := filepath.Join(qcr.Spec.ManifestsRoot, qcr.Spec.Profile)
mByte, err := executeKustomizeBuild(profilePath)
profilePath := filepath.Join(qcr.Spec.GetManifestsRoot(), qcr.Spec.GetProfileDir())
mByte, err := ExecuteKustomizeBuild(profilePath)
if err != nil {
fmt.Println("cannot generate manifests for "+profilePath, err)
return err
}
if err = qapi.KubectlApply(string(mByte)); err != nil {
if err = qapi.KubectlApply(string(mByte), qcr.GetNamespace()); err != nil {
return err
}
@@ -56,27 +102,120 @@ func applyConfigToK8s(qcr *qapi.QliksenseCR) error {
}
func (q *Qliksense) ConfigViewCR() error {
//get the current context cr
r, err := q.getCurrentCRString()
if err != nil {
return err
}
fmt.Println(r)
oth, err := q.getCurrentCrDependentResourceAsString()
if err != nil {
return err
}
fmt.Println(r + "\n" + oth)
return nil
}
func (q *Qliksense) getCurrentCRString() (string, error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
return q.getCRString(qConfig.Spec.CurrentContext)
}
func (q *Qliksense) getCRString(contextName string) (string, error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCR(contextName)
if err != nil {
fmt.Println("cannot get the current-context cr", err)
fmt.Println("cannot get the context cr", err)
return "", err
}
out, err := yaml.Marshal(qcr)
out, err := qapi.K8sToYaml(qcr)
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return "", err
}
return string(out), nil
}
func (q *Qliksense) getCurrentCrDependentResourceAsString() (string, error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCR(qConfig.Spec.CurrentContext)
if err != nil {
fmt.Println("cannot get the context cr", err)
return "", err
}
var crString strings.Builder
for svcName, v := range qcr.Spec.Secrets {
hasFile := false
for _, item := range v {
if item.ValueFrom != nil && item.ValueFrom.SecretKeyRef != nil {
hasFile = true
break
}
}
if hasFile {
secretFilePath := filepath.Join(q.QliksenseHome, QliksenseContextsDir, qcr.GetName(), QliksenseSecretsDir, svcName+".yaml")
if api.FileExists(secretFilePath) {
secretFile, err := ioutil.ReadFile(secretFilePath)
if err != nil {
return "", err
}
crString.WriteString("\n---\n")
crString.Write(secretFile)
}
}
}
crString.WriteString("\n---\n")
return crString.String(), nil
}
func (q *Qliksense) EditCR(contextName string) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if contextName == "" {
cr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
contextName = cr.GetName()
}
crFilePath := qConfig.GetCRFilePath(contextName)
tempFile, err := ioutil.TempFile("", "*.yaml")
if err != nil {
return err
}
crContent, err := ioutil.ReadFile(crFilePath)
if err != nil {
return err
}
if err := ioutil.WriteFile(tempFile.Name(), crContent, os.ModePerm); err != nil {
return nil
}
cmd := exec.Command(getKubeEditorTool(), tempFile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err = cmd.Run()
if err != nil {
return err
}
newCr, err := qapi.GetCRObject(tempFile.Name())
if err != nil {
return errors.New("cannot save the cr. Someting wrong in the file format. It is not saved\n" + err.Error())
}
oldCr, err := qapi.GetCRObject(crFilePath)
if oldCr.GetName() != newCr.GetName() {
return errors.New("cr name cannot be chagned")
}
if newCr.Validate() {
return qConfig.WriteCR(newCr)
}
return nil
}
func getKubeEditorTool() string {
editor := os.Getenv("KUBE_EDITOR")
if editor == "" {
editor = "vim"
}
return editor
}

View File

@@ -0,0 +1,24 @@
package qliksense
import (
"fmt"
"log"
"strings"
)
func AskForConfirmation(s string) bool {
for {
fmt.Printf("%s [y/n]: ", s)
var response string
_, err := fmt.Scanln(&response)
if err != nil {
log.Fatal(err)
}
if strings.EqualFold(response, "y") || strings.EqualFold(response, "yes") {
return true
} else if strings.EqualFold(response, "n") || strings.EqualFold(response, "no") {
return false
}
}
}

View File

@@ -0,0 +1,643 @@
package qliksense
import (
"errors"
"fmt"
"io"
"github.com/qlik-oss/k-apis/pkg/config"
"github.com/robfig/cron/v3"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"text/tabwriter"
b64 "encoding/base64"
ansi "github.com/mattn/go-colorable"
"github.com/qlik-oss/sense-installer/pkg/api"
"github.com/ttacon/chalk"
_ "gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// Below are some constants to support qliksense context setup
QliksenseConfigFile = "config.yaml"
QliksenseContextsDir = "contexts"
DefaultQliksenseContext = "qlik-default"
MaxContextNameLength = 17
QliksenseSecretsDir = "secrets"
imageRegistryConfigKey = "imageRegistry"
pullSecretName = "artifactory-docker-secret"
qliksenseOperatorImageRepo = "qlik-docker-oss.bintray.io"
qliksenseOperatorImageName = "qliksense-operator"
)
func (q *Qliksense) SetSecretsFromReader(arg string, reader io.Reader, createSecret, base64Encoded bool) error {
//take only name from the arguments, value should be from reader
argName := strings.SplitN(arg, "=", 1)
if len(argName) != 1 {
return errors.New("can only have one argument from pipe")
}
valueBytes, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
return q.SetSecrets([]string{argName[0] + "=" + string(valueBytes)}, createSecret, base64Encoded)
}
// SetSecrets - set-secrets <key>=<value> commands
func (q *Qliksense) SetSecrets(args []string, isSecretSet bool, base64Encoded bool) error {
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
// Metadata name in qliksense CR is the name of the current context
api.LogDebugMessage("Current context: %s", qliksenseCR.GetName())
encryptionKey, err := qConfig.GetEncryptionKeyForCurrent()
if err != nil {
return err
}
resultArgs, err := api.ProcessConfigArgs(args, base64Encoded)
if err != nil {
return err
}
for _, ra := range resultArgs {
api.LogDebugMessage("value args to be encrypted: %s", ra.Value)
if err := q.processSecret(ra, encryptionKey, qliksenseCR, isSecretSet); err != nil {
return err
}
}
// write modified content into context-yaml
return qConfig.WriteCR(qliksenseCR)
}
func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, encryptionKey string, qliksenseCR *api.QliksenseCR, isSecretSet bool) error {
cipherText, e2 := api.EncryptData([]byte(ra.Value), encryptionKey)
if e2 != nil {
return e2
}
secretName := ""
if isSecretSet {
secretFolder := qliksenseCR.GetK8sSecretsFolder(q.QliksenseHome)
secretFileName := filepath.Join(secretFolder, ra.SvcName+".yaml")
secretName = fmt.Sprintf("%s-%s-%s", qliksenseCR.GetName(), ra.SvcName, "senseinstaller")
api.LogDebugMessage("Constructed secret name: %s", secretName)
k8sSecret := v1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
},
Type: v1.SecretTypeOpaque,
}
if !api.DirExists(secretFolder) {
if err := os.MkdirAll(secretFolder, os.ModePerm); err != nil {
err = fmt.Errorf("Not able to create %s dir: %v", secretFolder, err)
log.Println(err)
return err
}
}
// if read from file errors out, we can ignore it here
_ = api.ReadFromFile(&k8sSecret, secretFileName)
if k8sSecret.Data == nil {
k8sSecret.Data = map[string][]byte{}
}
// v1.Secret does enconding, so no need to encode again
k8sSecret.Data[ra.Key] = []byte(cipherText)
// Write secret to file
k8sSecretBytes, err := api.K8sSecretToYaml(k8sSecret)
if err != nil {
api.LogDebugMessage("Error while converting K8s secret to yaml")
return err
}
if err = ioutil.WriteFile(secretFileName, k8sSecretBytes, os.ModePerm); err != nil {
api.LogDebugMessage("Error while writing K8s secret to file")
return err
}
api.LogDebugMessage("Created a Kubernetes secret")
}
base64EncodedSecret := b64.StdEncoding.EncodeToString([]byte(cipherText))
// write into CR the keyref of the secret
qliksenseCR.Spec.AddToSecrets(ra.SvcName, ra.Key, base64EncodedSecret, secretName)
return nil
}
func (q *Qliksense) SetConfigFromReader(arg string, reader io.Reader, base64Encoded bool) error {
//take only name from the arguments, value should be from reader
argName := strings.SplitN(arg, "=", 1)
if len(argName) != 1 {
return errors.New("can only have one argument from pipe")
}
valueBytes, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
return q.SetConfigs([]string{argName[0] + "=" + string(valueBytes)}, base64Encoded)
}
// SetConfigs - set-configs <key>=<value> commands
func (q *Qliksense) SetConfigs(args []string, base64Encoded bool) error {
// retieve current context from config.yaml
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
resultArgs, err := api.ProcessConfigArgs(args, base64Encoded)
if err != nil {
return err
}
for _, ra := range resultArgs {
qliksenseCR.Spec.AddToConfigs(ra.SvcName, ra.Key, ra.Value)
}
// write modified content into context.yaml
return qConfig.WriteCR(qliksenseCR)
}
func caseInsenstiveFieldByName(v reflect.Value, name string) reflect.Value {
name = strings.ToLower(name)
return v.FieldByNameFunc(func(n string) bool { return strings.ToLower(n) == name })
}
func validateCR(key string, keySub string, value string, crSpec *api.QliksenseCR) (bool, *api.QliksenseCR) {
cr := reflect.ValueOf(crSpec.Spec)
keyValid := caseInsenstiveFieldByName(reflect.Indirect(cr), key)
if !keyValid.IsValid() {
//not in main spec
fmt.Println(key, "is an invalid key")
return false, crSpec
} else if keySub == "" {
if key == "rotatekeys" {
if _, err := validateInput(value); err != nil {
return false, crSpec
}
}
}
// checks if it is git or gitops
if keySub != "" {
if !keyValid.IsNil() {
if !caseInsenstiveFieldByName(reflect.Indirect(keyValid), keySub).IsValid() {
fmt.Println(keySub, "is an invalid key")
return false, crSpec
} else {
// verify gitops enabled and gitops schedule
switch keySub {
case "schedule":
if _, err := cron.ParseStandard(value); err != nil {
fmt.Println("Please enter string with standard cron scheduling syntax ")
return false, crSpec
}
case "enabled":
if !strings.EqualFold(value, "yes") && !strings.EqualFold(value, "no") {
fmt.Println("Please use yes or no for key enabled")
return false, crSpec
}
}
}
} else {
switch key {
case "gitops":
crSpec.Spec.GitOps = &config.GitOps{}
case "git":
crSpec.Spec.Git = &config.Repo{}
}
}
}
return true, crSpec
}
// SetOtherConfigs - set profile/storageclassname/git.repository/manifestRoot commands
func (q *Qliksense) SetOtherConfigs(args []string) error {
// retieve current context from config.yaml
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
// modify appropriate fields
if len(args) == 0 {
err := fmt.Errorf("No args were provided. Please provide args to configure the current context")
log.Println(err)
return err
}
for _, arg := range args {
if strings.HasPrefix(arg, "fetchSource.") {
if err := q.processSetFetchSource(arg, qliksenseCR); err != nil {
return err
}
} else if strings.HasPrefix(arg, "git.") {
if err := q.processSetGit(arg, qliksenseCR); err != nil {
return err
}
} else if strings.HasPrefix(arg, "gitOps.") {
if err := q.processSetGitOps(arg, qliksenseCR); err != nil {
return err
}
} else {
if err := processSetSingleArg(arg, qliksenseCR); err != nil {
return err
}
}
fmt.Println(chalk.Green.Color("Successfully added to Custom Resource Spec"))
}
// write modified content into context.yaml
return qConfig.WriteCR(qliksenseCR)
}
func processSetSingleArg(arg string, cr *api.QliksenseCR) error {
nv := strings.Split(arg, "=")
switch nv[0] {
case "manifestsRoot":
cr.Spec.ManifestsRoot = nv[1]
case "profile":
cr.Spec.Profile = nv[1]
case "storageClassName":
cr.Spec.StorageClassName = nv[1]
case "rotateKeys":
valid := false
for _, v := range []string{"yes", "no", "None"} {
if nv[1] == v {
valid = true
}
}
if !valid {
return errors.New("please povide rotateKeys=yes|no|None")
}
cr.Spec.RotateKeys = nv[1]
default:
return errors.New("Please enter one of: profile, storageClassName,rotateKeys, manifestRoot to configure the current context")
}
return nil
}
func (q *Qliksense) processSetFetchSource(arg string, cr *api.QliksenseCR) error {
args := strings.Split(arg, "=")
subs := strings.Split(args[0], ".")
if cr.Spec.FetchSource == nil {
cr.Spec.FetchSource = &config.Repo{}
}
switch subs[1] {
case "repository":
cr.Spec.FetchSource.Repository = args[1]
case "accessToken":
qConfig := api.NewQConfig(q.QliksenseHome)
key, err := qConfig.GetEncryptionKeyFor(cr.GetName())
if err != nil {
return err
}
return cr.SetFetchAccessToken(args[1], key)
case "secretName":
cr.Spec.FetchSource.SecretName = args[1]
case "userName":
cr.Spec.FetchSource.UserName = args[1]
default:
return errors.New(arg + " does not match any cr spec")
}
return nil
}
func (q *Qliksense) processSetGit(arg string, cr *api.QliksenseCR) error {
args := strings.Split(arg, "=")
subs := strings.Split(args[0], ".")
if cr.Spec.Git == nil {
cr.Spec.Git = &config.Repo{}
}
switch subs[1] {
case "repository":
cr.Spec.Git.Repository = args[1]
case "accessToken":
cr.Spec.Git.AccessToken = args[1]
case "secretName":
cr.Spec.Git.SecretName = args[1]
case "userName":
cr.Spec.Git.UserName = args[1]
default:
return errors.New(arg + " does not match any cr spec")
}
return nil
}
func (q *Qliksense) processSetGitOps(arg string, cr *api.QliksenseCR) error {
args := strings.Split(arg, "=")
subs := strings.Split(args[0], ".")
if cr.Spec.Git == nil {
cr.Spec.GitOps = &config.GitOps{}
}
switch subs[1] {
case "enabled":
if args[1] != "yes" && args[1] != "no" {
return errors.New("Please use yes or no for key enabled")
}
cr.Spec.GitOps.Enabled = args[1]
case "schedule":
if _, err := cron.ParseStandard(args[1]); err != nil {
return errors.New("Please enter string with standard cron scheduling syntax ")
}
cr.Spec.GitOps.Schedule = args[1]
case "watchBranch":
cr.Spec.GitOps.WatchBranch = args[1]
case "image":
cr.Spec.GitOps.Image = args[1]
default:
return errors.New(arg + " does not match any cr spec")
}
return nil
}
// SetContextConfig - set the context for qliksense kubernetes resources to live in
func (q *Qliksense) SetContextConfig(args []string) error {
if len(args) == 1 {
err := q.SetUpQliksenseContext(args[0])
if err != nil {
return err
}
} else {
err := fmt.Errorf("Please provide a name to configure the context with")
log.Println(err)
return err
}
return nil
}
func (q *Qliksense) ListContextConfigs() error {
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
var qliksenseConfig api.QliksenseConfig
if err := api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return err
}
out := ansi.NewColorableStdout()
w := tabwriter.NewWriter(out, 5, 8, 0, '\t', 0)
fmt.Fprintln(w, chalk.Underline.TextStyle("Context Name"), "\t", chalk.Underline.TextStyle("CR File Location"))
w.Flush()
if len(qliksenseConfig.Spec.Contexts) > 0 {
for _, cont := range qliksenseConfig.Spec.Contexts {
fmt.Fprintln(w, cont.Name, "\t", qliksenseConfig.GetCRFilePath(cont.Name), "\t")
}
w.Flush()
fmt.Fprintln(out, "")
fmt.Fprintln(out, chalk.Bold.TextStyle("Current Context : "), qliksenseConfig.Spec.CurrentContext)
} else {
fmt.Fprintln(out, "No Contexts Available")
}
return nil
}
func (q *Qliksense) DeleteContextConfig(args []string, flag bool) error {
if len(args) == 1 {
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
var qliksenseConfig api.QliksenseConfig
api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile)
out := ansi.NewColorableStdout()
switch args[0] {
case qliksenseConfig.Spec.CurrentContext:
fmt.Fprintln(out, chalk.Yellow.Color("Please switch contexts to be able to delete this context."))
err := fmt.Errorf(chalk.Red.Color("Cannot delete current context - %s"), chalk.White.Color(chalk.Bold.TextStyle(qliksenseConfig.Spec.CurrentContext)))
return err
case DefaultQliksenseContext:
err := fmt.Errorf(chalk.Red.Color("Cannot delete default qliksense context"))
return err
default:
qliksenseContextsDir1 := filepath.Join(q.QliksenseHome, QliksenseContextsDir)
qliksenseContextFile := filepath.Join(qliksenseContextsDir1, args[0])
qliksenseSecretsDir1 := filepath.Join(q.QliksenseHome, QliksenseSecretsDir, QliksenseContextsDir)
qliksenseSecretsFile := filepath.Join(qliksenseSecretsDir1, args[0])
if err := os.RemoveAll(qliksenseContextFile); err != nil {
err = fmt.Errorf("Not able to delete %s dir: %v", qliksenseContextsDir1, err)
log.Println(err)
return err
} else if err := os.RemoveAll(qliksenseSecretsFile); err != nil {
err = fmt.Errorf("No Secrets Folder Detected")
log.Println(err)
return err
} else {
currentLength := len(qliksenseConfig.Spec.Contexts)
if currentLength > 0 {
temp := qliksenseConfig.Spec.Contexts
qliksenseConfig.Spec.Contexts = nil
for _, ctx := range temp {
if ctx.Name != args[0] {
qliksenseConfig.Spec.Contexts = append(qliksenseConfig.Spec.Contexts, api.Context{
Name: ctx.Name,
CrFile: ctx.CrFile,
})
}
}
newLength := len(qliksenseConfig.Spec.Contexts)
if currentLength != newLength {
ans := flag
if ans == false {
ans = AskForConfirmation("Are You Sure? ")
}
if ans == true {
api.WriteToFile(&qliksenseConfig, qliksenseConfigFile)
fmt.Fprintln(out, chalk.Yellow.Color(chalk.Underline.TextStyle("Warning: Active resources may still be running in-cluster")))
fmt.Fprintln(out, chalk.Green.Color("Successfully deleted context: "), chalk.Bold.TextStyle(args[0]))
} else {
return nil
}
} else {
err := fmt.Errorf(chalk.Red.Color("Context not found"))
return err
}
}
}
}
} else {
err := fmt.Errorf("Please provide a context as an argument to delete")
log.Println(err)
return err
}
return nil
}
// SetUpQliksenseDefaultContext - to setup dir structure for default qliksense context
func (q *Qliksense) SetUpQliksenseDefaultContext() error {
if api.FileExists(filepath.Join(q.QliksenseHome, "config.yaml")) {
qliksenseConfig := api.NewQConfig(q.QliksenseHome)
if qliksenseConfig.IsContextExist(DefaultQliksenseContext) {
return nil
}
}
return q.SetUpQliksenseContext(DefaultQliksenseContext)
}
// SetUpQliksenseContext - to setup qliksense context
func (q *Qliksense) SetUpQliksenseContext(contextName string) error {
if contextName == "" {
err := fmt.Errorf("Please enter a non-empty context-name")
log.Println(err)
return err
}
// check the length of the context name entered by the user, it should not exceed 17 chars
if len(contextName) > MaxContextNameLength {
err := fmt.Errorf("Please enter a context-name with utmost 17 characters")
log.Println(err)
return err
}
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
qliksenseConfig := api.NewQConfigEmpty(q.QliksenseHome)
if !api.FileExists(qliksenseConfigFile) {
qliksenseConfig.AddBaseQliksenseConfigs(contextName)
} else {
if err := api.ReadFromFile(qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return err
}
}
if qliksenseConfig.IsContextExist(contextName) {
qliksenseConfig.Spec.CurrentContext = contextName
return qliksenseConfig.Write()
}
qliksenseCR := &api.QliksenseCR{}
qliksenseCR.AddCommonConfig(contextName)
qliksenseConfig.Spec.CurrentContext = contextName
if err := qliksenseConfig.CreateOrWriteCrAndContext(qliksenseCR); err != nil {
return err
}
// set the encrypted default mongo
return q.SetSecrets([]string{`qliksense.mongoDbUri="mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"`}, false, false)
}
func validateInput(input string) (string, error) {
var err error
validInputs := []string{"yes", "no", "None"}
isValid := false
for _, elem := range validInputs {
if input == elem {
isValid = true
break
}
}
if !isValid {
err = fmt.Errorf("Please enter one of: yes, no or None")
log.Println(err)
}
return input, err
}
// PrepareK8sSecret targetFile contains base64 encoded value of encrypted value.
// this method decodes and decrypts the secret value in the secret.yaml file and returns a B64encoded string
func (q *Qliksense) PrepareK8sSecret(targetFile string) (string, error) {
// check if targetFile exists
if !api.FileExists(targetFile) {
err := fmt.Errorf("Target file does not exist in the path provided")
log.Println(err)
return "", err
}
qConfig := api.NewQConfig(q.QliksenseHome)
encryptionKey, err := qConfig.GetEncryptionKeyForCurrent()
if err != nil {
return "", err
}
// read the target file
k8sSecret, err := readTargetfile(targetFile)
if err != nil {
return "", err
}
// retrieve value from data section
k8sSecret1, err := api.K8sSecretFromYaml(k8sSecret)
if err != nil {
return "", err
}
dataMap := k8sSecret1.Data
var resultMap = make(map[string][]byte)
for k, v := range dataMap {
//k8s secrets has already base64 decoed value
decryptedString, err := api.DecryptData(v, encryptionKey)
if err != nil {
err := fmt.Errorf("Not able to decrypt message: %v", err)
return "", err
}
resultMap[k] = []byte(decryptedString)
}
// putting the above map back into the k8sSecret struct
k8sSecret1.Data = resultMap
k8sSecretBytes, err := api.K8sSecretToYaml(k8sSecret1)
if err != nil {
return "", err
}
return string(k8sSecretBytes), nil
}
func readTargetfile(targetFile string) ([]byte, error) {
k8sSecret, err := ioutil.ReadFile(targetFile)
if err != nil {
err := fmt.Errorf("Unable to read the targetFile")
log.Println(err)
return nil, err
}
return k8sSecret, nil
}
func (q *Qliksense) SetImageRegistry(registry, pushUsername, pushPassword, pullUsername, pullPassword string) error {
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
if pushUsername != "" {
if err := qConfig.SetPushDockerConfigJsonSecret(&api.DockerConfigJsonSecret{
Uri: registry,
Username: pushUsername,
Password: pushPassword,
}); err != nil {
return err
} else if err := qConfig.SetPullDockerConfigJsonSecret(&api.DockerConfigJsonSecret{
Name: pullSecretName,
Uri: registry,
Username: pullUsername,
Password: pullPassword,
Email: pullUsername,
}); err != nil {
return err
}
} else if err := qConfig.DeletePushDockerConfigJsonSecret(); err != nil && !os.IsNotExist(err) {
return err
} else if err := qConfig.DeletePullDockerConfigJsonSecret(); err != nil && !os.IsNotExist(err) {
return err
}
qliksenseCR.Spec.AddToConfigs("qliksense", imageRegistryConfigKey, registry)
return qConfig.WriteCR(qliksenseCR)
}
func (q *Qliksense) SetEulaAccepted() error {
qConfig := api.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
if !qcr.IsEULA() {
qcr.SetEULA("yes")
return qConfig.WriteCurrentContextCR(qcr)
}
return nil
}

View File

@@ -0,0 +1,865 @@
package qliksense
import (
"encoding/base64"
b64 "encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/qlik-oss/k-apis/pkg/config"
"github.com/qlik-oss/sense-installer/pkg/api"
"gopkg.in/yaml.v2"
)
const (
testDir = "./tests"
qlikDefaultContext = "qlik-default"
secrets = "secrets"
contexts = "contexts"
)
var targetFileStringTemplate = `
apiVersion: v1
data:
mongoDbUri: %s
kind: Secret
metadata:
name: testctx-qliksense-senseinstaller
type: Opaque
`
var decText = "mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"
func setupTargetFileAndPrivateKey() (string, string, error) {
secretKeyLocation := filepath.Join(testDir, secrets, contexts, qlikDefaultContext, secrets)
if err := os.MkdirAll(secretKeyLocation, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
os.Setenv("QLIKSENSE_KEY_LOCATION", secretKeyLocation)
//privKeyFile := filepath.Join(secretKeyLocation, "user_secret_key")
key, err := api.LoadSecretKey(secretKeyLocation)
if key == "" {
key, err = api.GenerateAndStoreSecretKey(secretKeyLocation)
}
encData, _ := api.EncryptData([]byte(decText), key)
encText := b64.StdEncoding.EncodeToString(encData)
targetFileString := fmt.Sprintf(targetFileStringTemplate, encText)
targetFile := filepath.Join(testDir, "targetfile.yaml")
// tests/config.yaml exists
err = ioutil.WriteFile(targetFile, []byte(targetFileString), 0777)
if err != nil {
log.Printf("Error while creating file: %v", err)
return "", "", err
}
return targetFile, key, err
}
func setup() func() {
// create tests dir
os.RemoveAll(testDir)
if err := os.Mkdir(testDir, 0777); err != nil {
log.Printf("\nError occurred: %v", err)
}
config :=
`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: qliksenseConfig
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`
configFile := filepath.Join(testDir, "config.yaml")
// tests/config.yaml exists
ioutil.WriteFile(configFile, []byte(config), 0777)
contextYaml :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik-default
`
qlikDefaultContext := "qlik-default"
// create contexts/qlik-default/ under tests/
contexts := "contexts"
contextsDir := filepath.Join(testDir, contexts, qlikDefaultContext)
if err := os.MkdirAll(contextsDir, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextFile := filepath.Join(contextsDir, qlikDefaultContext+".yaml")
ioutil.WriteFile(contextFile, []byte(contextYaml), 0777)
tearDown := func() {
os.RemoveAll(testDir)
}
return tearDown
}
func readCRFile() (*api.QliksenseCR, error) {
qlikDefaultContext := "qlik-default"
qliksenseCR := &api.QliksenseCR{}
contextFileContents, err := ioutil.ReadFile(filepath.Join(testDir, contexts, qlikDefaultContext, qlikDefaultContext+".yaml"))
if err != nil {
log.Println(err)
err = fmt.Errorf("Not able to read current context info")
return nil, err
}
if err := yaml.Unmarshal(contextFileContents, qliksenseCR); err != nil {
err = fmt.Errorf("An error occurred during unmarshalling: %v", err)
return nil, err
}
return qliksenseCR, nil
}
func Test_retrieveCurrentContextInfo(t *testing.T) {
tearDown := setup()
defer tearDown()
q := &Qliksense{
QliksenseHome: testDir,
}
qConfig := api.NewQConfig(q.QliksenseHome)
_, err := qConfig.GetCurrentCR()
if err != nil {
t.FailNow()
}
}
func TestSetUpQliksenseContext(t *testing.T) {
type args struct {
qlikSenseHome string
contextName string
isDefaultContext bool
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid contextname",
args: args{
qlikSenseHome: testDir,
contextName: "testContext1",
isDefaultContext: false,
},
wantErr: false,
},
{
name: "invalid contextname",
args: args{
qlikSenseHome: testDir,
contextName: "testContext_abcdefgh",
isDefaultContext: false,
},
wantErr: true,
},
{
name: "empty contextname",
args: args{
qlikSenseHome: testDir,
contextName: "",
isDefaultContext: false,
},
wantErr: true,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
if err := q.SetUpQliksenseContext(tt.args.contextName); (err != nil) != tt.wantErr {
t.Errorf("SetUpQliksenseContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetUpQliksenseDefaultContext(t *testing.T) {
type args struct {
qlikSenseHome string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
qlikSenseHome: testDir,
},
wantErr: false,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
if err := q.SetUpQliksenseDefaultContext(); (err != nil) != tt.wantErr {
t.Errorf("SetUpQliksenseDefaultContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetOtherConfigs(t *testing.T) {
type args struct {
q *Qliksense
args []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"profile=minikube", "rotateKeys=yes", "storageClassName=efs", "gitOps.enabled=yes", "gitOps.schedule=30 * * * *", "git.repository=master", "git.userName=foo", "git.accessToken=1234"},
},
wantErr: false,
},
{
name: "invalid configs",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"someconfig=somevalue, gitOps.schedule=bar", "gitOps.enabled=bar", "git.foo=bar", "rotateKeys=bar"},
},
wantErr: true,
},
{
name: "empty configs",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{},
},
wantErr: true,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.args.q.SetOtherConfigs(tt.args.args); (err != nil) != tt.wantErr {
t.Errorf("SetOtherConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetConfigs(t *testing.T) {
type args struct {
q *Qliksense
args []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"qliksense.acceptEULA=\"yes\"", "qliksense.mongoDbUri=\"mongo://mongo:3307\""},
},
wantErr: false,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.args.q.SetConfigs(tt.args.args, false); (err != nil) != tt.wantErr {
t.Errorf("SetConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetImageRegistry(t *testing.T) {
getQlikSense := func(tmpQlikSenseHome string) (*Qliksense, error) {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`), os.ModePerm); err != nil {
return nil, err
}
defaultContextDir := path.Join(tmpQlikSenseHome, "contexts", "qlik-default")
if err := os.MkdirAll(defaultContextDir, os.ModePerm); err != nil {
return nil, err
}
version := "foo"
manifestsRootDir := fmt.Sprintf("%s/repo/%s", defaultContextDir, version)
if err := ioutil.WriteFile(path.Join(defaultContextDir, "qlik-default.yaml"), []byte(fmt.Sprintf(`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
labels:
version: %s
spec:
profile: docker-desktop
manifestsRoot: %s
namespace: some-namespace
`, version, manifestsRootDir)), os.ModePerm); err != nil {
return nil, err
}
return &Qliksense{
QliksenseHome: tmpQlikSenseHome,
}, nil
}
testCases := []struct {
name string
registry string
pushUsername string
pushPassword string
pullUsername string
pullPassword string
expectSecretsExist bool
}{
{
name: "no auth",
registry: "foobar",
pushUsername: "",
pushPassword: "",
pullUsername: "",
pullPassword: "",
expectSecretsExist: false,
},
{
name: "auth",
registry: "foobar",
pushUsername: "foo-push",
pushPassword: "bar-push",
pullUsername: "foo-pull",
pullPassword: "bar-pull",
expectSecretsExist: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
tmpQlikSenseHome, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.RemoveAll(tmpQlikSenseHome)
q, err := getQlikSense(tmpQlikSenseHome)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := q.SetImageRegistry(testCase.registry, testCase.pushUsername, testCase.pushPassword,
testCase.pullUsername, testCase.pullPassword); err != nil {
t.Fatalf("unexpected error: %v", err)
}
qConfig := api.NewQConfig(q.QliksenseHome)
if testCase.expectSecretsExist {
if pushSecret, err := qConfig.GetPushDockerConfigJsonSecret(); err != nil {
t.Fatalf("unexpected error: %v", err)
} else if pushSecret.Uri != testCase.registry ||
pushSecret.Username != testCase.pushUsername || pushSecret.Password != testCase.pushPassword {
t.Fatalf("unexpected push secret content: %v", pushSecret)
}
if pullSecret, err := qConfig.GetPullDockerConfigJsonSecret(); err != nil {
t.Fatalf("unexpected error: %v", err)
} else if pullSecret.Uri != testCase.registry ||
pullSecret.Name != "artifactory-docker-secret" ||
pullSecret.Username != testCase.pullUsername || pullSecret.Password != testCase.pullPassword {
t.Fatalf("unexpected pull secret content: %v", pullSecret)
}
} else {
if _, err := qConfig.GetPushDockerConfigJsonSecret(); err == nil {
t.Fatal("unexpected image-registry-push-secret.yaml")
} else if _, err := qConfig.GetPullDockerConfigJsonSecret(); err == nil {
t.Fatal("unexpected image-registry-pull-secret.yaml")
}
}
})
}
}
func removePrivateKey() {
err := os.Remove(filepath.Join(testDir, secrets, contexts, qlikDefaultContext, secrets, "user_secret_key"))
if err != nil {
log.Fatalf("Could not delete private key %v", err)
}
return
}
func Test_PrepareK8sSecret(t *testing.T) {
type fields struct {
QliksenseHome string
}
tests := []struct {
name string
fields fields
want string
wantErr bool
setup func() (string, func())
}{
{
name: "valid case",
fields: fields{
QliksenseHome: testDir,
},
want: fmt.Sprintf(targetFileStringTemplate, base64.StdEncoding.EncodeToString([]byte(decText))),
wantErr: false,
setup: func() (string, func()) {
tearDown := setup()
targetFile, _, _ := setupTargetFileAndPrivateKey()
return targetFile, tearDown
},
},
{
name: "private key not supplied should result in decryption error",
fields: fields{
QliksenseHome: testDir,
},
want: "",
wantErr: true,
setup: func() (string, func()) {
tearDown := setup()
targetFile, _, _ := setupTargetFileAndPrivateKey()
removePrivateKey()
return targetFile, tearDown
},
},
{
name: "target file not supplied",
fields: fields{
QliksenseHome: testDir,
},
want: "",
wantErr: true,
setup: func() (string, func()) {
tearDown := setup()
setupTargetFileAndPrivateKey()
return "", tearDown
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
targetFile, tearDown := tt.setup()
q := &Qliksense{
QliksenseHome: tt.fields.QliksenseHome,
}
got, err := q.PrepareK8sSecret(targetFile)
if (err != nil) != tt.wantErr {
t.Errorf("Qliksense.PrepareK8sSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(strings.TrimSpace(got), strings.TrimSpace(tt.want)) {
t.Errorf("Qliksense.PrepareK8sSecret() = %v, want %v", got, tt.want)
}
tearDown()
})
}
}
func Test_ListContextConfigs(t *testing.T) {
type fields struct {
QliksenseHome string
}
tests := []struct {
name string
fields fields
wantErr bool
setup func() (string, func())
}{
{
name: "valid case",
fields: fields{
QliksenseHome: testDir,
},
wantErr: false,
setup: func() (string, func()) {
tearDown := setup()
return "", tearDown
},
},
{
name: "config yaml does not exist",
fields: fields{
QliksenseHome: testDir,
},
wantErr: true,
setup: func() (string, func()) {
return "", func() {}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, tearDown := tt.setup()
q := &Qliksense{
QliksenseHome: tt.fields.QliksenseHome,
}
if err := q.ListContextConfigs(); (err != nil) != tt.wantErr {
t.Errorf("ListContextConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
tearDown()
})
}
}
func Test_SetSecrets(t *testing.T) {
type fields struct {
QliksenseHome string
}
type args struct {
args []string
isSecretSet bool
base64 bool
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "valid secret secrets=false",
fields: fields{
QliksenseHome: testDir,
},
args: args{
args: []string{"qliksense.mongoDbUri=\"mongodb://qlik-default-mongodb:27017/qliksense?ssl=false\""},
isSecretSet: false,
},
wantErr: false,
},
{
name: "valid secret secrets=false base64 encoded",
fields: fields{
QliksenseHome: testDir,
},
args: args{
args: []string{"qliksense.mongoDbUri=bW9uZ29kYjovL3FsaWstZGVmYXVsdC1tb25nb2RiOjI3MDE3L3FsaWtzZW5zZT9zc2w9ZmFsc2U="},
isSecretSet: false,
base64: true,
},
wantErr: false,
},
{
name: "test1 valid secret secrets=true",
fields: fields{
QliksenseHome: testDir,
},
args: args{
args: []string{"qliksense.mongoDbUri=\"mongo://mongo:3307\""},
isSecretSet: true,
},
wantErr: false,
},
{
name: "test2 valid secret secrets=true",
fields: fields{
QliksenseHome: testDir,
},
args: args{
args: []string{"qliksense.mongoDbUri=\"mongodb://qlik-default-mongodb:27017/qliksense?ssl=false\""},
isSecretSet: true,
},
wantErr: false,
},
{
name: "invalid secret secrets=false",
fields: fields{
QliksenseHome: testDir,
},
args: args{
args: []string{},
isSecretSet: false,
},
wantErr: true,
},
}
tearDown := setup()
_, encryptionKey, err := setupTargetFileAndPrivateKey()
if err != nil {
t.FailNow()
}
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Qliksense{
QliksenseHome: tt.fields.QliksenseHome,
}
if err := q.SetSecrets(tt.args.args, tt.args.isSecretSet, tt.args.base64); (err != nil) != tt.wantErr {
t.Errorf("SetSecrets() error = %v, wantErr %v", err, tt.wantErr)
t.FailNow()
}
if tt.wantErr || len(tt.args.args) == 0 {
return
}
// VERIFICATION PART BELOW
// extract the value for testing
testValueArr := strings.SplitN(tt.args.args[0], "=", 2)
testValue := strings.ReplaceAll(testValueArr[1], "\"", "")
if tt.args.base64 {
d, _ := b64.StdEncoding.DecodeString(testValue)
testValue = strings.Trim(string(d), "\n ")
}
qliksenseCR, err := readCRFile()
if err != nil {
err = fmt.Errorf("Not able to read from context file: %v", err)
log.Println(err)
t.FailNow()
}
for svcName := range qliksenseCR.Spec.Secrets { // we are sure we only have one service
for _, v := range qliksenseCR.Spec.Secrets {
for _, item := range v { // we are sure we only have one entry
valToBeEncrypted, err := getValueToBeDecodedForSetSecrets(item, qliksenseCR, svcName)
if err != nil {
err := fmt.Errorf("Error occurred while decoding: %v", err)
log.Printf("decode error: %v", err)
t.FailNow()
}
decryptedVal, err := api.DecryptData([]byte(valToBeEncrypted), encryptionKey)
if err != nil {
err := fmt.Errorf("Error occurred while testing decryption: %v", err)
log.Printf("No Data in Secret: %v", err)
t.FailNow()
}
if string(decryptedVal) != testValue {
t.FailNow()
}
}
}
}
})
}
}
func getValueToBeDecodedForSetSecrets(item config.NameValue, qliksenseCR *api.QliksenseCR, svcName string) (string, error) {
if item.ValueFrom != nil && item.ValueFrom.SecretKeyRef != nil {
// secret=true
secretFilePath := filepath.Join(testDir, contexts, qliksenseCR.GetName(), QliksenseSecretsDir, svcName+".yaml")
if api.FileExists(secretFilePath) {
secretFileContents, err := ioutil.ReadFile(secretFilePath)
if err != nil {
err = fmt.Errorf("An error occurred during unmarshalling: %v", err)
return "", err
}
k8sSecret, err := api.K8sSecretFromYaml(secretFileContents)
if err != nil {
err = fmt.Errorf("An error occurred during unmarshalling: %v", err)
return "", err
}
if k8sSecret.Data == nil {
err = fmt.Errorf("No Data in Secret: %v", err)
return "", err
}
return string(k8sSecret.Data[item.ValueFrom.SecretKeyRef.Key]), nil
}
}
// secret=false
if item.Value != "" {
b, err := b64.RawStdEncoding.DecodeString(item.Value)
return string(b), err
}
err := fmt.Errorf("Both Value and ValueFrom are empty")
return "", err
}
func setupDeleteContext() func() {
if err := os.Mkdir(testDir, 0777); err != nil {
log.Printf("\nError occurred: %v", err)
}
config :=
`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: qliksenseConfig
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default.yaml
- name: qlik1
crFile: contexts/qlik1.yaml
- name: qlik2
crFile: contexts/qlik2.yaml
currentContext: qlik1
`
configFile := filepath.Join(testDir, "config.yaml")
// tests/config.yaml exists
ioutil.WriteFile(configFile, []byte(config), 0777)
contextYaml :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik-default
`
qlikDefaultContext := "qlik-default"
// create contexts/qlik-default/ under tests/
contexts := "contexts"
contextsDir1 := filepath.Join(testDir, contexts, qlikDefaultContext)
if err := os.MkdirAll(contextsDir1, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextYaml1 :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik1
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik1`
contextYaml2 :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik2
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik2`
contextsDir := filepath.Join(testDir, contexts, "qlik1")
if err := os.MkdirAll(contextsDir, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextsDir2 := filepath.Join(testDir, contexts, "qlik2")
if err := os.MkdirAll(contextsDir2, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextFile := filepath.Join(contextsDir, "qlik1.yaml")
ioutil.WriteFile(contextFile, []byte(contextYaml1), 0777)
contextFile2 := filepath.Join(contextsDir2, "qlik2.yaml")
ioutil.WriteFile(contextFile2, []byte(contextYaml2), 0777)
contextFile1 := filepath.Join(contextsDir1, "qlik-default.yaml")
ioutil.WriteFile(contextFile1, []byte(contextYaml), 0777)
tearDown := func() {
os.RemoveAll(testDir)
}
return tearDown
}
func TestDeleteContexts(t *testing.T) {
type args struct {
qlikSenseHome string
contextName string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik2",
},
wantErr: false,
},
{
name: "default context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik-default",
},
wantErr: true,
},
{
name: "non-existent context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik3",
},
wantErr: true,
},
{
name: "current context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik1",
},
wantErr: true,
},
}
tearDown := setupDeleteContext()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
var arg []string
arg = append(arg, tt.args.contextName)
if err := q.DeleteContextConfig(arg, true); (err != nil) != tt.wantErr {
t.Errorf("DeleteContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

112
pkg/qliksense/crds.go Normal file
View File

@@ -0,0 +1,112 @@
package qliksense
import (
"fmt"
"os"
"path/filepath"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
type CrdCommandOptions struct {
All bool
}
func (q *Qliksense) ViewCrds(opts *CrdCommandOptions) error {
//io.WriteString(os.Stdout, q.GetCRDString())
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
engineCRD, err := getQliksenseInitCrd(qcr)
if err != nil {
return err
}
customCrd, err := getCustomCrd(qcr)
if err != nil {
return nil
}
fmt.Println(engineCRD)
if customCrd != "" {
fmt.Println("---")
fmt.Println(customCrd)
}
if opts.All {
fmt.Println("---")
fmt.Printf("%s", q.GetOperatorCRDString())
}
return nil
}
func (q *Qliksense) InstallCrds(opts *CrdCommandOptions) error {
// install qliksense-init crd
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
if engineCRD, err := getQliksenseInitCrd(qcr); err != nil {
return err
} else if err = qapi.KubectlApply(engineCRD, ""); err != nil {
return err
}
if customCrd, err := getCustomCrd(qcr); err != nil {
return err
} else if customCrd != "" {
if err = qapi.KubectlApply(customCrd, ""); err != nil {
return err
}
}
if opts.All { // install opeartor crd
if err := qapi.KubectlApply(q.GetOperatorCRDString(), ""); err != nil {
fmt.Println("cannot do kubectl apply on opeartor CRD", err)
return err
}
}
return nil
}
func getQliksenseInitCrd(qcr *qapi.QliksenseCR) (string, error) {
var repoPath string
var err error
if qcr.Spec.GetManifestsRoot() != "" {
repoPath = qcr.Spec.GetManifestsRoot()
} else {
if repoPath, err = DownloadFromGitRepoToTmpDir(defaultConfigRepoGitUrl, "master"); err != nil {
return "", err
}
}
qInitMsPath := filepath.Join(repoPath, Q_INIT_CRD_PATH)
if _, err := os.Lstat(qInitMsPath); err != nil {
// older version of qliksense-init used
qInitMsPath = filepath.Join(repoPath, "manifests/base/manifests/qliksense-init")
}
qInitByte, err := ExecuteKustomizeBuild(qInitMsPath)
if err != nil {
fmt.Println("cannot generate crds for qliksense-init", err)
return "", err
}
return string(qInitByte), nil
}
func getCustomCrd(qcr *qapi.QliksenseCR) (string, error) {
crdPath := qcr.GetCustomCrdsPath()
if crdPath == "" {
return "", nil
}
qInitByte, err := ExecuteKustomizeBuild(crdPath)
if err != nil {
fmt.Println("cannot generate custom crds", err)
return "", err
}
return string(qInitByte), nil
}

View File

@@ -0,0 +1,41 @@
package qliksense
import (
"testing"
kapi_config "github.com/qlik-oss/k-apis/pkg/config"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func TestGetQliksenseInitCrd(t *testing.T) {
someTmpRepoPath, err := DownloadFromGitRepoToTmpDir(defaultConfigRepoGitUrl, "master")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
crdFromContextConfig, err := getQliksenseInitCrd(&qapi.QliksenseCR{
KApiCr: kapi_config.KApiCr{
Spec: &kapi_config.CRSpec{
ManifestsRoot: someTmpRepoPath,
},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
crdFromDownloadedConfig, err := getQliksenseInitCrd(&qapi.QliksenseCR{
KApiCr: kapi_config.KApiCr{
Spec: &kapi_config.CRSpec{
ManifestsRoot: "",
},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if crdFromContextConfig != crdFromDownloadedConfig {
t.Fatalf("expected %v to equal %v, but they didn't", crdFromContextConfig, crdFromDownloadedConfig)
}
}

View File

@@ -1,236 +1,272 @@
package qliksense
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports/alltransports"
imageTypes "github.com/containers/image/v5/types"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/registry"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"golang.org/x/net/context"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
)
// PullImages ...
func (p *Qliksense) PullImages(gitRef, profile string, engine bool) error {
var (
image, versionFile, imagesDir, homeDir string
err error
versionOut *VersionOutput
)
println("getting images list...")
type imageNameParts struct {
name string
tag string
}
// TODO: get getref and profile from config/cr for About function call
if versionOut, err = p.About(gitRef, profile); err != nil {
return err
}
const (
imagesDirName = "images"
imageIndexDirName = "index"
imageSharedBlobsDirName = "blobs"
)
if homeDir, err = homedir.Dir(); err != nil {
return err
}
imagesDir = filepath.Join(homeDir, ".qliksense", "images")
os.MkdirAll(imagesDir, os.ModePerm)
versionFile = filepath.Join(imagesDir, versionOut.QliksenseVersion)
if _, err = os.Stat(versionFile); err != nil {
if os.IsNotExist(err) {
if yamlVersion, err := yaml.Marshal(versionOut); err != nil {
return err
} else if err = ioutil.WriteFile(versionFile, yamlVersion, os.ModePerm); err != nil {
func (q *Qliksense) PullImages(version, profile string) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if version != "" {
if !qConfig.IsRepoExistForCurrent(version) {
if err := q.FetchQK8s(version); err != nil {
return err
}
} else {
return errors.Errorf("Unable to determine About file %v exists", versionFile)
}
}
for _, image = range versionOut.Images {
if _, err = p.PullImage(image, engine); err != nil {
fmt.Print(err)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
if !qcr.IsRepoExist() {
return errors.New("ManifestsRoot not found")
}
if profile != "" {
qcr.Spec.Profile = profile
if err := qConfig.WriteCR(qcr); err != nil {
return err
}
println("---")
}
return q.PullImagesForCurrentCR()
}
// PullImages ...
func (q *Qliksense) PullImagesForCurrentCR() error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
version := qcr.GetLabelFromCr("version")
profile := qcr.Spec.Profile
repoDir := qcr.Spec.ManifestsRoot
imagesDir, err := setupImagesDir(q.QliksenseHome)
if err != nil {
return err
}
versionOut, stored, err := q.readOrGenerateVersionOutput(imagesDir, version, repoDir, profile)
if err != nil {
return err
}
images := versionOut.Images
if err := q.appendAdditionalImages(&images, qcr); err != nil {
return err
}
for _, image := range images {
if err := pullImage(image, imagesDir); err != nil {
fmt.Printf("%v\n", err)
return err
}
fmt.Print("---\n")
}
if version != "" && !stored {
if err := q.writeVersionOutput(versionOut, imagesDir, version); err != nil {
return err
}
}
return nil
}
// PullImage ...
func (p *Qliksense) PullImage(imageName string, engine bool) (map[string]string, error) {
if engine {
return p.pullDockerImage(imageName)
func (q *Qliksense) appendGitOpsImage(images *[]string, qcr *qapi.QliksenseCR) {
if qcr.Spec.GitOps != nil && qcr.Spec.GitOps.Image != "" {
*images = append(*images, qcr.Spec.GitOps.Image)
}
return p.pullImage(imageName)
}
func (p *Qliksense) commandTimeoutContext(commandTimeout time.Duration) (context.Context, context.CancelFunc) {
ctx := context.Background()
var cancel context.CancelFunc = func() {}
if commandTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, commandTimeout)
func (q *Qliksense) appendPreflightImages(images *[]string) {
pf := qapi.NewPreflightConfig(q.QliksenseHome)
for _, preflightImage := range pf.GetImageMap() {
*images = append(*images, preflightImage)
}
return ctx, cancel
}
func (p *Qliksense) pullImage(imageName string) (map[string]string, error) {
var (
ctx context.Context
cancel context.CancelFunc
srcRef, destRef imageTypes.ImageReference
blobDir, targetDir, homeDir string
segments []string
nameTag []string
err error
policyContext *signature.PolicyContext
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if srcRef, err = alltransports.ParseImageName("docker://" + imageName); err != nil {
return nil, err
func (q *Qliksense) appendOperatorImages(images *[]string) error {
if operatorImages, err := getImageList([]byte(q.GetOperatorControllerString())); err != nil {
return err
} else {
*images = append(*images, operatorImages...)
return nil
}
segments = strings.Split(imageName, "/")
nameTag = strings.Split(segments[len(segments)-1], ":")
if len(nameTag) < 2 {
nameTag = append(nameTag, "latest")
}
if homeDir, err = homedir.Dir(); err != nil {
return nil, err
}
targetDir = filepath.Join(homeDir, ".qliksense", "images", nameTag[0], nameTag[1])
}
fmt.Printf("==> Pulling image %v:%v", nameTag[0], nameTag[1])
fmt.Println()
os.MkdirAll(targetDir, os.ModePerm)
blobDir = filepath.Join(homeDir, ".qliksense", "blobs")
os.MkdirAll(blobDir, os.ModePerm)
if destRef, err = alltransports.ParseImageName("oci:" + targetDir); err != nil {
return nil, err
func pullImage(image, imagesDir string) error {
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%v", image))
if err != nil {
return err
}
nameTag := getImageNameParts(image)
targetDir := filepath.Join(imagesDir, imageIndexDirName, nameTag.name, nameTag.tag)
if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
if policyContext, err = signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}); err != nil {
return nil, err
destRef, err := alltransports.ParseImageName(fmt.Sprintf("oci:%v", targetDir))
if err != nil {
return err
}
policyContext, err := signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}})
if err != nil {
return err
}
defer policyContext.Destroy()
_, err = copy.Image(ctx, policyContext, destRef, srcRef, &copy.Options{
fmt.Printf("==> Pulling image from %v\n", srcRef.StringWithinTransport())
if _, err := copy.Image(context.Background(), policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: &imageTypes.SystemContext{
ArchitectureChoice: "amd64",
OSChoice: "linux",
},
DestinationCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: blobDir,
OCISharedBlobDirPath: filepath.Join(imagesDir, imageSharedBlobsDirName),
},
})
return nil, err
}); err != nil {
return err
}
return nil
}
func (p *Qliksense) pullDockerImage(imageName string) (map[string]string, error) {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pullOptions types.ImagePullOptions
ctx context.Context
cancel context.CancelFunc
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if cli, err = command.NewDockerCli(); err != nil {
return nil, err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return nil, err
}
if ref, err = reference.ParseNormalizedNamed(imageName); err != nil {
return nil, err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return nil, err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return nil, err
}
pullOptions = types.ImagePullOptions{
RegistryAuth: encodedAuth,
}
if response, err = cli.Client().ImagePull(ctx, imageName, pullOptions); err != nil {
return nil, err
}
defer response.Close()
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
return nil, err
}
inspectData, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
func (q *Qliksense) PushImagesForCurrentCR() error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
return nil, err
return err
}
return inspectData.ContainerConfig.Labels, nil
}
version := qcr.GetLabelFromCr("version")
profile := qcr.Spec.Profile
repoDir := qcr.Spec.ManifestsRoot
//TagAndPushImages ...
func (p *Qliksense) TagAndPushImages(registry string, engine bool) error {
var (
image string
err error
yamlVersion string
images VersionOutput
)
dockerConfigJsonSecret, err := qConfig.GetPushDockerConfigJsonSecret()
if err != nil {
if os.IsNotExist(err) {
dockerConfigJsonSecret = &qapi.DockerConfigJsonSecret{
Uri: qcr.Spec.GetImageRegistry(),
}
} else {
return err
}
}
if err = yaml.Unmarshal([]byte(yamlVersion), &images); err != nil {
imagesDir, err := setupImagesDir(q.QliksenseHome)
if err != nil {
return err
}
for _, image = range images.Images {
if err = p.TagAndPush(image, registry, engine); err != nil {
fmt.Print(err)
versionOut, stored, err := q.readOrGenerateVersionOutput(imagesDir, version, repoDir, profile)
if err != nil {
return err
}
images := versionOut.Images
if err := q.appendAdditionalImages(&images, qcr); err != nil {
return err
}
for _, image := range images {
if err = pushImage(image, imagesDir, dockerConfigJsonSecret); err != nil {
fmt.Printf("%v\n", err)
return err
}
fmt.Print("---\n")
}
if version != "" && !stored {
if err := q.writeVersionOutput(versionOut, imagesDir, version); err != nil {
return err
}
println("---")
}
return nil
}
func (p *Qliksense) directoryExists(path string) (exists bool, err error) {
func (q *Qliksense) appendAdditionalImages(images *[]string, qcr *qapi.QliksenseCR) error {
if err := q.appendOperatorImages(images); err != nil {
return err
}
q.appendGitOpsImage(images, qcr)
q.appendPreflightImages(images)
return nil
}
func pushImage(image, imagesDir string, dockerConfigJsonSecret *qapi.DockerConfigJsonSecret) error {
imageNameParts := getImageNameParts(image)
srcDir := filepath.Join(imagesDir, imageIndexDirName, imageNameParts.name, imageNameParts.tag)
if exists, err := directoryExists(srcDir); err != nil {
return err
} else if !exists {
if err := pullImage(image, imagesDir); err != nil {
return err
}
}
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("oci:%v", srcDir))
if err != nil {
return err
}
newImage := fmt.Sprintf("%v/%v:%v", dockerConfigJsonSecret.Uri, imageNameParts.name, imageNameParts.tag)
destRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%v", newImage))
if err != nil {
return err
}
policyContext, err := signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}})
if err != nil {
return err
}
defer policyContext.Destroy()
destinationCtx := &imageTypes.SystemContext{
DockerInsecureSkipTLSVerify: imageTypes.OptionalBoolTrue,
}
if dockerConfigJsonSecret.Username != "" {
destinationCtx.DockerAuthConfig = &imageTypes.DockerAuthConfig{
Username: dockerConfigJsonSecret.Username,
Password: dockerConfigJsonSecret.Password,
}
}
fmt.Printf("==> Pushing image to: %v\n", destRef.StringWithinTransport())
if _, err = copy.Image(context.Background(), policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: filepath.Join(imagesDir, imageSharedBlobsDirName),
},
DestinationCtx: destinationCtx,
}); err != nil {
return err
}
return nil
}
func directoryExists(path string) (exists bool, err error) {
if info, err := os.Stat(path); err != nil && os.IsNotExist(err) {
exists = false
err = nil
@@ -245,166 +281,65 @@ func (p *Qliksense) directoryExists(path string) (exists bool, err error) {
return exists, err
}
//TagAndPush ...
func (p *Qliksense) TagAndPush(image string, registryName string, engine bool) error {
if engine {
return p.tagAndDockerPush(image, registryName)
}
return p.tagAndPush(image, registryName)
}
func (p *Qliksense) tagAndPush(image string, registryName string) error {
var (
ctx context.Context
cancel context.CancelFunc
srcRef, destRef imageTypes.ImageReference
blobDir, srcDir, homeDir, newName string
segments []string
nameTag []string
err error
policyContext *signature.PolicyContext
srcExists bool
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
segments = strings.Split(image, "/")
nameTag = strings.Split(segments[len(segments)-1], ":")
func getImageNameParts(image string) imageNameParts {
segments := strings.Split(image, "/")
nameTag := strings.Split(segments[len(segments)-1], ":")
if len(nameTag) < 2 {
nameTag = append(nameTag, "latest")
}
if homeDir, err = homedir.Dir(); err != nil {
return err
return imageNameParts{
name: nameTag[0],
tag: nameTag[1],
}
srcDir = filepath.Join(homeDir, ".qliksense", "images", nameTag[0], nameTag[1])
if srcExists, err = p.directoryExists(srcDir); err != nil {
return err
}
if !srcExists {
if _, err = p.PullImage(image, false); err != nil {
return err
}
}
if srcRef, err = alltransports.ParseImageName("oci:" + srcDir); err != nil {
return err
}
if segments[0] == "docker.io" {
image = strings.Join(segments[1:], "/")
}
newName = "//" + registryName + "/" + segments[len(segments)-1]
fmt.Printf("==> Tag and push image to %v", newName)
fmt.Println()
if destRef, err = alltransports.ParseImageName("docker:" + newName); err != nil {
return err
}
if policyContext, err = signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}); err != nil {
return err
}
defer policyContext.Destroy()
blobDir = filepath.Join(homeDir, ".qliksense", "blobs")
os.MkdirAll(blobDir, os.ModePerm)
_, err = copy.Image(ctx, policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: blobDir,
},
DestinationCtx: &imageTypes.SystemContext{
DockerDaemonInsecureSkipTLSVerify: true,
},
})
return err
}
// PullImage ...
func (p *Qliksense) tagAndDockerPush(image string, registryName string) error {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pushOptions types.ImagePushOptions
ctx context.Context
cancel context.CancelFunc
newName string
segments []string
imageList []types.ImageSummary
imageListOptions types.ImageListOptions
filterArgs filters.Args
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
// TODO: Create a real cli config context
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if cli, err = command.NewDockerCli(); err != nil {
return err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return err
}
segments = strings.Split(image, "/")
if segments[0] == "docker.io" {
image = strings.Join(segments[1:], "/")
}
newName = registryName + "/" + segments[len(segments)-1]
func setupImagesDir(qliksenseHome string) (string, error) {
imagesDir := filepath.Join(qliksenseHome, imagesDirName)
filterArgs = filters.NewArgs()
filterArgs.Add("reference", image)
imageListOptions = types.ImageListOptions{
Filters: filterArgs,
}
if imageList, err = cli.Client().ImageList(ctx, imageListOptions); err != nil {
return err
}
if imageList == nil || len(imageList) <= 0 {
fmt.Printf("Use `qliksense pull`, to pull %v for an air gap push", newName)
return nil
imageIndexDir := filepath.Join(imagesDir, imageIndexDirName)
if err := os.MkdirAll(imageIndexDir, os.ModePerm); err != nil {
return "", err
}
if err = cli.Client().ImageTag(ctx, image, newName); err != nil {
return err
sharedBlobsDir := filepath.Join(imagesDir, imageSharedBlobsDirName)
if err := os.MkdirAll(sharedBlobsDir, os.ModePerm); err != nil {
return "", err
}
if ref, err = reference.ParseNormalizedNamed(image); err != nil {
return err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return err
}
pushOptions = types.ImagePushOptions{
All: true,
RegistryAuth: encodedAuth,
}
return imagesDir, nil
}
if response, err = cli.Client().ImagePush(ctx, newName, pushOptions); err != nil {
return err
func (q *Qliksense) readOrGenerateVersionOutput(imagesDir, version, repoDir, profile string) (versionOut *VersionOutput, stored bool, err error) {
if version != "" {
versionOut, err = q.readVersionOutput(imagesDir, version)
if versionOut != nil {
stored = true
}
}
defer response.Close()
if versionOut == nil {
if versionOut, err = q.AboutDir(repoDir, profile); err != nil {
return nil, false, err
}
}
return versionOut, stored, nil
}
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
func (q *Qliksense) readVersionOutput(imagesDir, version string) (*VersionOutput, error) {
var versionOut VersionOutput
versionFile := filepath.Join(imagesDir, version)
if versionOutBytes, err := ioutil.ReadFile(versionFile); err != nil {
return nil, err
} else if err = yaml.Unmarshal(versionOutBytes, &versionOut); err != nil {
return nil, err
}
return &versionOut, nil
}
func (q *Qliksense) writeVersionOutput(versionOut *VersionOutput, imagesDir, version string) error {
versionFile := filepath.Join(imagesDir, version)
if versionOutBytes, err := yaml.Marshal(versionOut); err != nil {
return err
} else if err = ioutil.WriteFile(versionFile, versionOutBytes, os.ModePerm); err != nil {
return err
}
return nil

View File

@@ -0,0 +1,617 @@
package qliksense
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gobuffalo/packr/v2"
"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports/alltransports"
imageTypes "github.com/containers/image/v5/types"
"golang.org/x/net/context"
"github.com/qlik-oss/sense-installer/pkg/api"
"gopkg.in/yaml.v2"
)
func Test_locateDockerRegistryBinary(t *testing.T) {
binary, err := locateDockerRegistryBinary()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmd := exec.Command(binary, "--version")
out, err := cmd.Output()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Logf("output: %v\n", string(out))
}
func Test_getSelfSignedCertAndKey(t *testing.T) {
host := "andriy.registry.com"
validity := time.Hour * 24 * 365
selfSignedCert, key, err := getSelfSignedCertAndKey(host, validity)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fmt.Print(string(selfSignedCert))
fmt.Print(string(key))
}
type clientAuthType byte
const (
clientAuthNotProvided clientAuthType = iota
clientAuthProvided
clientAuthProvidedButIncorrect
)
func Test_Pull_Push_ImagesForCurrentCR(t *testing.T) {
if testing.Short() {
t.Skip("Skipping pull/push tests in short mode")
}
var testCases = []struct {
name string
registryAuth bool
clientAuth clientAuthType
expectPushSuccess bool
}{
{
name: "registry does not require auth and we do not provide auth",
registryAuth: false,
clientAuth: clientAuthNotProvided,
expectPushSuccess: true,
},
{
name: "registry does not require auth but we provide auth",
registryAuth: false,
clientAuth: clientAuthProvided,
expectPushSuccess: true,
},
{
name: "registry requires auth but we do not provide auth",
registryAuth: true,
clientAuth: clientAuthNotProvided,
expectPushSuccess: false,
},
{
name: "registry requires auth but we provide wrong auth",
registryAuth: true,
clientAuth: clientAuthProvidedButIncorrect,
expectPushSuccess: false,
},
{
name: "registry requires auth and we provide auth",
registryAuth: true,
clientAuth: clientAuthProvided,
expectPushSuccess: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
registryURI := "127.0.0.1:5555"
registry, err := setupRegistryV2At(registryURI, testCase.registryAuth)
if registry != nil {
defer func() {
registry.Close()
//fmt.Printf("registry stdout:\n%v\n", registry.stdOutBuffer.String())
//fmt.Printf("registry stderr:\n%v\n", registry.stdErrBuffer.String())
}()
}
if err != nil {
t.Fatalf("unexpected error setting up local registry: %v", err)
}
tmpQlikSenseHome, err := ioutil.TempDir("", "tmp-qlik-sense-home-")
if err != nil {
t.Fatalf("unexpected error creating tmp dir: %v", err)
}
defer os.RemoveAll(tmpQlikSenseHome)
if err := setupQlikSenseHome(t, tmpQlikSenseHome, registry, testCase.clientAuth); err != nil {
t.Fatalf("unexpected error setting up qliksense home: %v", err)
}
q := &Qliksense{
QliksenseHome: tmpQlikSenseHome,
CrdBox: &packr.Box{},
}
var versionOut VersionOutput
if err := q.PullImagesForCurrentCR(); err != nil {
t.Fatalf("unexpected pull error: %v", err)
} else if versionOutBytes, err := ioutil.ReadFile(path.Join(tmpQlikSenseHome, "images", "foo")); err != nil {
t.Fatalf("unexpected error reading version file: %v", err)
} else if err = yaml.Unmarshal(versionOutBytes, &versionOut); err != nil {
t.Fatalf("unexpected error unmarshalling version file: %v", err)
} else if len(versionOut.Images) != 1 || versionOut.Images[0] != "alpine:latest" {
t.Fatal(`did not find "alpine:latest"" in the version file`)
} else if infos, err := ioutil.ReadDir(path.Join(tmpQlikSenseHome, "images", "index", "alpine", "latest")); err != nil || len(infos) == 0 {
t.Fatal("expected images/index/alpine/latest directory to be non-empty")
} else if blobInfos, err := ioutil.ReadDir(path.Join(tmpQlikSenseHome, "images", "blobs", "sha256")); err != nil || len(blobInfos) == 0 {
t.Fatal("expected images/blobs/sha256 directory to be non-empty")
}
if testCase.expectPushSuccess {
if err := q.PushImagesForCurrentCR(); err != nil {
t.Fatalf("unexpected push error: %v", err)
} else if tmpImagesDir, err := ioutil.TempDir("", "tmp-images-"); err != nil {
t.Fatalf("unexpected error creating tmp dir: %v", err)
} else if err := testPullImage(fmt.Sprintf("%s/alpine:latest", registryURI), tmpImagesDir, registry); err != nil {
t.Fatalf("unexpected error pulling alpine:latest from the local registry: %v", err)
} else if infos, err := ioutil.ReadDir(path.Join(tmpImagesDir, "index", "alpine", "latest")); err != nil || len(infos) == 0 {
t.Fatal("expected index/alpine/latest directory to be non-empty")
} else if blobInfos, err := ioutil.ReadDir(path.Join(tmpImagesDir, "blobs", "sha256")); err != nil || len(blobInfos) == 0 {
t.Fatal("expected blobs/sha256 directory to be non-empty")
}
} else {
if err := q.PushImagesForCurrentCR(); err == nil {
t.Fatal("unexpected push success")
}
}
})
}
}
func Test_appendAdditionalImages(t *testing.T) {
tmpQlikSenseHome, err := ioutil.TempDir("", "tmp-qlik-sense-home-")
if err != nil {
t.Fatalf("unexpected error creating tmp dir: %v", err)
}
defer os.RemoveAll(tmpQlikSenseHome)
setupQliksenseTestDefaultContext(t, tmpQlikSenseHome, `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
gitOps:
image: some-gitops-image
`)
q := &Qliksense{
QliksenseHome: tmpQlikSenseHome,
CrdBox: packr.New("crds", "./crds"),
}
pf := api.NewPreflightConfig(q.QliksenseHome)
if err := pf.Initialize(); err != nil {
t.Fatalf("unexpected error initializing preflight: %v", err)
}
qConfig := api.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
t.Fatalf("unexpected error getting current CR: %v", err)
}
images := make([]string, 0)
if err := q.appendAdditionalImages(&images, qcr); err != nil {
t.Fatalf("unexpected error appending additional images: %v", err)
}
expectedNumberAdditionalImages := 5
if len(images) != expectedNumberAdditionalImages {
t.Fatalf("unexpected number of additional images: %v, expected: %v", len(images), expectedNumberAdditionalImages)
}
haveMatchingImage := func(test func(string) bool) bool {
for _, image := range images {
if test(image) {
return true
}
}
return false
}
if !haveMatchingImage(func(image string) bool {
return strings.Contains(image, "qlik-docker-oss.bintray.io/qliksense-operator:v")
}) {
t.Fatal("expected to find the operator image in the list, but it wasn't there")
}
if !haveMatchingImage(func(image string) bool {
return image == "some-gitops-image"
}) {
t.Fatal("expected to find the GitOps image in the list, but it wasn't there")
}
if !haveMatchingImage(func(image string) bool {
return image == "nginx"
}) {
t.Fatal("expected to find the nginx Preflight image in the list, but it wasn't there")
}
if !haveMatchingImage(func(image string) bool {
return image == "subfuzion/netcat"
}) {
t.Fatal("expected to find the netcat Preflight image in the list, but it wasn't there")
}
if !haveMatchingImage(func(image string) bool {
return image == "mongo"
}) {
t.Fatal("expected to find the mongo Preflight image in the list, but it wasn't there")
}
}
func setupQlikSenseHome(t *testing.T, tmpQlikSenseHome string, registry *testRegistryV2, clientAuth clientAuthType) error {
version := "foo"
manifestsRootDir := filepath.ToSlash(path.Join(tmpQlikSenseHome, "contexts", "qlik-default", "repo", version))
cr := fmt.Sprintf(`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
labels:
version: %s
spec:
profile: docker-desktop
configs:
qliksense:
- name: imageRegistry
value: %s
manifestsRoot: %s
rotateKeys: "yes"
releaseName: qlik-default
`, version, registry.url, manifestsRootDir)
setupQliksenseTestDefaultContext(t, tmpQlikSenseHome, cr)
if clientAuth == clientAuthProvided || clientAuth == clientAuthProvidedButIncorrect {
if registry.username == "" || clientAuth == clientAuthProvidedButIncorrect {
registry.username = "bad"
}
if registry.password == "" || clientAuth == clientAuthProvidedButIncorrect {
registry.password = "worse"
}
qConfig := api.NewQConfig(tmpQlikSenseHome)
if err := qConfig.SetPushDockerConfigJsonSecret(&api.DockerConfigJsonSecret{
Uri: registry.url,
Username: registry.username,
Password: registry.password,
}); err != nil {
return err
}
}
profileDir := path.Join(manifestsRootDir, "manifests", "docker-desktop")
if err := os.MkdirAll(profileDir, os.ModePerm); err != nil {
return err
}
if err := ioutil.WriteFile(path.Join(profileDir, "kustomization.yaml"), []byte(`
resources:
- deployment.yaml
`), os.ModePerm); err != nil {
return err
}
if err := ioutil.WriteFile(path.Join(profileDir, "deployment.yaml"), []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: the-deployment
spec:
template:
spec:
containers:
- name: the-container
image: alpine:latest
`), os.ModePerm); err != nil {
return err
}
transformersDir := path.Join(manifestsRootDir, "transformers")
if err := os.MkdirAll(transformersDir, os.ModePerm); err != nil {
return err
}
if err := ioutil.WriteFile(path.Join(transformersDir, "qseokversion.yaml"), []byte(`
apiVersion: qlik.com/v1
kind: SelectivePatch
metadata:
name: qseokversion
enabled: true
patches:
- target:
kind: HelmChart
labelSelector: name!=qliksense-init
patch: |-
chartName: qliksense
chartVersion: 1.21.23
`), os.ModePerm); err != nil {
return err
}
return nil
}
type testRegistryV2 struct {
cmd *exec.Cmd
url string
dir string
username string
password string
email string
stdOutBuffer *bytes.Buffer
stdErrBuffer *bytes.Buffer
}
func locateDockerRegistryBinary() (string, error) {
if exePath, err := exec.LookPath("docker-registry"); err != nil {
if cwd, err := os.Getwd(); err != nil {
return "", err
} else {
return path.Join(cwd, "docker-registry"), nil
}
} else {
return exePath, nil
}
}
func setupRegistryV2At(url string, auth bool) (*testRegistryV2, error) {
reg, err := newTestRegistryV2At(url, auth)
if err != nil {
return nil, err
}
// Wait for registry to be ready to serve requests.
for i := 0; i != 50; i++ {
if err := reg.Ping("http"); err == nil {
fmt.Print("registry http ping succeeded\n")
break
} else {
fmt.Printf("registry http ping error: %v\n", err)
}
if err := reg.Ping("https"); err == nil {
fmt.Print("registry https ping succeeded\n")
break
} else {
fmt.Printf("registry https ping error: %v\n", err)
}
time.Sleep(100 * time.Millisecond)
}
if err != nil {
return reg, errors.New("timeout waiting for test registry to become available")
}
return reg, nil
}
func newTestRegistryV2At(url string, auth bool) (*testRegistryV2, error) {
tmp, err := ioutil.TempDir("", "registry-test-")
if err != nil {
return nil, err
}
template := `version: 0.1
loglevel: info
storage:
filesystem:
rootdirectory: %s
delete:
enabled: true
http:
addr: %s
%s`
var (
htpasswd string
username string
password string
email string
)
var env []string
if auth {
if certificate, key, err := getSelfSignedCertAndKey("localhost", time.Hour*24); err != nil {
return nil, err
} else {
certPath := filepath.Join(tmp, "domain.crt")
if err := ioutil.WriteFile(certPath, certificate, os.FileMode(0644)); err != nil {
return nil, err
}
keyPath := filepath.Join(tmp, "domain.key")
if err := ioutil.WriteFile(keyPath, key, os.FileMode(0644)); err != nil {
return nil, err
}
env = append(env, fmt.Sprintf("REGISTRY_HTTP_TLS_CERTIFICATE=%v", certPath))
env = append(env, fmt.Sprintf("REGISTRY_HTTP_TLS_KEY=%v", keyPath))
}
htpasswdPath := filepath.Join(tmp, "htpasswd")
userpasswd := "testuser:$2y$05$sBsSqk0OpSD1uTZkHXc4FeJ0Z70wLQdAX/82UiHuQOKbNbBrzs63m"
username = "testuser"
password = "testpassword"
email = "test@test.org"
if err := ioutil.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0644)); err != nil {
return nil, err
}
htpasswd = fmt.Sprintf(`auth:
htpasswd:
realm: basic-realm
path: %s
`, htpasswdPath)
}
confPath := filepath.Join(tmp, "config.yaml")
config, err := os.Create(confPath)
if err != nil {
return nil, err
}
if _, err := fmt.Fprintf(config, template, tmp, url, htpasswd); err != nil {
os.RemoveAll(tmp)
return nil, err
}
dockerRegistryBinaryPath, err := locateDockerRegistryBinary()
if err != nil {
return nil, err
}
cmd := exec.Command(dockerRegistryBinaryPath, "serve", confPath)
cmd.Env = env
stdOutBuf, stdErrBuf, err := consumeAndLogOutputs(fmt.Sprintf("registry-%s", url), cmd)
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
os.RemoveAll(tmp)
return nil, err
}
return &testRegistryV2{
cmd: cmd,
url: url,
dir: tmp,
username: username,
password: password,
email: email,
stdOutBuffer: stdOutBuf,
stdErrBuffer: stdErrBuf,
}, nil
}
func (t *testRegistryV2) Ping(protocol string) error {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(fmt.Sprintf("%v://%s/v2/", protocol, t.url))
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
return fmt.Errorf("registry ping replied with an unexpected status code %d", resp.StatusCode)
}
return nil
}
func (t *testRegistryV2) Close() {
t.cmd.Process.Kill()
os.RemoveAll(t.dir)
}
func consumeAndLogOutputStream(id string, f io.ReadCloser) *bytes.Buffer {
buff := &bytes.Buffer{}
go func() {
defer func() {
f.Close()
fmt.Fprintf(buff, "[%s]: Closed\n", id)
}()
buf := make([]byte, 1024)
for {
fmt.Fprintf(buff, "[%s]: waiting\n", id)
n, err := f.Read(buf)
fmt.Fprintf(buff, "[%s]: got %d,%#v: %s\n", id, n, err, strings.TrimSuffix(string(buf[:n]), "\n"))
if n <= 0 {
break
}
}
}()
return buff
}
// consumeAndLogOutputs causes all output to stdout and stderr from an *exec.Cmd to be logged to c
func consumeAndLogOutputs(id string, cmd *exec.Cmd) (*bytes.Buffer, *bytes.Buffer, error) {
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, nil, err
}
return consumeAndLogOutputStream(id+" stdout", stdout), consumeAndLogOutputStream(id+" stderr", stderr), nil
}
func getSelfSignedCertAndKey(hostname string, validity time.Duration) (certificate, key []byte, err error) {
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, fmt.Errorf("ailed to generate serial number: %s", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"self-signed"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(validity),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{hostname},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %s", err)
}
certificate = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, nil, fmt.Errorf("unable to marshal private key: %v", err)
}
key = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
return certificate, key, nil
}
func testPullImage(image, imagesDir string, registry *testRegistryV2) error {
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%v", image))
if err != nil {
return err
}
nameTag := getImageNameParts(image)
targetDir := filepath.Join(imagesDir, imageIndexDirName, nameTag.name, nameTag.tag)
if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
destRef, err := alltransports.ParseImageName(fmt.Sprintf("oci:%v", targetDir))
if err != nil {
return err
}
policyContext, err := signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}})
if err != nil {
return err
}
defer policyContext.Destroy()
fmt.Printf("==> Test is pulling image from %v\n", srcRef.StringWithinTransport())
sourceCtx := &imageTypes.SystemContext{
ArchitectureChoice: "amd64",
OSChoice: "linux",
DockerInsecureSkipTLSVerify: imageTypes.OptionalBoolTrue,
}
if registry.username != "" {
sourceCtx.DockerAuthConfig = &imageTypes.DockerAuthConfig{
Username: registry.username,
Password: registry.password,
}
}
if _, err := copy.Image(context.Background(), policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: sourceCtx,
DestinationCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: filepath.Join(imagesDir, imageSharedBlobsDirName),
},
}); err != nil {
return err
}
return nil
}

View File

@@ -1,10 +1,161 @@
package qliksense
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) FetchQK8s(version string) {
//io.WriteString(os.Stdout, q.GetCRDString())
fmt.Println(version)
type FetchCommandOptions struct {
GitUrl string
AccessToken string
Version string
SecretName string
Overwrite bool
}
const (
QLIK_GIT_REPO = "https://github.com/qlik-oss/qliksense-k8s"
)
func (q *Qliksense) FetchQK8s(version string) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
return fetchAndUpdateCR(qConfig, version)
}
func (q *Qliksense) FetchK8sWithOpts(opts *FetchCommandOptions) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
cr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
if opts.AccessToken != "" {
encKey, err := qConfig.GetEncryptionKeyFor(cr.GetName())
if err != nil {
return err
}
if err := cr.SetFetchAccessToken(opts.AccessToken, encKey); err != nil {
return err
}
}
if opts.SecretName != "" {
cr.SetFetchAccessSecretName(opts.SecretName)
}
if opts.GitUrl != "" {
cr.SetFetchUrl(opts.GitUrl)
}
v := getVersion(opts, cr)
if v == "" {
return errors.New("Cannot find gitref/tag/branch/version to fetch")
}
if qConfig.IsRepoExistForCurrent(v) {
if opts.Overwrite || getVerionsOverwriteConfirmation(v) == "y" {
if err := qConfig.DeleteRepoForCurrent(v); err != nil {
return err
}
} else {
// nothing to do
return nil
}
}
qConfig.WriteCR(cr)
return fetchAndUpdateCR(qConfig, v)
}
// fetchAndUpdateCR fetch
func fetchAndUpdateCR(qConfig *qapi.QliksenseConfig, version string) error {
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
if version == "" {
if qcr.GetLabelFromCr("version") == "" {
if encKey, err := qConfig.GetEncryptionKeyFor(qcr.GetName()); err != nil {
return err
} else if version, err = getLatestTag(qcr.GetFetchUrl(), qcr.GetFetchAccessToken(encKey)); err != nil {
return err
}
} else {
version = qcr.GetLabelFromCr("version")
}
}
encKey, err := qConfig.GetEncryptionKeyFor(qcr.GetName())
if err != nil {
return err
}
// downlaod to temp first
tempDest, err := fetchToTempDir(qcr.GetFetchUrl(), version, qcr.GetFetchAccessToken(encKey))
if err != nil {
return err
}
destDir := qConfig.BuildRepoPath(version)
fmt.Printf("fetching version [%s] from %s\n", version, qcr.GetFetchUrl())
if err := qapi.CopyDirectory(tempDest, destDir); err != nil {
return nil
}
qcr.Spec.ManifestsRoot = qConfig.BuildCurrentManifestsRoot(version)
qcr.AddLabelToCr("version", version)
return qConfig.WriteCurrentContextCR(qcr)
}
func fetchToTempDir(gitUrl, gitRef, accessToken string) (string, error) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
downloadPath := path.Join(tmpDir, "repo")
var auth transport.AuthMethod
if accessToken != "" {
auth = &http.BasicAuth{
Username: "something",
Password: accessToken,
}
}
if repo, err := kapis_git.CloneRepository(downloadPath, gitUrl, auth); err != nil {
return "", err
} else if err := kapis_git.Checkout(repo, gitRef, "", auth); err != nil {
return "", err
} else {
return downloadPath, nil
}
}
func getVersion(opts *FetchCommandOptions, qcr *qapi.QliksenseCR) string {
if opts.Version == "" {
if qcr.GetLabelFromCr("version") != "" {
return qcr.GetLabelFromCr("version")
}
}
return opts.Version
}
func getVerionsOverwriteConfirmation(version string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Println("The version [" + version + "] already exist")
cfm := "n"
for {
fmt.Print("Do you want to delete and fetch again [y/N]: ")
cfm, _ = reader.ReadString('\n')
cfm = strings.Replace(cfm, "\n", "", -1)
cfm = strings.TrimSpace(cfm)
if cfm == "" {
cfm = "n"
}
cfm = strings.ToLower(cfm)
if cfm == "y" || cfm == "n" {
break
}
}
return cfm
}

View File

@@ -0,0 +1,50 @@
package qliksense
import (
"io/ioutil"
"path/filepath"
"testing"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func TestFetchAndUpdateCR(t *testing.T) {
tempHome, _ := ioutil.TempDir("", "")
q := &Qliksense{
QliksenseHome: tempHome,
}
q.SetUpQliksenseContext("test1")
qConfig := qapi.NewQConfig(tempHome)
if err := fetchAndUpdateCR(qConfig, "v0.0.2"); err != nil {
t.Log(err)
t.FailNow()
}
actualCrFile := filepath.Join(tempHome, "contexts", "test1", "test1.yaml")
cr := &qapi.QliksenseCR{}
if err := qapi.ReadFromFile(cr, actualCrFile); err != nil {
t.Log(err)
t.FailNow()
}
if cr.Spec.ManifestsRoot != "contexts/test1/qlik-k8s/v0.0.2" {
t.Log("actual path: " + cr.Spec.ManifestsRoot + ", expected path: contexts/test1/qlik-k8s/v0.0.2")
t.FailNow()
}
//testing latest tag is fetched
cr.AddLabelToCr("version", "")
qConfig.WriteCR(cr)
err := fetchAndUpdateCR(qConfig, "")
if err != nil {
t.Log(err)
t.Fail()
}
cr = &qapi.QliksenseCR{}
qapi.ReadFromFile(cr, actualCrFile)
v := cr.GetLabelFromCr("version")
if v == "" || v == "v0.0.2" {
t.Log("should get latest but got version: " + v)
t.Fail()
}
}

View File

@@ -0,0 +1,155 @@
package qliksense
import (
"errors"
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/k-apis/pkg/git"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
type LsRemoteCmdOptions struct {
IncludeBranches bool
Limit int
}
func (q *Qliksense) GetInstallableVersions(opts *LsRemoteCmdOptions) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
var repoPath string
if qcr.Spec.GetManifestsRoot() != "" {
repoPath = qcr.Spec.GetManifestsRoot()
} else {
repoPath, err = DownloadFromGitRepoToTmpDir(defaultConfigRepoGitUrl, "master")
if err != nil {
return err
}
}
r, err := git.OpenRepository(repoPath)
if err != nil {
return err
}
remoteRefsList, err := git.GetRemoteRefs(r, nil,
&git.RemoteRefConstraints{
Include: true,
Sort: true,
SortOrder: git.RefSortOrderDescending,
},
&git.RemoteRefConstraints{
Include: opts.IncludeBranches,
Sort: true,
SortOrder: git.RefSortOrderAscending,
})
if err != nil {
return err
}
if len(remoteRefsList) < 1 {
return errors.New("cannot find git remote information in the config repository")
}
var originRemoteRefs *git.RemoteRefs
for _, remoteRefs := range remoteRefsList {
if remoteRefs.Name == "origin" {
originRemoteRefs = remoteRefs
break
}
}
if originRemoteRefs == nil {
return errors.New(`cannot find git remote called "origin" in the config repository`)
}
tags := originRemoteRefs.Tags
if len(tags) > opts.Limit {
tags = tags[:opts.Limit]
}
fmt.Print("Versions:\n")
for _, tag := range tags {
fmt.Printf(" %s\n", tag)
}
if opts.IncludeBranches {
branches := originRemoteRefs.Branches
if len(branches) > opts.Limit {
branches = branches[:opts.Limit]
}
fmt.Print("Branches:\n")
for _, branch := range branches {
fmt.Printf(" %s\n", branch)
}
}
return nil
}
func getLatestTag(repoUrl, accessToken string) (string, error) {
if repoUrl == "" {
return "", errors.New("repo url is empty")
}
repoPath, err := fetchToTempDir(repoUrl, "master", accessToken)
if err != nil {
return "", err
}
r, err := git.OpenRepository(repoPath)
if err != nil {
return "", err
}
remoteRefsList, err := git.GetRemoteRefs(r, nil,
&git.RemoteRefConstraints{
Include: true,
Sort: true,
SortOrder: git.RefSortOrderDescending,
},
&git.RemoteRefConstraints{
Include: false,
Sort: true,
SortOrder: git.RefSortOrderAscending,
})
if err != nil {
return "", err
}
if len(remoteRefsList) < 1 {
return "", errors.New("cannot find git remote information in the config repository")
}
var originRemoteRefs *git.RemoteRefs
for _, remoteRefs := range remoteRefsList {
if remoteRefs.Name == "origin" {
originRemoteRefs = remoteRefs
break
}
}
if originRemoteRefs == nil {
return "", errors.New(`cannot find git remote called "origin" in the config repository`)
}
tags := originRemoteRefs.Tags
if len(tags) == 0 {
return "", errors.New(("no tags exists in the repo: " + repoPath))
}
maxSem, _ := semver.NewVersion(tags[0])
for _, sv := range tags[1:] {
if sv == "" {
continue
}
v, err := semver.NewVersion(sv)
if err != nil {
// it may happen, in the repo some tags may not conform to semver
fmt.Print("Unconform tags: " + sv)
continue
}
if maxSem == nil || maxSem.LessThan(v) {
maxSem = v
}
}
return maxSem.Original(), nil
}

View File

@@ -0,0 +1,25 @@
package qliksense
import (
"testing"
"github.com/Masterminds/semver/v3"
)
func TestGetLatestTag(t *testing.T) {
s, err := getLatestTag(defaultConfigRepoGitUrl, "")
if s == "" || err != nil {
t.Log(err)
t.Fail()
}
sv, err := semver.NewVersion(s)
if err != nil {
t.Log(err)
t.Log(sv)
}
baseV, _ := semver.NewVersion("v0.0.7")
if !sv.GreaterThan(baseV) {
t.Log("Expected greater than v0.0.7, but got: " + s)
t.Fail()
}
}

View File

@@ -1,52 +1,225 @@
package qliksense
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/qlik-oss/k-apis/pkg/config"
"sigs.k8s.io/kustomize/api/filesys"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) InstallQK8s(version string) error {
type InstallCommandOptions struct {
StorageClass string
MongoDbUri string
RotateKeys string
}
func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions, keepPatchFiles bool) error {
// step1: fetch 1.0.0 # pull down qliksense-k8s@1.0.0
// step2: operator view | kubectl apply -f # operator manifest (CRD)
// step3: config apply | kubectl apply -f # generates patches (if required) in configuration directory, applies manifest
// step4: config view | kubectl apply -f # generates Custom Resource manifest (CR)
//io.WriteString(os.Stdout, q.GetCRDString())
//fmt.Println(version)
fmt.Println("Fetching " + version)
//qConfig := qapi.NewQConfig(q.QliksenseHome)
//qcr, err := qConfig.GetCurrentCR()
//TODO: may need to check if CRD already installed, but doing apply does not hurt for now
//install crd into cluster
fmt.Println("Installing operator CRD")
if err := qapi.KubectlApply(q.GetCRDString()); err != nil {
fmt.Println("cannot do kubectl apply on opeartor CRD", err)
return err
}
// install generated manifests into cluster
fmt.Println("Installing generated manifests into cluster")
// fetch the version
qConfig := qapi.NewQConfig(q.QliksenseHome)
if !keepPatchFiles {
if err := q.DiscardAllUnstagedChangesFromGitRepo(qConfig); err != nil {
fmt.Printf("error removing temporary changes to the config: %v\n", err)
}
}
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
if err := applyConfigToK8s(qcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
qcr.SetEULA("yes")
if opts.MongoDbUri != "" {
qcr.Spec.AddToSecrets("qliksense", "mongoDbUri", opts.MongoDbUri, "")
}
if opts.StorageClass != "" {
qcr.Spec.StorageClassName = opts.StorageClass
}
if opts.RotateKeys != "" {
qcr.Spec.RotateKeys = opts.RotateKeys
}
qConfig.WriteCurrentContextCR(qcr)
//if the docker pull secret exists on disk, install it in the cluster
//if it doesn't exist on disk, remove it in the cluster
if err := installOrRemoveImagePullSecret(qConfig); err != nil {
return err
}
// install operator cr into cluster
//get the current context cr
fmt.Println("Install operator CR into cluster")
r, err := q.getCurrentCRString()
if err != nil {
// check if acceptEULA is yes or not
if !qcr.IsEULA() {
return errors.New(agreementTempalte + "\n Please do $ qliksense install --acceptEULA=yes\n")
}
//CRD will be installed outside of operator
//install operator controller into the namespace
fmt.Println("Installing operator controller")
if operatorControllerString, err := q.getProcessedOperatorControllerString(qcr); err != nil {
fmt.Println("error extracting/transforming operator controller", err)
return err
} else if err := qapi.KubectlApply(operatorControllerString, ""); err != nil {
fmt.Println("cannot do kubectl apply on operator controller", err)
return err
}
if err := qapi.KubectlApply(r); err != nil {
fmt.Println("cannot do kubectl apply on operator CR")
// create patch dependent resoruces
fmt.Println("Installing resoruces used kuztomize patch")
if err := q.createK8sResoruceBeforePatch(qcr); err != nil {
return err
}
if qcr.Spec.Git != nil && qcr.Spec.Git.Repository != "" {
// fetching and applying manifest will be in the operator controller
// get decrypted cr
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
return q.applyCR(dcr)
}
}
if !qcr.IsRepoExist() {
if err := fetchAndUpdateCR(qConfig, version); err != nil {
return err
}
}
qcr, err = qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
} else if qcr.Spec.GetManifestsRoot() == "" {
return errors.New("cannot get the manifest root. Use qliksense fetch <version> or qliksense set manifestsRoot")
}
// install generated manifests into cluster
fmt.Println("Installing generated manifests into cluster")
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
if IsQliksenseInstalled(dcr.GetName()) {
return q.UpgradeQK8s(keepPatchFiles)
}
if err := q.applyConfigToK8s(dcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
} else {
return q.applyCR(dcr)
}
}
}
func (q *Qliksense) getProcessedOperatorControllerString(qcr *qapi.QliksenseCR) (string, error) {
operatorControllerString := q.GetOperatorControllerString()
if imageRegistry := qcr.Spec.GetImageRegistry(); imageRegistry != "" {
return kustomizeForImageRegistry(operatorControllerString, pullSecretName,
path.Join(qliksenseOperatorImageRepo, qliksenseOperatorImageName),
path.Join(imageRegistry, qliksenseOperatorImageName))
}
return operatorControllerString, nil
}
func installOrRemoveImagePullSecret(qConfig *qapi.QliksenseConfig) error {
if pullDockerConfigJsonSecret, err := qConfig.GetPullDockerConfigJsonSecret(); err == nil {
if dockerConfigJsonSecretYaml, err := pullDockerConfigJsonSecret.ToYaml(""); err != nil {
return err
} else if err := qapi.KubectlApply(string(dockerConfigJsonSecretYaml), ""); err != nil {
return err
}
} else {
deleteDockerConfigJsonSecret := qapi.DockerConfigJsonSecret{
Name: pullSecretName,
}
if deleteDockerConfigJsonSecretYaml, err := deleteDockerConfigJsonSecret.ToYaml(""); err != nil {
return err
} else if err := qapi.KubectlDelete(string(deleteDockerConfigJsonSecretYaml), ""); err != nil {
qapi.LogDebugMessage("failed deleting %v, error: %v\n", pullSecretName, err)
}
}
return nil
}
func kustomizeForImageRegistry(resources, dockerConfigJsonSecretName, name, newName string) (string, error) {
dir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
defer os.RemoveAll(dir)
if err := ioutil.WriteFile(filepath.Join(dir, "resources.yaml"), []byte(resources), os.ModePerm); err != nil {
return "", err
} else if err := ioutil.WriteFile(filepath.Join(dir, "addImagePullSecrets.yaml"), []byte(fmt.Sprintf(`
apiVersion: builtin
kind: PatchTransformer
metadata:
name: notImportantHere
patch: '[{"op": "add", "path": "/spec/template/spec/imagePullSecrets", "value": [{"name": "%v"}]}]'
target:
name: .*-operator
kind: Deployment
`, dockerConfigJsonSecretName)), os.ModePerm); err != nil {
return "", err
} else if err := ioutil.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(fmt.Sprintf(`
resources:
- resources.yaml
transformers:
- addImagePullSecrets.yaml
images:
- name: %s
newName: %s
`, name, newName)), os.ModePerm); err != nil {
return "", err
} else if out, err := executeKustomizeBuildForFileSystem(dir, filesys.MakeFsOnDisk()); err != nil {
return "", err
} else {
return string(out), nil
}
}
func (q *Qliksense) applyCR(cr *qapi.QliksenseCR) error {
// install operator cr into cluster
//get the current context cr
fmt.Println("Install operator CR into cluster")
r, err := cr.GetString()
if err != nil {
return err
}
if err := qapi.KubectlApply(r, ""); err != nil {
fmt.Println("cannot do kubectl apply on operator CR")
return err
}
return nil
}
func (q *Qliksense) createK8sResoruceBeforePatch(qcr *qapi.QliksenseCR) error {
for svc, nvs := range qcr.Spec.Secrets {
for _, nv := range nvs {
if isK8sSecretNeedToCreate(nv) {
fmt.Println(filepath.Join(qcr.GetK8sSecretsFolder(q.QliksenseHome), svc+".yaml"))
if secS, err := q.PrepareK8sSecret(filepath.Join(qcr.GetK8sSecretsFolder(q.QliksenseHome), svc+".yaml")); err != nil {
return err
} else {
return qapi.KubectlApply(secS, "")
}
}
}
}
return nil
}
func isK8sSecretNeedToCreate(nv config.NameValue) bool {
return nv.ValueFrom != nil
}

View File

@@ -0,0 +1,176 @@
package qliksense
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"testing"
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"github.com/gobuffalo/packr/v2"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func TestCreateK8sResoruceBeforePatch(t *testing.T) {
td := setup()
sampleCr := `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-test3
labels:
version: v0.0.2
spec:
git:
repository: https://github.com/ffoysal/qliksense-k8s
accessToken: abababababababaab
userName: "blblbl"
gitOps:
enabled: "no"
schedule: "*/1 * * * *"
watchBranch: pr-branch-db1d26d6
image: qlik-docker-oss.bintray.io/qliksense-repo-watcher
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: mongodb://qlik-default-mongodb:27017/qliksense?ssl=false
profile: docker-desktop
rotateKeys: "yes"`
crFile := filepath.Join(testDir, "install_test.yaml")
ioutil.WriteFile(crFile, []byte(sampleCr), 0644)
q := New(testDir)
file, e := os.Open(crFile)
if e != nil {
t.Log(e)
t.FailNow()
}
if err := q.LoadCr(file, false); err != nil {
t.Log(err)
t.FailNow()
}
qConfig := qapi.NewQConfig(testDir)
cr, err := qConfig.GetCR("qlik-test3")
if err != nil {
t.Log(err)
t.FailNow()
}
if err = q.createK8sResoruceBeforePatch(cr); err != nil {
t.Log(err)
t.FailNow()
}
td()
}
func setupQliksenseTestDefaultContext(t *testing.T, tmpQlikSenseHome, CR string) {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`), os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
defaultContextDir := path.Join(tmpQlikSenseHome, "contexts", "qlik-default")
if err := os.MkdirAll(defaultContextDir, os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := ioutil.WriteFile(path.Join(defaultContextDir, "qlik-default.yaml"), []byte(CR), os.ModePerm); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func Test_getProcessedOperatorControllerString(t *testing.T) {
tmpQlikSenseHome, err := ioutil.TempDir("", "tmp-qlik-sense-home-")
if err != nil {
t.Fatalf("unexpected error creating tmp dir: %v", err)
}
defer os.RemoveAll(tmpQlikSenseHome)
registry := "registryFoo"
setupQliksenseTestDefaultContext(t, tmpQlikSenseHome, fmt.Sprintf(`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
configs:
qliksense:
- name: imageRegistry
value: %v
`, registry))
q := &Qliksense{
QliksenseHome: tmpQlikSenseHome,
CrdBox: packr.New("crds", "./crds"),
}
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
t.Fatalf("unexpected error getting current CR: %v", err)
}
originalOperatorString := q.GetOperatorControllerString()
processedOperatorString, err := q.getProcessedOperatorControllerString(qcr)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
controllerImageChecks := map[string]func(t *testing.T, controllerImage string){
originalOperatorString: func(t *testing.T, controllerImage string) {
expectedControllerImagePrefix := fmt.Sprintf("%v/%v:", qliksenseOperatorImageRepo, qliksenseOperatorImageName)
if !strings.HasPrefix(controllerImage, expectedControllerImagePrefix) {
t.Fatalf("expected controller image: %v to have prefix: %v", controllerImage, expectedControllerImagePrefix)
}
},
processedOperatorString: func(t *testing.T, controllerImage string) {
expectedControllerImagePrefix := fmt.Sprintf("%v/%v:", registry, qliksenseOperatorImageName)
if !strings.HasPrefix(controllerImage, expectedControllerImagePrefix) {
t.Fatalf("expected controller image: %v to have prefix: %v", controllerImage, expectedControllerImagePrefix)
}
},
}
resourceFactory := resmap.NewFactory(resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl()), nil)
for operatorString, controllerImageCheck := range controllerImageChecks {
resMap, err := resourceFactory.NewResMapFromBytes([]byte(operatorString))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
res, err := resMap.GetById(resid.NewResId(resid.Gvk{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}, "qliksense-operator"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
controllerImage, err := res.GetString("spec.template.spec.containers[0].image")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
controllerImageCheck(t, controllerImage)
}
}

View File

@@ -1,22 +1,31 @@
package qliksense
import (
"bufio"
"log"
"os"
"strings"
"github.com/qlik-oss/sense-installer/pkg/api"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // don't delete this line ref: https://github.com/kubernetes/client-go/issues/242
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/types"
)
func executeKustomizeBuild(directory string) ([]byte, error) {
//ExecuteKustomizeBuild execute kustomize to the directory and return manifest as byte array
func ExecuteKustomizeBuild(directory string) ([]byte, error) {
return executeKustomizeBuildForFileSystem(directory, filesys.MakeFsOnDisk())
}
func executeKustomizeBuildForFileSystem(directory string, fSys filesys.FileSystem) ([]byte, error) {
log.SetOutput(&nullWriter{})
defer func() {
log.SetOutput(os.Stderr)
}()
fSys := filesys.MakeFsOnDisk()
options := &krusty.Options{
DoLegacyResourceSort: false,
LoadRestrictions: types.LoadRestrictionsNone,
@@ -30,3 +39,29 @@ func executeKustomizeBuild(directory string) ([]byte, error) {
}
return resMap.AsYaml()
}
func executeKustomizeBuildWithStdoutProgress(path string) (kuzManifest []byte, err error) {
result, err := api.ExecuteTaskWithBlinkingStdoutFeedback(func() (interface{}, error) {
return ExecuteKustomizeBuild(path)
}, "...")
if err != nil {
return nil, err
}
return result.([]byte), nil
}
//GetYamlsFromMultiDoc filter yaml docs from multiyaml based on kind
func GetYamlsFromMultiDoc(multiYaml string, kind string) string {
yamlDocs := strings.Split(string(multiYaml), "---")
resultDocs := ""
for _, doc := range yamlDocs {
scanner := bufio.NewScanner(strings.NewReader(doc))
for scanner.Scan() {
if scanner.Text() == "kind: "+kind {
resultDocs = resultDocs + "\n---\n" + doc
break
}
}
}
return resultDocs
}

View File

@@ -1,15 +1,26 @@
package qliksense
import (
"bytes"
"encoding/base64"
"io"
"io/ioutil"
"log"
"os"
"path"
"strings"
"testing"
"gopkg.in/yaml.v3"
"github.com/Shopify/ejson"
"github.com/qlik-oss/k-apis/pkg/config"
"github.com/qlik-oss/k-apis/pkg/qust"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
)
func Test_executeKustomizeBuild(t *testing.T) {
func Test_ExecuteKustomizeBuild(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
@@ -30,7 +41,7 @@ configMapGenerator:
t.Fatalf("error writing kustomization file to path: %v error: %v\n", kustomizationYamlFilePath, err)
}
result, err := executeKustomizeBuild(tmpDir)
result, err := ExecuteKustomizeBuild(tmpDir)
if err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
@@ -47,11 +58,7 @@ metadata:
}
}
func Test_executeKustomizeBuild_onQlikConfig_DISABLED(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
func Test_executeKustomizeBuild_onQlikConfig_regenerateKeys(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
@@ -59,13 +66,170 @@ func Test_executeKustomizeBuild_onQlikConfig_DISABLED(t *testing.T) {
defer os.RemoveAll(tmpDir)
configPath := path.Join(tmpDir, "config")
if repo, err := kapis_git.CloneRepository(configPath, defaultGitUrl, nil); err != nil {
if repo, err := kapis_git.CloneRepository(configPath, defaultConfigRepoGitUrl, nil); err != nil {
t.Fatalf("unexpected error: %v\n", err)
} else if err := kapis_git.Checkout(repo, "v1.21.23-edge", "", nil); err != nil {
} else if err := kapis_git.Checkout(repo, "e38df644e759abf0b5941c1511d1a2cd5e3c42fa", "", nil); err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
if _, err := executeKustomizeBuild(path.Join(configPath, "manifests", "base")); err != nil {
cr := &config.CRSpec{
ManifestsRoot: configPath,
}
if err := os.Setenv("EJSON_KEYDIR", tmpDir); err != nil {
t.Fatalf("unexpected error setting EJSON_KEYDIR environment variable: %v\n", err)
}
if err := os.Unsetenv("EJSON_KEY"); err != nil {
t.Fatalf("unexpected error unsetting EJSON_KEY: %v\n", err)
}
generateKeys(cr, "won't-use")
yamlResources, err := ExecuteKustomizeBuild(path.Join(configPath, "manifests", "base", "resources", "users"))
if err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
decoder := yaml.NewDecoder(bytes.NewReader(yamlResources))
var resource map[string]interface{}
keyIdBase64 := ""
for {
err := decoder.Decode(&resource)
if err != nil {
if err != io.EOF {
t.Fatalf("unexpected yaml decode error: %v\n", err)
}
break
}
if resource["kind"].(string) == "Secret" && strings.Contains(resource["metadata"].(map[string]interface{})["name"].(string), "users-secrets-") {
keyIdBase64 = resource["data"].(map[string]interface{})["tokenAuthPrivateKeyId"].(string)
break
}
}
untransformedKeyId := `(( (ds "data").kid ))`
if keyIdBase64 == "" {
t.Fatalf("expected keyIdBase64 for users secret to be non empty:\n")
} else if keyId, err := base64.StdEncoding.DecodeString(keyIdBase64); err != nil {
t.Fatalf("unexpected base64 decode error: %v\n", err)
} else if string(keyId) == untransformedKeyId {
t.Fatalf("unexpected users keyId: %v\n", untransformedKeyId)
}
}
func generateKeys(cr *config.CRSpec, defaultKeyDir string) {
log.Println("rotating all keys")
keyDir := getEjsonKeyDir(defaultKeyDir)
if ejsonPublicKey, ejsonPrivateKey, err := ejson.GenerateKeypair(); err != nil {
log.Printf("error generating an ejson key pair: %v\n", err)
} else if err := qust.GenerateKeys(cr, ejsonPublicKey); err != nil {
log.Printf("error generating application keys: %v\n", err)
} else if err := os.MkdirAll(keyDir, os.ModePerm); err != nil {
log.Printf("error makeing sure private key storage directory: %v exists, error: %v\n", keyDir, err)
} else if err := ioutil.WriteFile(path.Join(keyDir, ejsonPublicKey), []byte(ejsonPrivateKey), os.ModePerm); err != nil {
log.Printf("error storing ejson private key: %v\n", err)
}
}
func getEjsonKeyDir(defaultKeyDir string) string {
ejsonKeyDir := os.Getenv("EJSON_KEYDIR")
if ejsonKeyDir == "" {
ejsonKeyDir = defaultKeyDir
}
return ejsonKeyDir
}
func Test_GetYamlDocKindFromMultiDoc(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
defer os.RemoveAll(tmpDir)
kustomizationYamlFilePath := path.Join(tmpDir, "kustomization.yaml")
testResFileYamlFilePath := path.Join(tmpDir, "test-file.yaml")
kustomizationYaml := `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- test-file.yaml
`
testYaml := `
apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
name: foo-config
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: qix-sessions
chart: qix-sessions-4.0.10
heritage: Helm
release: qliksense
name: qliksense-qix-sessions
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
labels:
app: chronos
chart: chronos-1.5.7
heritage: Helm
release: qliksense
name: qliksense-chronos
namespace: default
rules:
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- update
`
err = ioutil.WriteFile(kustomizationYamlFilePath, []byte(kustomizationYaml), os.ModePerm)
if err != nil {
t.Fatalf("error writing kustomization file to path: %v error: %v\n", kustomizationYamlFilePath, err)
}
err = ioutil.WriteFile(testResFileYamlFilePath, []byte(testYaml), os.ModePerm)
if err != nil {
t.Fatalf("error writing test-file to path: %v error: %v\n", testResFileYamlFilePath, err)
}
result, err := ExecuteKustomizeBuild(tmpDir)
if err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
resultYaml := GetYamlsFromMultiDoc(string(result), "Role")
expectedK8sYaml := `
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
labels:
app: chronos
chart: chronos-1.5.7
heritage: Helm
release: qliksense
name: qliksense-chronos
namespace: default
rules:
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- update
`
if resultYaml != expectedK8sYaml {
t.Fatalf("expected k8s yaml: [%v] but got: [%v]\n", expectedK8sYaml, resultYaml)
}
}

93
pkg/qliksense/load_cr.go Normal file
View File

@@ -0,0 +1,93 @@
package qliksense
import (
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) LoadCr(reader io.Reader, overwriteExistingContext bool) error {
if crBytes, err := ioutil.ReadAll(reader); err != nil {
return err
} else if crName, err := q.loadCrStringIntoFileSystem(string(crBytes), overwriteExistingContext); err != nil {
return err
} else {
fmt.Println("cr name: [ " + crName + " ] has been loaded")
}
return nil
}
func (q *Qliksense) IsEulaAcceptedInCrFile(reader io.Reader) (bool, error) {
if crBytes, err := ioutil.ReadAll(reader); err != nil {
return false, err
} else if cr, err := qapi.CreateCRObjectFromString(string(crBytes)); err != nil {
return false, err
} else {
return cr.IsEULA(), nil
}
}
func (q *Qliksense) loadCrStringIntoFileSystem(crstr string, overwriteExistingContext bool) (string, error) {
cr, err := qapi.CreateCRObjectFromString(crstr)
if err != nil {
return "", err
}
qConfig := qapi.NewQConfig(q.QliksenseHome)
if qConfig.IsContextExist(cr.GetName()) {
if !overwriteExistingContext {
return "", errors.New("Context with name: " + cr.GetName() + " already exists. " +
"Please delete the existing context first using the delete-context command or specify the --overwrite flag.")
}
// else if err := os.RemoveAll(qConfig.GetContextPath(cr.GetName())); err != nil {
// return "", err
// }
}
if err := qConfig.CreateContextDirs(cr.GetName()); err != nil {
return "", err
}
// encrypt the secrets and do base64 then update the CR
encryptionKey, err := qConfig.GetEncryptionKeyFor(cr.GetName())
if err != nil {
return "", err
}
for svc, nvs := range cr.Spec.Secrets {
for _, nv := range nvs {
if nv.ValueFrom == nil {
skv := &qapi.ServiceKeyValue{
Key: nv.Name,
Value: nv.Value,
SvcName: svc,
}
if err := q.processSecret(skv, encryptionKey, cr, false); err != nil {
return cr.GetName(), err
}
}
}
}
if cr.Spec.FetchSource != nil && cr.Spec.FetchSource.AccessToken != "" {
if err := cr.SetFetchAccessToken(cr.Spec.FetchSource.AccessToken, encryptionKey); err != nil {
return "", err
}
}
// update manifestsRoot in case already exist
if existingCr, err := qConfig.GetCR(cr.GetName()); err == nil {
// cr exists, so update the manifestsRoot if version exist
newV := cr.GetLabelFromCr("version")
if strings.HasSuffix(existingCr.Spec.ManifestsRoot, newV) {
cr.Spec.ManifestsRoot = existingCr.Spec.ManifestsRoot
}
}
// write to disk
if err = qConfig.CreateOrWriteCrAndContext(cr); err != nil {
return "", err
}
qConfig.SetCurrentContextName(cr.GetName())
qConfig.Write()
return cr.GetName(), nil
}

View File

@@ -0,0 +1,140 @@
package qliksense
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func TestLoadCrFile(t *testing.T) {
td := setup()
setup()
sampleCr1 := `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-test
labels:
version: v0.0.2
spec:
git:
repository: https://github.com/ffoysal/qliksense-k8s
accessToken: abababababababaab
userName: "blblbl"
gitOps:
enabled: "no"
schedule: "*/1 * * * *"
watchBranch: pr-branch-db1d26d6
image: qlik-docker-oss.bintray.io/qliksense-repo-watcher
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: mongodb://qlik-default-mongodb:27017/qliksense?ssl=false
profile: docker-desktop
rotateKeys: "yes"`
sampleCr2 := `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-test3
labels:
version: v0.0.2
spec:
git:
repository: https://github.com/ffoysal/qliksense-k8s
accessToken: abababababababaab
userName: "blblbl"
gitOps:
enabled: "no"
schedule: "*/1 * * * *"
watchBranch: pr-branch-db1d26d6
image: qlik-docker-oss.bintray.io/qliksense-repo-watcher
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
value: mongodb://qlik-default-mongodb:27017/qliksense?ssl=false
profile: docker-desktop
rotateKeys: "yes"`
duplicateCr := `
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
labels:
version: v0.0.2
spec:
git:
repository: https://github.com/ffoysal/qliksense-k8s
accessToken: abababababababaab
userName: "blblbl"`
crFile1 := filepath.Join(testDir, "testcr1.yaml")
ioutil.WriteFile(crFile1, []byte(sampleCr1), 0644)
crFile2 := filepath.Join(testDir, "testcr2.yaml")
ioutil.WriteFile(crFile2, []byte(sampleCr2), 0644)
dupCrFile := filepath.Join(testDir, "dupcr.yaml")
ioutil.WriteFile(dupCrFile, []byte(duplicateCr), 0644)
q := New(testDir)
file1, e := os.Open(crFile1)
if e != nil {
t.Log(e)
t.FailNow()
}
if err := q.LoadCr(file1, false); err != nil {
t.Log(err)
t.FailNow()
}
file2, e := os.Open(crFile2)
if e != nil {
t.Log(e)
t.FailNow()
}
if err := q.LoadCr(file2, false); err != nil {
t.Log(err)
t.FailNow()
}
qConfig := qapi.NewQConfig(testDir)
cr, err := qConfig.GetCR("qlik-test")
if err != nil {
t.Log(err)
t.FailNow()
}
if cr.GetName() != "qlik-test" {
t.FailNow()
}
cr, err = qConfig.GetCR("qlik-test3")
if err != nil {
t.Log(err)
t.FailNow()
}
if cr.GetName() != "qlik-test3" {
t.FailNow()
}
if qConfig.Spec.CurrentContext != "qlik-test3" {
t.FailNow()
}
file, e := os.Open(dupCrFile)
if e != nil {
t.Log(e)
t.FailNow()
}
if err := q.LoadCr(file, false); err == nil {
t.FailNow()
}
td()
}

View File

@@ -4,24 +4,38 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func (q *Qliksense) ViewOperatorCrd() {
io.WriteString(os.Stdout, q.GetCRDString())
func (q *Qliksense) ViewOperator() error {
io.WriteString(os.Stdout, q.GetOperatorCRDString())
return nil
}
func (q *Qliksense) ViewOperatorController() error {
io.WriteString(os.Stdout, q.GetOperatorControllerString())
return nil
}
// this will return crd,deployment,role, rolebinding,serviceaccount for operator
func (q *Qliksense) GetCRDString() string {
func (q *Qliksense) GetOperatorCRDString() string {
result := ""
for _, v := range q.getFileList("crd") {
result = q.getYamlFromPackrFile(v)
}
return result
}
func (q *Qliksense) GetOperatorControllerString() string {
result := ""
for _, v := range q.getFileList("crd-deploy") {
result = result + q.getYamlFromPackrFile(v)
}
return result
}
func (q *Qliksense) getYamlFromPackrFile(packrFile string) string {
s, err := q.CrdBox.FindString(packrFile)
if err != nil {
@@ -32,7 +46,7 @@ func (q *Qliksense) getYamlFromPackrFile(packrFile string) string {
func (q *Qliksense) getFileList(resourceType string) []string {
var resList []string
for _, v := range q.CrdBox.List() {
if strings.Contains(v, resourceType+"/") {
if strings.Contains(v, filepath.Join(resourceType, "")) {
resList = append(resList, []string{v}...)
}
}

View File

@@ -8,13 +8,15 @@ import (
// Qliksense is the logic behind the qliksense client
type Qliksense struct {
QliksenseHome string
CrdBox *packr.Box
CrdBox *packr.Box ``
}
// New qliksense client, initialized with useful defaults.
func New(qliksenseHome string) *Qliksense {
return &Qliksense{
qliksenseClient := &Qliksense{
QliksenseHome: qliksenseHome,
CrdBox: packr.New("crds", "./crds"),
}
return qliksenseClient
}

23
pkg/qliksense/repo.go Normal file
View File

@@ -0,0 +1,23 @@
package qliksense
import (
"errors"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) DiscardAllUnstagedChangesFromGitRepo(qConfig *qapi.QliksenseConfig) error {
if qcr, err := qConfig.GetCurrentCR(); err != nil {
return err
} else if version := qcr.GetLabelFromCr("version"); version == "" {
return errors.New("version label is not set in CR")
} else if qcr.Spec.ManifestsRoot == qConfig.BuildRepoPath(version) {
if repo, err := kapis_git.OpenRepository(qcr.Spec.ManifestsRoot); err != nil {
return err
} else if err = kapis_git.DiscardAllUnstagedChanges(repo); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package qliksense
import (
"errors"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) UninstallQK8s(contextName string, skipConfirmation bool) error {
ans := skipConfirmation
if ans == false {
ans = AskForConfirmation("Are You Sure? ")
}
if ans == true {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if contextName == "" {
contextName = qConfig.Spec.CurrentContext
} else if !qConfig.IsContextExist(contextName) {
return errors.New("context name [ " + contextName + " ] not found")
}
str, err := q.getCRString(contextName)
if err != nil {
return err
}
return qapi.KubectlDelete(str, "")
}
return nil
}

47
pkg/qliksense/upgrade.go Normal file
View File

@@ -0,0 +1,47 @@
package qliksense
import (
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) UpgradeQK8s(keepPatchFiles bool) error {
// step1: get CR
// step2: run kustomize
// step3: run kubectl apply
// fetch the version
qConfig := qapi.NewQConfig(q.QliksenseHome)
if !keepPatchFiles {
defer func() {
if err := q.DiscardAllUnstagedChangesFromGitRepo(qConfig); err != nil {
fmt.Printf("error removing temporary changes to the config: %v\n", err)
}
}()
}
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
qcr.Spec.RotateKeys = "no"
dcr, err := qConfig.GetDecryptedCr(qcr)
if err != nil {
return err
}
if dcr.Spec.Git != nil && dcr.Spec.Git.Repository != "" {
// fetching and applying manifest will be in the operator controller
// get decrypted cr
return q.applyCR(dcr)
}
err = q.applyConfigToK8s(dcr)
if err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
}
return q.applyCR(dcr)
}