Compare commits

..

175 Commits

Author SHA1 Message Date
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
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
36008ab0dc Merge branch 'ms-3' into context_delete 2020-02-28 09:36:37 -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
c65fad8f5c updated docs 2020-02-27 14:37:01 -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
74d6863acf delete context change 2020-02-25 15:42:53 -05:00
Sanat Nayar
d261be6c13 delete context change 2020-02-25 11:41:41 -05:00
Sanat Nayar
a3a6c47375 delete context change 2020-02-24 16:41:35 -05:00
Sanat Nayar
b413e1bca9 init 2020-02-20 14:39:52 -05:00
81 changed files with 5915 additions and 903 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

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

@@ -0,0 +1,27 @@
name: Build Sense installer
on: [pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Set GOPATH
# temporary fix
# see https://github.com/actions/setup-go/issues/14
run: |
echo "##[set-env name=GOPATH;]$(dirname $GITHUB_WORKSPACE)"
echo "##[add-path]$(dirname $GITHUB_WORKSPACE)/bin"
shell: bash
- run: make test
- run: make build

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 }}

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

@@ -0,0 +1,37 @@
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@v1
with:
go-version: 1.13
- uses: actions/checkout@v2
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* # Needed in makefile for versioning
- name: Set GOPATH
# temporary fix
# see https://github.com/actions/setup-go/issues/14
run: |
echo "##[set-env name=GOPATH;]$(dirname $GITHUB_WORKSPACE)"
echo "##[add-path]$(dirname $GITHUB_WORKSPACE)/bin"
shell: bash
- run: make test
- run: make xbuild-all
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: bin/**/*

4
.gitignore vendored
View File

@@ -5,3 +5,7 @@ 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

@@ -44,51 +44,63 @@ build: clean generate
$(MAKE) clean
.PHONY: test
test:
test: clean generate
ifeq ($(shell ${WHICH} docker-registry 2>${DEVNUL}),)
$(eval TMP := $(shell mktemp -d))
git clone https://github.com/docker/distribution.git $(TMP)/docker-distribution
cd $(TMP)/docker-distribution; git checkout -b v2.7.1; make
cp $(TMP)/docker-distribution/bin/registry pkg/qliksense/docker-registry
$(eval TMP-docker-distribution := $(shell mktemp -d))
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
cp $(TMP-docker-distribution)/docker-distribution/bin/registry pkg/qliksense/docker-registry
-rm -rf $(TMP-docker-distribution)
endif
go test -short -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)
tar -czvf $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH).tar.gz -C $(BINDIR)/$(VERSION)/ $(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
#tar -C $(BINDIR)/$(VERSION)/ -cvf $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH).tar.gz $(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
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 ./...
HAS_PACKR2 := $(shell 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
clean-packr: packr2
cd pkg/qliksense && packr2 clean
get-crds:
$(eval TMP := $(shell mktemp -d))
git clone git@github.com:qlik-oss/qliksense-operator.git -b ms-3 $(TMP)/operator
ifeq ($(QLIKSENSE_OPERATOR_DIR),)
$(eval TMP-operator := $(shell mktemp -d))
git clone https://github.com/qlik-oss/qliksense-operator.git -b master $(TMP-operator)/operator
$(MAKE) QLIKSENSE_OPERATOR_DIR=$(TMP-operator)/operator get-crds
-rm -rf $(TMP-operator)
else
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
cp $(QLIKSENSE_OPERATOR_DIR)/deploy/*.yaml pkg/qliksense/crds/crd-deploy
cp $(QLIKSENSE_OPERATOR_DIR)/deploy/crds/*_crd.yaml pkg/qliksense/crds/crd
cp $(QLIKSENSE_OPERATOR_DIR)/deploy/crds/*_cr.yaml pkg/qliksense/crds/cr
endif

143
README.md
View File

@@ -1,150 +1,25 @@
# (WIP) Qlik Sense installation and operations CLI
- [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)
- [TL;DR](#TL;DR)
- [How qliksense CLI works](#how-qliksense-cli-works)
- [Witout Git Repo](#Without-git-repo)
- [With Git Repo](#With-a-git-repo)
- [Air Gapped](#air-gaped)
## Documentation
To learn more about Qlik Sense installer go to https://qlik-oss.github.io/sense-installer/
## About
The Qlik Sense installer CLI (qliksense) 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). This cli faciliates to do
The Qlik Sense installer 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 cli facilitates:
- installation of QSEoK
- installation of qliksense operator to manage QSEoK
- air gapped installation of QSEoK
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.
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.
For each version of a qliksense sense edge build there should be a corresponding release in [qliksense-k8s] repository under [releases](https://github.com/qlik-oss/qliksense-k8s/releases)
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
- More operations:
- Expanded preflight checks
- Expand preflight checks
- backup/restore operations
- fully support airgap installation of QSEoK
- restore unwanted deletion of kubernetes resoureces
## Getting Started
### Requirements
- `kubectl` need to be installed and configured properly so that `kubectl` can connect to the kubernetes cluser. The `qliksense` CLI uses `kubectl` under the hood to perform operations on cluster
- (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) and rename it to `qliksense`. All the examplease down below uses `qliksense`.
### TL;DR
- 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"
```
## How qliksense cli works
At the initialization `qliksense` cli create 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 has been created with some default values like this
```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 latter on. New context and configuration can be created by the cli.
```console
$ qliksense config
do operations on/around CR
Usage:
qliksense config [command]
Available Commands:
apply generate the patchs and apply manifests to k8s
list-contexts retrieves the contexts and lists them
set configure a key value pair into the current context
set-configs set configurations into the qliksense context
set-context Sets the context in which the Kubernetes cluster and resources live in
set-secrets set secrets configurations into the qliksense context
view view the qliksense operator CR
Flags:
-h, --help help for config
Use "qliksense config [command] --help" for more information about a command.
```
`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 download the specified version from [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and put it into folder `~/.qliksense/contexts/<context-name>/qlik-k8s`.
The qliksense cli create a CR for the qliksense operator and all the config operations are peformed to edit the CR. So when `qliksense install` or `qliksense config apply` both generate patches in local file system (i.e `~/.qliksense/contexts/<context-name>/qlik-k8s`) and install those manifests into the cluster and create a custom resoruce (CR) for the `qliksene operator` then the operator make association to the isntalled resoruces so that when `qliksenes uninstall` is performed the operator can delete all those kubernetes resources related to QSEoK for the current context.
### With a git repo
User has to create fork or clone of [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and push it to their own git server. When user perform `qliksense install` or `qliksene config apply` the qliksense operator do these tasks
- downloads the corresponding version of manifests from the user's git repo.
- generate kustomize patches
- install kubernetes resoruces
- push those generated patches into a new branch in the provided git repo. so that user user can merge those patches into their master branch.
- spinup a cornjob to monitor master branch. If user modifies anything in the master branch those changes will be applied into the cluster. This is a light weight `git-ops` model
This is how repo info is provided into the CR
```console
qliksense config set git.repository="https://github.com/my-org/qliksense-k8s"
qliksense config set git.accessToken=blablalaala
```
## Air gaped
- restore unwanted deletion of kubernetes resources

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

@@ -1,31 +0,0 @@
# qliksense config
Config action will perform operations on configurations and contexts regarding the [qliksense-k8](https://github.com/qlik-oss/qliksense-k8s) release.
it will support following commands:
- `qliksense config apply` - generate the patchs and apply manifests to k8s
- `qliksense config list-contexts` - retrieves the contexts and lists them
- `qliksense config set` - configure a key value pair into the current context
- `qliksense config set-configs` - set configurations into the qliksense context
- `qliksense config set-context` - sets the context in which the Kubernetes cluster and resources live in
- `qliksense config set-secrets` - set secrets configurations into the qliksense context
- `qliksense config view` - view the qliksense operator CR
the global file that abstracts all the contexts is `config.yaml`, located at: `~/.qliksense/config.yaml`:
```yaml
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: /Users/fff/.qliksense/contexts/qlik-default/qlik-default.yaml
- name: myqliksense
crFile: /Users/fff/.qliksense/contexts/myqliksense/myqliksense.yaml
- name: hello
crFile: /Users/fff/.qliksense/contexts/hello/hello.yaml
currentContext: hello
```

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

@@ -0,0 +1,37 @@
package main
import (
"io"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func applyCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.InstallCommandOptions{}
filePath := ""
keepPatchFiles := 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(reader io.Reader) error {
return q.ApplyCRFromReader(reader, opts, keepPatchFiles, true)
})
},
}
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)
eulaPreRunHooks.addValidator(c.Name(), loadOrApplyCommandEulaPreRunHook)
return c
}

View File

@@ -5,16 +5,22 @@ import (
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "do operations on/around CR",
Long: `do operations on/around CR`,
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 {
@@ -36,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
}

View File

@@ -2,6 +2,9 @@ package main
import (
"errors"
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
@@ -13,9 +16,12 @@ func setContextConfigCmd(q *qliksense.Qliksense) *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>`,
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)
},
@@ -45,9 +51,12 @@ func setOtherConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
)
cmd = &cobra.Command{
Use: "set",
Short: "configure a key value pair into the current context",
Example: `qliksense config set <key>=<value>`,
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)
},
@@ -61,9 +70,12 @@ func setConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
)
cmd = &cobra.Command{
Use: "set-configs",
Short: "set configurations into the qliksense context",
Example: `qliksense config set-configs <key>=<value>`,
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
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.SetConfigs(args)
},
@@ -78,9 +90,18 @@ func setSecretsCmd(q *qliksense.Qliksense) *cobra.Command {
)
cmd = &cobra.Command{
Use: "set-secrets",
Short: "set secrets configurations into the qliksense context",
Example: `qliksense config set-secrets <key>=<value> --secret=true`,
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 `,
RunE: func(cmd *cobra.Command, args []string) error {
return q.SetSecrets(args, secret)
},
@@ -90,6 +111,22 @@ func setSecretsCmd(q *qliksense.Qliksense) *cobra.Command {
return cmd
}
func deleteContextConfigCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
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)
},
}
return cmd
}
func setImageRegistryCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
@@ -140,3 +177,19 @@ qliksense config set-image-registry https://your.private.registry.example.com:50
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
},
}
}

View File

@@ -22,7 +22,7 @@ func crdsViewCmd(q *qliksense.Qliksense) *cobra.Command {
},
}
f := c.Flags()
f.BoolVarP(&opts.All, "all", "a", false, "Include All CRDs")
f.BoolVarP(&opts.All, "all", "", false, "Include All CRDs")
return c
}
@@ -37,6 +37,6 @@ func crdsInstallCmd(q *qliksense.Qliksense) *cobra.Command {
},
}
f := c.Flags()
f.BoolVarP(&opts.All, "all", "a", false, "Include All CRDs")
f.BoolVarP(&opts.All, "all", "", false, "Include All CRDs")
return c
}

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

@@ -0,0 +1,105 @@
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 == "install" || commandName == "upgrade" || commandName == "apply"
}
func globalEulaPreRun(cmd *cobra.Command, q *qliksense.Qliksense) {
if isEulaEnforced(cmd.Name()) {
if strings.TrimSpace(strings.ToLower(cmd.Flag("acceptEULA").Value.String())) != "yes" {
if eulaPreRunHook := eulaPreRunHooks.getValidator(cmd.Name()); 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.Name()) {
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

@@ -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

@@ -7,24 +7,26 @@ import (
func installCmd(q *qliksense.Qliksense) *cobra.Command {
opts := &qliksense.InstallCommandOptions{}
keepPatchFiles := false
c := &cobra.Command{
Use: "install",
Short: "install a qliksense release",
Long: `install a qliksense release`,
Example: `qliksense install <version>`,
Example: `qliksense install <version> #if no version provides, expect manifestsRoot is set somewhere in the file system`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return q.InstallQK8s("", opts)
version := ""
if len(args) != 0 {
version = args[0]
}
return q.InstallQK8s(args[0], opts)
return q.InstallQK8s(version, opts, keepPatchFiles)
},
}
f := c.Flags()
f.StringVarP(&opts.AcceptEULA, "acceptEULA", "a", "", "AcceptEULA for qliksense")
f.StringVarP(&opts.Namespace, "namespace", "n", "", "Namespace where to install the qliksense")
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)
return c
}

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

@@ -0,0 +1,84 @@
package main
import (
"bytes"
"io"
"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(reader io.Reader) error {
return q.LoadCr(reader, 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(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(io.Reader) error) error {
if crBytes := eulaPreRunHooks.getPostValidationArtifact("CR"); crBytes != nil {
return callBack(bytes.NewBuffer(crBytes.([]byte)))
} else {
file, err := getCrFileFromFlag(cmd, "file")
if err != nil {
return err
}
defer file.Close()
return callBack(file)
}
}

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

@@ -0,0 +1,332 @@
package main
import (
"fmt"
"log"
"github.com/qlik-oss/sense-installer/pkg/preflight"
"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 {
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}
// Preflight DNS check
fmt.Printf("Preflight DNS check\n")
fmt.Println("---------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight DNS check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckDns(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Print("Preflight DNS check FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightDnsCmd
}
func pfK8sVersionCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight Kubernetes minimum version check
fmt.Printf("Preflight kubernetes minimum version check\n")
fmt.Println("------------------------------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight kubernetes minimum version check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckK8sVersion(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Printf("Preflight kubernetes minimum version check FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightCheckK8sVersionCmd
}
func pfAllChecksCmd(q *qliksense.Qliksense) *cobra.Command {
var mongodbUrl string
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}
// Preflight run all checks
fmt.Printf("Running all preflight checks\n")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Println(err)
fmt.Printf("Running preflight check suite has FAILED...\n")
log.Fatal()
}
qp.RunAllPreflightChecks(namespace, kubeConfigContents, mongodbUrl)
return nil
},
}
f := preflightAllChecksCmd.Flags()
f.StringVarP(&mongodbUrl, "mongodb-url", "", "", "mongodbUrl to try connecting to")
return preflightAllChecksCmd
}
func pfDeploymentCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight deployments check
fmt.Printf("Preflight deployment check\n")
fmt.Println("--------------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight deployment check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckDeployment(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Print("Preflight deploy check FAILED\n")
log.Fatal()
}
return nil
},
}
return pfDeploymentCheckCmd
}
func pfServiceCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight service check
fmt.Printf("Preflight service check\n")
fmt.Println("-----------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight service check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckService(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Print("Preflight service check FAILED\n")
log.Fatal()
}
return nil
},
}
return pfServiceCheckCmd
}
func pfPodCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight pod check
fmt.Printf("Preflight pod check\n")
fmt.Println("--------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight pod check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckPod(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Print("Preflight pod check FAILED\n")
log.Fatal()
}
return nil
},
}
return pfPodCheckCmd
}
func pfCreateRoleCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight role check
fmt.Printf("Preflight role check\n")
fmt.Println("---------------------------")
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight role check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckCreateRole(namespace); err != nil {
fmt.Println(err)
fmt.Print("Preflight role FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightRoleCmd
}
func pfCreateRoleBindingCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight createRoleBinding check
fmt.Printf("Preflight rolebinding check\n")
fmt.Println("---------------------------")
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight rolebinding check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckCreateRoleBinding(namespace); err != nil {
fmt.Println(err)
fmt.Print("Preflight rolebinding check FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightRoleBindingCmd
}
func pfCreateServiceAccountCheckCmd(q *qliksense.Qliksense) *cobra.Command {
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}
// Preflight createServiceAccount check
fmt.Printf("Preflight ServiceAccount check\n")
fmt.Println("-------------------------------------")
namespace, _, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight serviceaccount check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckCreateServiceAccount(namespace); err != nil {
fmt.Println(err)
fmt.Print("Preflight serviceaccount check FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightServiceAccountCmd
}
func pfCreateRBCheckCmd(q *qliksense.Qliksense) *cobra.Command {
var preflightCreateRBCmd = &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}
// Preflight authcheck
fmt.Printf("Preflight authcheck\n")
fmt.Println("------------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight authcheck FAILED\n")
log.Fatal(err)
}
if err = qp.CheckCreateRB(namespace, kubeConfigContents); err != nil {
fmt.Println(err)
fmt.Print("Preflight authcheck FAILED\n")
log.Fatal()
}
return nil
},
}
return preflightCreateRBCmd
}
func pfMongoCheckCmd(q *qliksense.Qliksense) *cobra.Command {
var mongodbUrl string
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}
// Preflight mongo check
fmt.Printf("Preflight mongo check\n")
fmt.Println("-------------------------------------")
namespace, kubeConfigContents, err := preflight.InitPreflight()
if err != nil {
fmt.Printf("Preflight mongo check FAILED\n")
log.Fatal(err)
}
if err = qp.CheckMongo(kubeConfigContents, namespace, mongodbUrl); err != nil {
fmt.Println(err)
fmt.Print("Preflight mongo check FAILED\n")
log.Fatal()
}
return nil
},
}
f := preflightMongoCmd.Flags()
f.StringVarP(&mongodbUrl, "url", "", "", "mongodbUrl to try connecting to")
return preflightMongoCmd
}

View File

@@ -20,19 +20,7 @@ func pullQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
if err != nil {
return err
}
if version != "" {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if !qConfig.IsRepoExistForCurrent(version) {
if err := q.FetchQK8s(version); err != nil {
return err
}
} else if err := qConfig.SwitchCurrentCRToVersionAndProfile(version, opts.Profile); err != nil {
return err
}
}
return q.PullImagesForCurrentCR()
return q.PullImages(version, opts.Profile)
},
}
f := cmd.Flags()

View File

@@ -2,29 +2,32 @@ package main
import (
"fmt"
"io"
"log"
"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/preflight"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/ttacon/chalk"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
// 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)"
)
func initAndExecute() error {
@@ -39,18 +42,14 @@ func initAndExecute() error {
// create dirs and appropriate files for setting up contexts
api.LogDebugMessage("QliksenseHomeDir: %s", qlikSenseHome)
qliksenseClient, err := qliksense.New(qlikSenseHome)
if err != nil {
qliksenseClient := qliksense.New(qlikSenseHome)
cmd := rootCmd(qliksenseClient)
if err := cmd.Execute(); err != nil {
//levenstein checks (auto-suggestions)
levenstein(cmd)
return err
}
qliksenseClient.SetUpQliksenseDefaultContext()
cmd := rootCmd(qliksenseClient)
//levenstein checks
if levenstein(cmd) == false {
if err := cmd.Execute(); err != nil {
return err
}
}
return nil
}
@@ -80,30 +79,64 @@ func setUpPaths() (string, error) {
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 != "qliksense" && commandName != "help" && commandName != "version"
}
cmd = &cobra.Command{
func getRootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd := &cobra.Command{
Use: "qliksense",
Short: "Qliksense cli tool",
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.Name()) {
globalEulaPreRun(cmd, p)
if err := p.SetUpQliksenseDefaultContext(); err != nil {
panic(err)
}
pf := preflight.NewPreflightConfig(p.QliksenseHome)
if err := pf.Initialize(); err != nil {
panic(err)
}
globalEulaPostRun(cmd, p)
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if commandUsesContext(cmd.Name()) {
globalEulaPostRun(cmd, p)
}
},
}
origHelpFunc := cmd.HelpFunc()
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
if !commandUsesContext(cmd.Name()) {
cmd.Flags().MarkHidden("acceptEULA")
}
origHelpFunc(cmd, args)
})
accept := ""
cmd.PersistentFlags().StringVarP(&accept, "acceptEULA", "a", "", "Accept EULA for qliksense")
cmd.Flags().SetInterspersed(false)
return cmd
}
func initConfig() {
viper.SetEnvPrefix("QLIKSENSE")
viper.AutomaticEnv()
}
func rootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd := getRootCmd(p)
cobra.OnInitialize(initConfig)
// For qliksense overrides/commands
cmd.AddCommand(getInstallableVersionsCmd(p))
cmd.AddCommand(pullQliksenseImages(p))
cmd.AddCommand(pushQliksenseImages(p))
cmd.AddCommand(about(p))
@@ -123,6 +156,7 @@ func rootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd.AddCommand(installCmd(p))
// add config command
configCmd := configCmd(p)
cmd.AddCommand(configCmd)
configCmd.AddCommand(configApplyCmd(p))
configCmd.AddCommand(configViewCmd(p))
@@ -147,9 +181,17 @@ func rootCmd(p *qliksense.Qliksense) *cobra.Command {
// 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))
@@ -157,40 +199,27 @@ func rootCmd(p *qliksense.Qliksense) *cobra.Command {
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(pfCreateRBCheckCmd(p))
cmd.AddCommand(preflightCmd)
cmd.AddCommand(loadCrFile(p))
cmd.AddCommand((applyCmd(p)))
return cmd
}
func initConfig() {
viper.SetEnvPrefix("QLIKSENSE")
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()
// Get the data
if resp, err = http.Get(url); err != nil {
return err
}
defer resp.Body.Close()
// Write the body to file
if _, err = io.Copy(out, resp.Body); err != nil {
return err
}
return nil
}
func copy(src, dst string) (int64, error) {
var (
source, destination *os.File
@@ -219,28 +248,21 @@ func copy(src, dst string) (int64, error) {
return nBytes, err
}
func levenstein(cmd *cobra.Command) bool {
cmd.SuggestionsMinimumDistance = 4
func levenstein(cmd *cobra.Command) {
cmd.SuggestionsMinimumDistance = 2
if len(os.Args) > 1 {
args := os.Args[1]
for _, ctx := range cmd.Commands() {
val := *ctx
if args == val.Name() {
//found command
return false
}
}
suggest := cmd.SuggestionsFor(os.Args[1])
suggest := cmd.SuggestionsFor(args)
if len(suggest) > 0 {
arg := []string{}
for _, cm := range os.Args {
arg = append(arg, cm)
}
arg[1] = suggest[0]
out := ansi.NewColorableStdout()
fmt.Fprintln(out, chalk.Green.Color("Did you mean: "), chalk.Bold.TextStyle(strings.Join(arg, " ")), "?")
return true
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, " ")), "?")
}
}
}
return false
}

View File

@@ -1,7 +1,6 @@
package main
import (
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
@@ -9,7 +8,7 @@ import (
func uninstallCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "uninstall",
Short: "Uninstall the deployed qliksense with release name [ " + qapi.NewQConfig(q.QliksenseHome).Spec.CurrentContext + " ]",
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 {

View File

@@ -6,15 +6,18 @@ import (
)
func upgradeCmd(q *qliksense.Qliksense) *cobra.Command {
keepPatchFiles := false
c := &cobra.Command{
Use: "upgrade",
Short: "upgrade qliksense release",
Long: `upgrade qliksense release`,
Example: `qliksense upgrade <version>`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.UpgradeQK8s()
return q.UpgradeQK8s(keepPatchFiles)
},
}
f := c.Flags()
f.BoolVar(&keepPatchFiles, keepPatchFilesFlagName, keepPatchFiles, keepPatchFilesFlagUsage)
return c
}

0
docs/air_gap.md Normal file
View File

119
docs/command_reference.md Normal file
View File

@@ -0,0 +1,119 @@
# qliksense command reference
## qliksense apply
`qliksense apply` command takes input a cr file or input from pipe
- `qliksense apply -f cr-file.yaml`
- `cat cr-file.yaml | qliksense apply -f -`
the content of `cr-file.yaml` should be something similar
```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"
```
This will do everything `qliksense load` does and install the qliksense into the cluster.
## qliksense load
`qliksense load` command takes input a cr file or input from pipe.
- `qliksense load -f cr-file.yaml`
- `cat cr-file.yaml | qliksense load -f -`
This will load the 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 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
## qliksense config
Config action will perform operations on configurations and contexts regarding the [qliksense-k8](https://github.com/qlik-oss/qliksense-k8s) release.
it will support following commands:
- `qliksense config apply` - generate the patchs and apply manifests to k8s
- `qliksense config list-contexts` - retrieves the contexts and lists them
- `qliksense config set` - configure a key value pair into the current context
- `qliksense config set-configs` - set configurations into the qliksense context as key-value pairs
- `qliksense config set-context` - sets the context in which the Kubernetes cluster and resources live in
- `qliksense config set-secrets <service_name>.<attribute>="<value>" --secret=false` - set secrets configurations into the 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 the 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 in-cluster secrets)
the global file that abstracts all the contexts is `config.yaml`, located at: `~/.qliksense/config.yaml`:
```yaml
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: QliksenseConfigMetadata
spec:
contexts:
- name: qlik-default
crFile: /Users/fff/.qliksense/contexts/qlik-default/qlik-default.yaml
- name: myqliksense
crFile: /Users/fff/.qliksense/contexts/myqliksense/myqliksense.yaml
- name: hello
crFile: /Users/fff/.qliksense/contexts/hello/hello.yaml
currentContext: hello
```

97
docs/concepts.md Normal file
View File

@@ -0,0 +1,97 @@
# How qliksense 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 peformed 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 resoruce (CR) for the `qliksene operator`.
The operator makes the association to the installed resoruces 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 Sense installer
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"
```

22
docs/index.md Normal file
View File

@@ -0,0 +1,22 @@
# Overview
The Qlik Sense installer 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 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
## Future Direction
Operations:
- Expand preflight checks
- Backup/restore operations
- Fully support airgap installation of QSEoK
- Restore unwanted deletion of kubernetes resources

253
docs/preflight_checks.md Normal file
View File

@@ -0,0 +1,253 @@
##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.
The suite consists of a set of `collectors` which run the specifications of every test and `analyzers` which analyze the results of every test run by the collector.
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:
```console
$ 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.
```console
$ 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:
```console
$ 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.
```console
$ 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.
```console
$ 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.
```console
$ 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
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("64f639d3-2c93-4894-80f6-ee14acaf56a5") }
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.
```console
$ qliksense preflight all --mongodb-url=<url> OR
$ qliksense preflight all
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
```

19
go.mod
View File

@@ -10,12 +10,13 @@ replace (
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.20200206224201-2e697eccbad9
sigs.k8s.io/kustomize/api => github.com/qlik-oss/kustomize/api v0.3.3-0.20200402170547-2e8140160c36
)
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/Shopify/ejson v1.2.1
github.com/aws/aws-sdk-go v1.28.9 // indirect
github.com/bugsnag/bugsnag-go v1.5.3 // indirect
@@ -33,29 +34,33 @@ require (
github.com/golang/protobuf v1.3.3 // indirect
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
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/qlik-oss/k-apis v0.0.11
github.com/pkg/errors v0.8.1
github.com/qlik-oss/k-apis v0.0.34
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/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
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/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect
golang.org/x/tools v0.0.0-20200228135638-5c7c66ced534 // indirect
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
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/yaml.v2 v2.2.8
gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555
k8s.io/api v0.17.0
k8s.io/apimachinery v0.17.0
k8s.io/client-go v11.0.0+incompatible
k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51
sigs.k8s.io/kustomize/api v0.3.2
sigs.k8s.io/yaml v1.1.0
)

46
go.sum
View File

@@ -131,6 +131,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
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-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
@@ -380,7 +382,6 @@ 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 v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
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=
@@ -562,6 +563,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=
@@ -569,6 +572,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=
@@ -697,9 +701,13 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
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-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=
@@ -844,14 +852,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.11 h1:dhbcH1+8r14OMlTSrP2RAlr+707QXcrj1iVnA/y9r5I=
github.com/qlik-oss/k-apis v0.0.11/go.mod h1:KOFzKVIdRqp47ytnHg3+9zb8fTlnrQjO6aKiwcrCJUE=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200206224201-2e697eccbad9 h1:iqeqTS4zjp6rPEaxmFB7pemA2CMjOEN5dYSXZaQ82uw=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200206224201-2e697eccbad9/go.mod h1:OCt7FTrRVHj4kmR2xLJJUIqu00BTr6GeF09hSmM17Kw=
github.com/qlik-oss/k-apis v0.0.34 h1:lOC21wz/nNZNmSfTXZSJCOm1BulaZfdg7tAuYb7knAE=
github.com/qlik-oss/k-apis v0.0.34/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200401055330-fa528324112a h1:Vzod5XB+e25ENy5Lse0pXNmSYSDFxSEYhH/6Sj7twPg=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200401055330-fa528324112a/go.mod h1:tSQaDZ4Jt9KwYvD7LlMUPi5nkiGOno3PAKl5/XqEfxs=
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/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/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=
@@ -956,6 +968,7 @@ 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=
@@ -990,6 +1003,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
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/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-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.6 h1:qMJQYPNdtJ7UNYHjX38KXZtltKTqimMuoQjNnSVIuJg=
@@ -1041,8 +1056,8 @@ golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPh
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-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/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=
@@ -1077,6 +1092,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
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=
@@ -1117,6 +1134,8 @@ 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/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=
@@ -1194,21 +1213,14 @@ golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtn
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-20200224181240-023911ca70b2 h1:L/G4KZvrQn7FWLN/LlulBtBzrLUhqjiGfTWWDmrh+IQ=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200225230052-807dcd883420 h1:4RJNOV+2rLxMEfr6QIpC7GEv9MjD6ApGXTCLrNF9+eA=
golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200226205201-eb7c56241bdb h1:RXjcsi6scaPhM5uXm7JRqP2JibKvbgMqx9zDLDB9voM=
golang.org/x/tools v0.0.0-20200226205201-eb7c56241bdb/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d h1:7M9AXzLrJWWGdDYtBblPHBTnHtaN6KKQ98OYb35mLlY=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200228135638-5c7c66ced534 h1:XVzrScQUlfS6ssloilmEJdJhlMDtnculCx+0zmVHSA8=
golang.org/x/tools v0.0.0-20200228135638-5c7c66ced534/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=

24
mkdocs.yml Normal file
View File

@@ -0,0 +1,24 @@
site_name: Qlik Sense installer
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
- preflight_checks.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

@@ -8,9 +8,13 @@ import (
"log"
"os"
"path/filepath"
"strings"
"github.com/qlik-oss/k-apis/pkg/config"
b64 "encoding/base64"
"github.com/jinzhu/copier"
"gopkg.in/yaml.v2"
)
const (
@@ -18,38 +22,44 @@ const (
pullSecretFileName = "image-registry-pull-secret.yaml"
qliksenseContextsDirName = "contexts"
qliksenseSecretsDirName = "secrets"
qliksenseEjsonDirName = "ejson"
)
// 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)
}
qc.QliksenseHomePath = qsHome
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 := qc.getCRFilePath(contextName)
crFilePath := qc.GetCRFilePath(contextName)
if crFilePath == "" {
return nil, errors.New("context name " + contextName + " not found")
}
return getCRObject(crFilePath)
}
func getUnencryptedCR() {
return qc.GetAndTransformCrObject(crFilePath)
}
// GetCurrentCR create a QliksenseCR object for current context
@@ -58,14 +68,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}...)
@@ -76,14 +86,36 @@ 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
@@ -91,16 +123,27 @@ func getCRObject(crfile string) (*QliksenseCR, error) {
return cr, nil
}
func (qc *QliksenseConfig) getCRFilePath(contextName string) string {
func (qc *QliksenseConfig) GetCRFilePath(contextName string) string {
crFilePath := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = ctx.CrFile
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 (qc *QliksenseConfig) IsRepoExist(contextName, version string) bool {
if _, err := os.Lstat(qc.BuildRepoPathForContext(contextName, version)); err != nil {
return false
@@ -120,29 +163,71 @@ func (qc *QliksenseConfig) BuildRepoPath(version string) string {
}
func (qc *QliksenseConfig) BuildRepoPathForContext(contextName, version string) string {
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, contextName, "qlik-k8s", version)
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, contextName string) error {
crf := qc.getCRFilePath(contextName)
func (qc *QliksenseConfig) WriteCR(cr *QliksenseCR) error {
crf := qc.GetCRFilePath(cr.GetName())
if crf == "" {
return errors.New("context name " + contextName + " not found")
return errors.New("context name " + cr.GetName() + " not found")
}
by, err := yaml.Marshal(cr)
if err != nil {
fmt.Println("cannot marshal cr ", err)
return err
}
ioutil.WriteFile(crf, by, 0644)
return nil
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, qc.Spec.CurrentContext)
return qc.WriteCR(cr)
}
func (qc *QliksenseConfig) IsContextExist(ctxName string) bool {
@@ -158,7 +243,7 @@ func (qc *QliksenseConfig) GetCurrentContextDir() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, qcr.Metadata.Name), nil
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, qcr.GetObjectMeta().GetName()), nil
}
}
@@ -193,10 +278,30 @@ func (qc *QliksenseConfig) SetPullDockerConfigJsonSecret(dockerConfigJsonSecret
}
func (qc *QliksenseConfig) GetPushDockerConfigJsonSecret() (*DockerConfigJsonSecret, error) {
return qc.GetDockerConfigJsonSecret(pushSecretFileName)
return qc.getDockerConfigJsonSecret(pushSecretFileName)
}
func (qc *QliksenseConfig) GetDockerConfigJsonSecret(name string) (*DockerConfigJsonSecret, error) {
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
@@ -211,6 +316,15 @@ func (qc *QliksenseConfig) GetDockerConfigJsonSecret(name string) (*DockerConfig
}
func (qc *QliksenseConfig) getCurrentContextEncryptionKeyPairLocation() (string, error) {
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
return qc.getContextEncryptionKeyPairLocation(qcr.GetName())
}
}
func (qc *QliksenseConfig) getContextEncryptionKeyPairLocation(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") != "" {
@@ -219,25 +333,40 @@ func (qc *QliksenseConfig) getCurrentContextEncryptionKeyPairLocation() (string,
} else {
// QLIKSENSE_KEY_LOCATION has not been set, hence storing key pair in default location:
// /.qliksense/secrets/contexts/<current-context>/secrets/
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
secretKeyPairLocation = filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, qcr.Metadata.Name, qliksenseSecretsDirName)
}
secretKeyPairLocation = filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, contextName, qliksenseSecretsDirName)
}
LogDebugMessage("SecretKeyLocation to store key pair: %s", secretKeyPairLocation)
return secretKeyPairLocation, nil
}
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) GetCurrentContextEncryptionKeyPair() (*rsa.PublicKey, *rsa.PrivateKey, error) {
secretKeyPairLocation, err := qc.getCurrentContextEncryptionKeyPairLocation()
if qcr, err := qc.GetCurrentCR(); err != nil {
return nil, nil, err
} else {
return qc.GetContextEncryptionKeyPair(qcr.GetName())
}
}
func (qc *QliksenseConfig) GetContextEncryptionKeyPair(contextName string) (*rsa.PublicKey, *rsa.PrivateKey, error) {
secretKeyPairLocation, err := qc.getContextEncryptionKeyPairLocation(contextName)
if err != nil {
return nil, nil, err
}
publicKeyFilePath := filepath.Join(secretKeyPairLocation, QliksensePublicKey)
privateKeyFilePath := filepath.Join(secretKeyPairLocation, QliksensePrivateKey)
// try to create the dir if it doesn't exist
if !FileExists(publicKeyFilePath) || !FileExists(privateKeyFilePath) {
LogDebugMessage("Qliksense secretKeyLocation dir does not exist, creating it now: %s", secretKeyPairLocation)
@@ -271,22 +400,20 @@ func (qc *QliksenseConfig) GetCurrentContextEncryptionKeyPair() (*rsa.PublicKey,
}
func (cr *QliksenseCR) AddLabelToCr(key, value string) {
if cr.Metadata.Labels == nil {
cr.Metadata.Labels = make(map[string]string)
m := cr.GetObjectMeta().GetLabels()
if m == nil {
m = make(map[string]string)
}
cr.Metadata.Labels[key] = value
m[key] = value
cr.GetObjectMeta().SetLabels(m)
}
func (cr *QliksenseCR) GetLabelFromCr(key string) string {
val := ""
if cr.Metadata.Labels != nil {
val = cr.Metadata.Labels[key]
}
return val
return cr.GetObjectMeta().GetLabels()[key]
}
func (cr *QliksenseCR) GetString() (string, error) {
out, err := yaml.Marshal(cr)
out, err := K8sToYaml(cr)
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return "", err
@@ -304,3 +431,111 @@ func (cr *QliksenseCR) GetImageRegistry() string {
}
return ""
}
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)
_, rsaPrivateKey, err := qc.GetCurrentContextEncryptionKeyPair()
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 := Decrypt(b, rsaPrivateKey)
if err != nil {
return nil, err
}
newNvs = append(newNvs, config.NameValue{
Name: nv.Name,
Value: string(db),
})
}
}
finalSecrets[k] = newNvs
}
newCr.Spec.Secrets = finalSecrets
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,9 @@
package api
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
@@ -45,12 +47,17 @@ metadata:
spec:
profile: docker-desktop
manifestsRoot: /Users/mqb/.qliksense/contexts/contx1/qlik-k8s/v0.0.1/manifests
namespace: myqliksense
storageClassName: efs
configs:
qliksense:
- name: acceptEULA
value: "yes"
secrets:
qliksense:
- name: mongoDbUri
# this is rsa encrypted value, the pub and pri keys are in setuPublicAndPrivateKey() method
# actual value: mongodb://qlik-default-mongodb:27017/qliksense?ssl=false
value: n/pDi7Z/A3i16cAHFFwMp19/egNKc8WZxm6MKHLT/B1DMv3U6pDXWyXT5fYYDV1wDTO3Vk43yECST1UgZYmMpgUOwgSfGgqTVi2VqS0JQsnwI+Twwhnvha8RJANX8b/XIoSFVWaOgy7+RP35ZkvOqHdCfC2aT8JMIHgBQqqCbsNgimCuRSxi0klR000ic/Tp5PYSz5mD+WLrkPw2FbS0OVBsQ/hIp5GZrmVpvEOZdbT63Sz+n/G4Br6GTv2LkZcU7JBuKQm2wfB+mRjJmJnNrPawLfn2UZ89Rz0BLwIy+6b24/RoIUgoNowfGkJreGiwItGK8fjCcx11oavK/yAo6pYZXCcru46pmHbxxle1OlkdTKkG6EVtJuKjSZXtVmBHZYRFzsR7HnAiXnL7QzSEcS7ieZlQvTmNLfpidJhK199oSbyKREqXGl2S8DzPKM9RLccVbQTy6X8qWimP3MYCnO4K0KoQnNQAgfuV8ZxnvdDecByLDPIpmFMGy0Xm9pUZWxmSoDBq+p5WBI2HdCX2gCYVv5yxS2iBqO5SMKo8iOglHtPI9NIMvloERdN1vZtxSRkY5uDEfrU9ysYwfayEXxvXmdWv0HxlotcgUinP02j7k+OfIapTmY/jGfvF4euyCGRKuJ9JlSD9pIiRdAcekjL6hCxXLJLdajCV4sL/YDo=
`
ctx1Dir := filepath.Join(homeDir, "contexts", "contx1")
crFile := filepath.Join(ctx1Dir, "contx1.yaml")
@@ -67,7 +74,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 +90,136 @@ 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()
if err != nil {
t.Fail()
t.Log(err)
}
setuPublicAndPrivateKey(dir)
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()
}
td()
}
func setuPublicAndPrivateKey(homeDir string) ([]byte, []byte, error) {
privKeyBytes := []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAwUCimKCidbF3UxEHPy8K+hvhklRB9JYhj5sJy0if4lTVibkK
1MrYCykOnmC40pPU9GLY1b8HxAg9tvyRn0YHUxOra6vVQaVcOVJhTM8D18d+lSr3
Lp1yiX+UGT4nzWI9+R1CCbwXrqeQVoZs6QZKynEXMkFI9/wNMOwPOvQFOSTuoEoC
O+zyTyUWEkNbUq825ELUQdIsjgmlWUOONudxsAr7ESRXW9QTHVh6uWmr3VRKZHby
1JdU3I/wjdlGg5M2dDuXy5nQO9w/nYLjJXiw+zzOetZ/+t7/VOkOpNTeJQhwTM1W
F7Y2VLetbi9FHgyzHatrduh07+XEiTbgDf3GIx2bp2p6oh0G3N2zpiLcK/aZj8ro
uWWydfFfsU3MZ4FfJDP8I6b9awxjmKYqIr6hiPQCJaLBED8mwK+I5evIbnKv6E6u
K+BApWA/R7ElragoFYbqQ1VpvntVMtJt9Dy5ZrI+IQARdXD3bb34oh0IPBhClnvv
MUc1cWxDoXEX6oJ4I+LzxE87Zkwnan9qOwengolMVKFwPx1o37qrbmrXID21kKt7
FL6xN4HxHLkItr1fKzdyWDFRHgASTAWfx5BIwvPuUW0vZHkvO80VyV2L63whVhPn
PASmFkbviomrBttYfpr2aGQqF/qR1Nlxe834MFxk1pS9LMa/WnzvFr0gWakCAwEA
AQKCAgARSp9B2N2wejibDiL/3E23I1eDqFZedDB8kPrHXbAwqDaTJCN79spt9TaB
pVXkQaYEV/Pe7EDdoX8kKGU/QxzUqiXkdHOYdBtUZbKfFMbbP9ZrsnR7j0r4UpoF
yDH3hprU93E5PcNAtW2M0GpeT1nR01yn+n908PCdOAIE3GC7RDq1zOl2QzVLL55R
9ATv2Q2oTvJ/ETc7XlGVMx4+e2cIwXLFjeLjLI6pSYlxnarrGuetJZeEviWxto9n
odFVZI6yx8JFTXX8ZTCr/1IjwDDVyhMPmrHI2Lsv9cqBpSpbVe32cUkKxhsGaYjz
GvesQKamOPhco2ATNxPm0yopFlPsGKMfVl0BK0J6BqFh1BvU/SYJmXfnFuUNO3vV
4u2Saa0q1iddxV0rXDwIqUfn+S6rwzK0G7y8bH2yvpB2VwiG3TFPnULep4wsefNq
Fj92kqFBjacGpQLEEslUY0CMgeZ2+NuBQSUTscP3wBRsottMR6YXJtINdvfHBx+e
EcN71z8D00w3mYqIQ7qb4Ml6HOqknunn58g43L9sACMUMTlEBXa9pUnScNYgWBAz
W2q2mH37cIydM2JRZPpA8B4yTHt5ugJmChwyNFM7941arjKrebH+6AzLkofGedOP
zg+vZQuPEXWs+3MBBnkWoyJW3Y0fbQdjsuQTtnd+7iyoxoBroQKCAQEA4dIiFlIS
MDfRhQQWSiDvaw9aneDEJ3uo63ZRH5tm/IynLgtjYgEm/ZxlBCQgqRKLYELBxhu8
SaF0uPK8pmpFJt0mIwSlsdeVhuE2obQeKUCczaqrKeaHS3PdWLjTlwph81BGRkHy
qfqtNylyyMxrdEbnR51EtsWgFq6anTUAui1Q09JMuMNZRMOzDs1F4gExgD22rc0V
c9YQ+jHJRxBGtNKMpMEqc8cvaxBidbItrN9SMTSWog7uYPBuEuaJ6K9vpgyJMOzJ
SYcQEFGqgIqIDCg+ABE4d/4YROMKZ1DV/bJCind9brUHSx6XALsF0nC5c1Q9TnUL
qI2khOwts4KYKwKCAQEA2xRC6Az97Vkdzu7BjLJ1FKmx4S2nEEgVS12ds82U+5Xf
BHKAJnjqlqmmpzzJG+d77IYktz0+mey1QCNkqlm2fhuKs8LZMnpZRf0l8VcoBsUP
/xKz7wfiE7RRFZtLJhPp4hhe43GzX5/JFMWMnC6UykwQbj4t1E/GNM/Suqwvg12M
wktAJ6nqLgfhjQSO4xWo+nPzcbX+fNtrPCZVrBhYXihhcwRRNImWUCGJ6J4LMdPY
Y9Z59qhOvE9cReH/Xw1av46omyiSyAqlgPyZ/kzA2IJSqYCjiQR/2+RD/g13jpcJ
jatXLVZ8MJSL5OTS40G/HHTNNpNHbKKh0GOyxBA3ewKCAQBAn8UXhCcmW2L/YPsL
/b7mcX9qPP+FmRLvR23R0MQ5M/tH5wRq8I969n3GIJykJeVzB8eybQ+GNslTgEvS
iAkAJTubu+G7MkndTqg2wHf9MDtvdA8Fr646Po8yq7oJuHPtkKR7yLWsRUu6xIbP
xgheP0hCq1QVxhqZQyCGKrvpi7xc0gsYuPbcAfFFJCOCmPrUi1SzCkTAYJt9LjA+
wP6rErIjGBCRD4iXaBn1OqdtmH9KC5WsDP/VCBlIGWeQCly2NVIxiSHVg+xp7yUP
IhXq/L05gbQaSsIhPKQmivCiaJg4The8TdwneDqYf+0bmxzHT203/bD3bImPbJNr
ksz/AoIBAEwu4Y1cZzkQUmNRd5D7xecnk6ngfEYXKwCIT3zlMrfCSEl9n77BMaKu
4Dsr0iuX9eosQ7xM2eYhAG6LYEg05lc4MKWOToVVMpI6E+W3Dz47bPKgiF3I+f8s
Jz5CQIG/TwfGvciOE3hfUkec4ua09BzdEqGjkcBQ9XYMBxXPJr6h2379OBQS7FKR
fwfQ2/dv4tElXTTfut2kV8gU9Jnh5Wjo1epvR+XjKpg28YQo4W+0YX1magcyRB8L
4eSTUIC3XiVa8Jr0IwbZXPBb5xkdi7o+p4w2JahSHjxTRqmj+T1mnHXdbXVgq9Mg
9Pzl7cgFZvX4UBx4XtASRf73jITNtt0CggEADH9K+O7FrIOSQly0sMvsRCMtejp3
o+MDh1Q+vEg2kEgNXjS4ZFVljUpM2kg1OdUz7feS4dLXUJiIQ8ZWtZPedcq7wjHd
02he5+s06l0jPifN3tX1ADfXGpXg5R2fbkrIzakkPP5/RO/aDxIUo7qhklNsVTXO
VlGGfWLdk0ekA4upKm02Q1+YOlbIcAicEYYY8K7IffUwnohzKwL9yfuGi1VKTXpE
4fzdegsHI03FSqR7V+LvtBpIupQ7RO4kuBmCEyI4E9FVknchg4te4gO3qwd9y0rJ
Gu7HNIOrwOHzviI7J6Nd/l9MmeKqklHSgJvko/f5TmiXuQQ8xDZf84rcjQ==
-----END RSA PRIVATE KEY-----
`)
publicKeyBytes := []byte(`-----BEGIN RSA PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwUCimKCidbF3UxEHPy8K
+hvhklRB9JYhj5sJy0if4lTVibkK1MrYCykOnmC40pPU9GLY1b8HxAg9tvyRn0YH
UxOra6vVQaVcOVJhTM8D18d+lSr3Lp1yiX+UGT4nzWI9+R1CCbwXrqeQVoZs6QZK
ynEXMkFI9/wNMOwPOvQFOSTuoEoCO+zyTyUWEkNbUq825ELUQdIsjgmlWUOONudx
sAr7ESRXW9QTHVh6uWmr3VRKZHby1JdU3I/wjdlGg5M2dDuXy5nQO9w/nYLjJXiw
+zzOetZ/+t7/VOkOpNTeJQhwTM1WF7Y2VLetbi9FHgyzHatrduh07+XEiTbgDf3G
Ix2bp2p6oh0G3N2zpiLcK/aZj8rouWWydfFfsU3MZ4FfJDP8I6b9awxjmKYqIr6h
iPQCJaLBED8mwK+I5evIbnKv6E6uK+BApWA/R7ElragoFYbqQ1VpvntVMtJt9Dy5
ZrI+IQARdXD3bb34oh0IPBhClnvvMUc1cWxDoXEX6oJ4I+LzxE87Zkwnan9qOwen
golMVKFwPx1o37qrbmrXID21kKt7FL6xN4HxHLkItr1fKzdyWDFRHgASTAWfx5BI
wvPuUW0vZHkvO80VyV2L63whVhPnPASmFkbviomrBttYfpr2aGQqF/qR1Nlxe834
MFxk1pS9LMa/WnzvFr0gWakCAwEAAQ==
-----END RSA PUBLIC KEY-----
`)
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)
privKeyFile := filepath.Join(secretKeyPairDir, "qliksensePriv")
// construct and write priv key file into secretsDir location
err := ioutil.WriteFile(privKeyFile, privKeyBytes, 0777)
if err != nil {
log.Printf("Error while creating file: %v", err)
return nil, nil, err
}
pubKeyFile := filepath.Join(secretKeyPairDir, "qliksensePub")
// construct and write pub key file into secretsDir location
err = ioutil.WriteFile(pubKeyFile, publicKeyBytes, 0777)
if err != nil {
log.Printf("Error while creating file: %v", err)
return nil, nil, err
}
return publicKeyBytes, privKeyBytes, nil
}

View File

@@ -1,51 +1,56 @@
package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"github.com/qlik-oss/k-apis/pkg/config"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime/schema"
machine_yaml "k8s.io/apimachinery/pkg/util/yaml"
)
const (
QliksenseConfigApiVersion = "config.qlik.com/v1"
QliksenseConfigKind = "QliksenseConfig"
QliksenseContextApiVersion = "qlik.com/v1"
QliksenseContextKind = "Qliksense"
QliksenseDefaultProfile = "docker-desktop"
DefaultRotateKeys = "yes"
QliksenseMetadataName = "QliksenseConfigMetadata"
DefaultMongoDbUri = "mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"
DefaultMongoDbUriKey = "mongoDbUri"
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.ApiVersion = QliksenseContextApiVersion
qliksenseCR.Kind = QliksenseContextKind
if qliksenseCR.Metadata == nil {
qliksenseCR.Metadata = &Metadata{}
qliksenseCR.SetGroupVersionKind(schema.GroupVersionKind{
Group: QliksenseGroup,
Kind: QliksenseKind,
Version: QliksenseApiVersion,
})
qliksenseCR.SetName(contextName)
qliksenseCR.Spec = &config.CRSpec{
Profile: QliksenseDefaultProfile,
RotateKeys: DefaultRotateKeys,
}
if qliksenseCR.Metadata.Name == "" {
qliksenseCR.Metadata.Name = contextName
}
qliksenseCR.Spec = &config.CRSpec{}
qliksenseCR.Spec.Profile = QliksenseDefaultProfile
qliksenseCR.Spec.ReleaseName = contextName
qliksenseCR.Spec.RotateKeys = DefaultRotateKeys
qliksenseCR.Spec.AddToSecrets("qliksense", DefaultMongoDbUriKey, DefaultMongoDbUri, "")
}
// AddBaseQliksenseConfigs adds configs into config.yaml
func (qliksenseConfig *QliksenseConfig) AddBaseQliksenseConfigs(defaultQliksenseContext string) {
qliksenseConfig.ApiVersion = QliksenseConfigApiVersion
qliksenseConfig.Kind = QliksenseConfigKind
if qliksenseConfig.Metadata == nil {
qliksenseConfig.Metadata = &Metadata{}
}
qliksenseConfig.Metadata.Name = QliksenseMetadataName
qliksenseConfig.SetGroupVersionKind(schema.GroupVersionKind{
Group: QliksenseConfigApiGroup,
Kind: QliksenseConfigKind,
Version: QliksenseConfigApiVersion,
})
qliksenseConfig.SetName(QliksenseMetadataName)
if defaultQliksenseContext != "" {
if qliksenseConfig.Spec == nil {
qliksenseConfig.Spec = &ContextSpec{}
@@ -79,13 +84,12 @@ func WriteToFile(content interface{}, targetFile string) error {
return nil
}
x, err := yaml.Marshal(content)
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 {
@@ -101,14 +105,22 @@ func ReadFromFile(content interface{}, sourceFile string) error {
if content == nil || sourceFile == "" {
return nil
}
contents, err := ioutil.ReadFile(sourceFile)
if err != nil {
err = fmt.Errorf("There was an error reading from file: %s, %v", sourceFile, err)
return err
file, e := os.Open(sourceFile)
if e != nil {
return e
}
if err := yaml.Unmarshal(contents, content); err != nil {
err = fmt.Errorf("An error occurred during unmarshalling: %v", err)
return err
}
return nil
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

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/qlik-oss/k-apis/pkg/config"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
@@ -12,6 +13,26 @@ var (
)
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
@@ -27,27 +48,7 @@ func TestAddCommonConfig(t *testing.T) {
qliksenseCR: &QliksenseCR{},
contextName: "myqliksense",
},
want: &QliksenseCR{
CommonConfig: CommonConfig{
ApiVersion: QliksenseContextApiVersion,
Kind: QliksenseContextKind,
Metadata: &Metadata{
Name: "myqliksense",
},
},
Spec: &config.CRSpec{
Profile: QliksenseDefaultProfile,
ReleaseName: "myqliksense",
RotateKeys: DefaultRotateKeys,
Secrets: map[string]config.NameValues{
"qliksense": []config.NameValue{{
Name: DefaultMongoDbUriKey,
Value: DefaultMongoDbUri,
},
},
},
},
},
want: q,
},
}
for _, tt := range tests {
@@ -61,6 +62,18 @@ func TestAddCommonConfig(t *testing.T) {
}
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
@@ -76,18 +89,7 @@ func TestAddBaseQliksenseConfigs(t *testing.T) {
qliksenseConfig: &QliksenseConfig{},
defaultQliksenseContext: "qlik-default",
},
want: &QliksenseConfig{
CommonConfig: CommonConfig{
ApiVersion: QliksenseConfigApiVersion,
Kind: QliksenseConfigKind,
Metadata: &Metadata{
Name: QliksenseMetadataName,
},
},
Spec: &ContextSpec{
CurrentContext: "qlik-default",
},
},
want: qc,
},
}
for _, tt := range tests {

View File

@@ -28,7 +28,6 @@ func (kdcjt *k8sDockerConfigJsonType) GenerateAuth() {
type DockerConfigJsonSecret struct {
Name string
Namespace string
Uri string
Username string
Password string
@@ -51,9 +50,13 @@ func (d *DockerConfigJsonSecret) ToYaml(encryptionKey *rsa.PublicKey) ([]byte, e
if err != nil {
return nil, err
}
k8sDockerConfigJsonMapEncryptedBytes, err := Encrypt(k8sDockerConfigJsonMapBytes, encryptionKey)
if err != nil {
return nil, err
var k8sDockerConfigJsonMapMaybeEncryptedBytes []byte
if encryptionKey != nil {
if k8sDockerConfigJsonMapMaybeEncryptedBytes, err = Encrypt(k8sDockerConfigJsonMapBytes, encryptionKey); err != nil {
return nil, err
}
} else {
k8sDockerConfigJsonMapMaybeEncryptedBytes = k8sDockerConfigJsonMapBytes
}
k8sSecret := v1.Secret{
@@ -63,11 +66,10 @@ func (d *DockerConfigJsonSecret) ToYaml(encryptionKey *rsa.PublicKey) ([]byte, e
},
ObjectMeta: metav1.ObjectMeta{
Name: d.Name,
Namespace: d.Namespace,
},
Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
".dockerconfigjson": k8sDockerConfigJsonMapEncryptedBytes,
".dockerconfigjson": k8sDockerConfigJsonMapMaybeEncryptedBytes,
},
}
@@ -90,7 +92,6 @@ func (d *DockerConfigJsonSecret) FromYaml(secretBytes []byte, decryptionKey *rsa
return err
} else {
d.Name = k8sSecret.ObjectMeta.Name
d.Namespace = k8sSecret.ObjectMeta.Namespace
for registry, k8sDockerConfigJson := range k8sDockerConfigJsonMap.Auths {
d.Uri = registry
d.Username = k8sDockerConfigJson.Username

View File

@@ -15,7 +15,6 @@ import (
func TestDockerConfigJsonSecret(t *testing.T) {
dockerConfigJsonSecret := DockerConfigJsonSecret{
Name: "some-name",
Namespace: "some-namespace",
Uri: "some-uri",
Username: "some-username",
Password: "some-password",
@@ -38,7 +37,6 @@ func TestDockerConfigJsonSecret(t *testing.T) {
} else if validYamlMap["apiVersion"] != "v1" ||
validYamlMap["kind"] != "Secret" ||
validYamlMap["metadata"].(map[string]interface{})["name"] != dockerConfigJsonSecret.Name ||
validYamlMap["metadata"].(map[string]interface{})["namespace"] != dockerConfigJsonSecret.Namespace ||
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 {

View File

@@ -67,8 +67,8 @@ func writeContentToFile(keyData []byte, fileName string) error {
// Encrypt encrypts data with public key
func Encrypt(pt []byte, pub *rsa.PublicKey) ([]byte, error) {
// hash := sha512.New()
// ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
//hash := sha512.New()
//ct, err := rsa.EncryptOAEP(hash, rand.Reader, pub, pt, nil)
ct, err := rsa.EncryptPKCS1v15(rand.Reader, pub, pt)
if err != nil {
log.Println(err)

View File

@@ -114,7 +114,7 @@ MFxk1pS9LMa/WnzvFr0gWakCAwEAAQ==
}
encDataStr := base64.StdEncoding.EncodeToString(encData)
log.Printf("encrypted data: %s\n", encDataStr)
log.Println("Encoded text:", encDataStr)
dec, _ := base64.StdEncoding.DecodeString(encDataStr)
data, err := Decrypt(dec, privKey)
if err != nil {
@@ -125,5 +125,4 @@ MFxk1pS9LMa/WnzvFr0gWakCAwEAAQ==
t.Error("original string and decrypted string don't match")
t.FailNow()
}
log.Printf("decrypted data: %s\n", data)
}

View File

@@ -6,15 +6,7 @@ import (
)
func K8sSecretToYaml(k8sSecret v1.Secret) ([]byte, error) {
k8sSecretYamlMap := map[string]interface{}{}
if k8sSecretYamlBytes, err := yaml.Marshal(k8sSecret); 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)
}
return K8sToYaml(k8sSecret)
}
func K8sSecretFromYaml(k8sSecretBytes []byte) (v1.Secret, error) {
@@ -24,3 +16,15 @@ func K8sSecretFromYaml(k8sSecretBytes []byte) (v1.Secret, error) {
}
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,20 +1,65 @@
package api
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
)
// 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)
}
// 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 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 {
tempYaml, err := ioutil.TempFile("", "")
if err != nil {
@@ -42,14 +87,32 @@ func kubectlOperation(manifests string, oprName string, namespace string) error
cmd = exec.Command("kubectl", arguments...)
}
sterrBuffer := &bytes.Buffer{}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
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())
}
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()
}
}

View File

@@ -1,25 +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"`
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

View File

@@ -1,13 +1,21 @@
package api
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
)
func checkExists(filename string) os.FileInfo {
@@ -59,24 +67,22 @@ func ProcessConfigArgs(args []string) ([]*ServiceKeyValue, error) {
// 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")
log.Println(err)
return nil, err
}
resultSvcKV := make([]*ServiceKeyValue, len(args))
re1 := regexp.MustCompile(`(\w{1,}).(\w{1,})=("*[\w\-_/:0-9]+"*)`)
re1 := regexp.MustCompile(`([\w\-]{1,}).([\w\-]{1,})=("*[\w\-?=_/:0-9\.]+"*)`)
for i, arg := range args {
LogDebugMessage("Arg received: %s", arg)
result := re1.FindStringSubmatch(arg)
// check if result array's length is == 4 (index 0 - is the full match & indices 1,2,3- are the fields we need)
if len(result) != 4 {
err := fmt.Errorf("Please provide valid args for this command")
log.Println(err)
return nil, err
}
resultSvcKV[i] = &ServiceKeyValue{
SvcName: result[1],
Key: result[2],
Value: result[3],
Value: strings.ReplaceAll(result[3], `"`, ""),
}
}
return resultSvcKV, nil
@@ -111,3 +117,151 @@ func ExecuteTaskWithBlinkingStdoutFeedback(task func() (interface{}, error), fee
}
}
}
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
}

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

@@ -0,0 +1,46 @@
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",
}
expectedKeys := []string{"mongodb", "test", "dash-key", "dot-key", "key123"}
expectedValue := []string{"mongouri://something?ffall", "value_under", "value-dash", "127.0.0.1", "value123"}
exppectedSvc := []string{"qliksense", "test_under", "test-dash", "test-dot", "test123"}
sv, err := ProcessConfigArgs(args)
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
}

View File

@@ -0,0 +1,97 @@
package preflight
import (
"fmt"
)
func (qp *QliksensePreflight) RunAllPreflightChecks(namespace string, kubeConfigContents []byte, mongodbUrl string) {
checkCount := 0
// Preflight minimum kuberenetes version check
fmt.Printf("\nPreflight kubernetes minimum version check\n")
fmt.Println("------------------------------------------")
if err := qp.CheckK8sVersion(namespace, kubeConfigContents); err != nil {
fmt.Printf("Preflight kubernetes minimum version check: FAILED\n")
} else {
checkCount++
}
// Preflight deployment check
fmt.Printf("\nPreflight deployment check\n")
fmt.Println("--------------------------")
if err := qp.CheckDeployment(namespace, kubeConfigContents); err != nil {
fmt.Printf("Preflight deployment check: FAILED\n")
} else {
checkCount++
}
// Preflight service check
fmt.Printf("\nPreflight service check\n")
fmt.Println("-----------------------")
if err := qp.CheckService(namespace, kubeConfigContents); err != nil {
fmt.Printf("Preflight service check: FAILED\n")
} else {
checkCount++
}
// Preflight pod check
fmt.Printf("\nPreflight pod check\n")
fmt.Println("-----------------------")
if err := qp.CheckPod(namespace, kubeConfigContents); err != nil {
fmt.Printf("Preflight pod check: FAILED\n")
} else {
checkCount++
}
// Preflight role check
fmt.Printf("\nPreflight role check\n")
fmt.Println("--------------------------")
if err := qp.CheckCreateRole(namespace); err != nil {
fmt.Printf("Preflight role check: FAILED\n")
} else {
checkCount++
}
// Preflight rolebinding check
fmt.Printf("\nPreflight rolebinding check\n")
fmt.Println("---------------------------------")
if err := qp.CheckCreateRoleBinding(namespace); err != nil {
fmt.Printf("Preflight rolebinding check: FAILED\n")
} else {
checkCount++
}
// Preflight serviceaccount check
fmt.Printf("\nPreflight serviceaccount check\n")
fmt.Println("------------------------------------")
if err := qp.CheckCreateServiceAccount(namespace); err != nil {
fmt.Printf("Preflight serviceaccount check: FAILED\n")
} else {
checkCount++
}
// Preflight mongo check
fmt.Printf("\nPreflight mongo check\n")
fmt.Println("---------------------")
if err := qp.CheckMongo(kubeConfigContents, namespace, mongodbUrl); err != nil {
fmt.Printf("Preflight mongo check: FAILED\n")
} else {
checkCount++
}
// Preflight DNS check
fmt.Printf("\nPreflight DNS check\n")
fmt.Println("-------------------")
if err := qp.CheckDns(namespace, kubeConfigContents); err != nil {
fmt.Printf("Preflight DNS check: FAILED\n")
} else {
checkCount++
}
if checkCount == 9 {
fmt.Printf("\nAll preflight checks have PASSED\n")
} else {
fmt.Printf("\n1 or more preflight checks have FAILED\n")
}
fmt.Println("Completed running all preflight checks")
}

View File

@@ -0,0 +1,118 @@
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)
fmt.Print(err)
return err
}
// Deployment check
fmt.Printf("Preflight deployment check: \n")
err = qp.checkPfDeployment(clientset, namespace, "deployment-preflight-check")
if err != nil {
fmt.Println("Preflight Deployment check: FAILED")
return err
}
fmt.Println("Completed preflight deployment check")
return nil
}
func (qp *QliksensePreflight) CheckService(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)
fmt.Println(err)
return err
}
// Service check
fmt.Printf("\nPreflight service check: \n")
err = checkPfService(clientset, namespace)
if err != nil {
fmt.Println("Preflight Service check: FAILED")
return err
}
fmt.Println("Completed preflight service check")
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)
fmt.Print(err)
return err
}
// Pod check
fmt.Printf("\nPreflight pod check: \n")
err = qp.checkPfPod(clientset, namespace)
if err != nil {
fmt.Println("Preflight Pod check: FAILED")
return err
}
fmt.Println("Completed preflight pod check")
return nil
}
func (qp *QliksensePreflight) checkPfPod(clientset *kubernetes.Clientset, namespace string) error {
// create a pod
podName := "pod-pf-check"
pod, err := createPreflightTestPod(clientset, namespace, podName, qp.GetPreflightConfigObj().GetImageName(nginx))
if err != nil {
err = fmt.Errorf("error: unable to create pod %s - %v\n", podName, err)
return err
}
defer deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, pod); err != nil {
return err
}
fmt.Println("Preflight pod creation check: PASSED")
fmt.Println("Cleaning up resources...")
return nil
}
func checkPfService(clientset *kubernetes.Clientset, namespace string) error {
// creating service
serviceName := "svc-pf-check"
pfService, err := createPreflightTestService(clientset, namespace, serviceName)
if err != nil {
err = fmt.Errorf("error: unable to create service : %s\n", serviceName)
return err
}
defer deleteService(clientset, namespace, serviceName)
_, err = getService(clientset, namespace, pfService.GetName())
if err != nil {
err = fmt.Errorf("error: unable to retrieve service: %s\n", serviceName)
return err
}
fmt.Println("Preflight service creation check: PASSED")
fmt.Println("Cleaning up resources...")
return nil
}
func (qp *QliksensePreflight) checkPfDeployment(clientset *kubernetes.Clientset, namespace, depName string) error {
// check if we are able to create a deployment
pfDeployment, err := createPreflightTestDeployment(clientset, namespace, depName, qp.GetPreflightConfigObj().GetImageName(nginx))
if err != nil {
err = fmt.Errorf("error: unable to create deployment: %v\n", err)
return err
}
defer deleteDeployment(clientset, namespace, depName)
if err := waitForDeployment(clientset, namespace, pfDeployment); err != nil {
return err
}
fmt.Println("Preflight Deployment check: PASSED")
fmt.Println("Cleaning up resources...")
return nil
}

View File

@@ -0,0 +1,81 @@
package preflight
import (
"fmt"
"strings"
"github.com/qlik-oss/sense-installer/pkg/api"
)
const (
nginx = "nginx"
netcat = "netcat"
)
func (qp *QliksensePreflight) CheckDns(namespace string, kubeConfigContents []byte) error {
clientset, clientConfig, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("error: unable to create a kubernetes client: %v\n", err)
fmt.Println(err)
return err
}
// creating deployment
depName := "dep-dns-preflight-check"
dnsDeployment, err := createPreflightTestDeployment(clientset, namespace, depName, qp.GetPreflightConfigObj().GetImageName(nginx))
if err != nil {
err = fmt.Errorf("error: unable to create deployment: %v\n", err)
fmt.Println(err)
return err
}
defer deleteDeployment(clientset, namespace, depName)
if err := waitForDeployment(clientset, namespace, dnsDeployment); err != nil {
return err
}
// creating service
serviceName := "svc-dns-pf-check"
dnsService, err := createPreflightTestService(clientset, namespace, serviceName)
if err != nil {
err = fmt.Errorf("error: unable to create service : %s\n", serviceName)
return err
}
defer deleteService(clientset, namespace, serviceName)
// create a pod
podName := "pf-pod-1"
dnsPod, err := createPreflightTestPod(clientset, namespace, podName, qp.GetPreflightConfigObj().GetImageName(netcat))
if err != nil {
err = fmt.Errorf("error: unable to create pod : %s\n", podName)
return err
}
defer deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, dnsPod); err != nil {
return err
}
if len(dnsPod.Spec.Containers) == 0 {
err := fmt.Errorf("error: there are no containers in the pod")
fmt.Println(err)
return err
}
api.LogDebugMessage("Exec-ing into the container...")
stdout, stderr, err := executeRemoteCommand(clientset, clientConfig, dnsPod.Name, dnsPod.Spec.Containers[0].Name, namespace, []string{"nc", "-z", "-v", "-w 1", dnsService.Name, "80"})
if err != nil {
err = fmt.Errorf("error: unable to execute dns check in the cluster: %v", err)
fmt.Println(err)
return err
}
if strings.HasSuffix(stdout, "succeeded!") || strings.HasSuffix(stderr, "succeeded!") {
fmt.Println("Preflight DNS check: PASSED")
} else {
fmt.Println("Preflight DNS check: FAILED")
}
fmt.Println("Completed preflight DNS check")
fmt.Println("Cleaning up resources...")
return nil
}

View File

@@ -0,0 +1,85 @@
package preflight
import (
"fmt"
"strings"
"github.com/qlik-oss/sense-installer/pkg/api"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
const (
mongoImage = "mongo"
)
func (qp *QliksensePreflight) CheckMongo(kubeConfigContents []byte, namespace, mongodbUrl string) error {
fmt.Printf("Preflight mongodb check: \n")
if mongodbUrl == "" {
// infer mongoDbUrl from currentCR
fmt.Println("MongoDbUri is empty, infer from CR")
qConfig := qapi.NewQConfig(qp.Q.QliksenseHome)
var currentCR *qapi.QliksenseCR
var err error
qConfig.SetNamespace(namespace)
currentCR, err = qConfig.GetCurrentCR()
if err != nil {
fmt.Printf("Unable to retrieve current CR: %v\n", err)
return err
}
decryptedCR, err := qConfig.GetDecryptedCr(currentCR)
mongodbUrl = decryptedCR.Spec.GetFromSecrets("qliksense", "mongoDbUri")
}
fmt.Printf("mongodbUrl: %s\n", mongodbUrl)
if err := mongoConnCheck(kubeConfigContents, namespace, mongodbUrl); err != nil {
return err
}
fmt.Println("Completed preflight mongodb check")
return nil
}
func mongoConnCheck(kubeConfigContents []byte, namespace, mongodbUrl string) error {
clientset, clientConfig, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("error: unable to create a kubernetes client: %v\n", err)
fmt.Println(err)
return err
}
// create a pod
podName := "pf-mongo-pod"
mongoPod, err := createPreflightTestPod(clientset, namespace, podName, mongoImage)
if err != nil {
err = fmt.Errorf("error: unable to create pod : %s\n", podName)
fmt.Println("Preflight mongo check: FAILED")
return err
}
defer deletePod(clientset, namespace, podName)
if err := waitForPod(clientset, namespace, mongoPod); err != nil {
return err
}
if len(mongoPod.Spec.Containers) == 0 {
err := fmt.Errorf("error: there are no containers in the pod")
fmt.Println(err)
return err
}
api.LogDebugMessage("Exec-ing into the container...")
stdout, stderr, err := executeRemoteCommand(clientset, clientConfig, mongoPod.Name, mongoPod.Spec.Containers[0].Name, namespace, []string{"mongo", mongodbUrl})
if err != nil {
err = fmt.Errorf("error: unable to execute mongo check in the cluster: %v", err)
fmt.Println(err)
return err
}
fmt.Println("stdout:", stdout)
fmt.Println("stderr:", stderr)
stringToCheck := "Implicit session"
if strings.Contains(stdout, stringToCheck) || strings.Contains(stderr, stringToCheck) {
fmt.Println("Preflight mongo check: PASSED")
} else {
fmt.Println("Preflight mongo check: FAILED")
}
return nil
}

View File

@@ -0,0 +1,109 @@
package preflight
import (
"os"
"path/filepath"
api "github.com/qlik-oss/sense-installer/pkg/api"
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{}
if err := api.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 api.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) string {
if p.Spec.Images == nil {
return ""
}
return p.Spec.Images[imageFor]
}
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")
return p.Write()
}

View File

@@ -0,0 +1,41 @@
package preflight
import (
"io/ioutil"
"testing"
api "github.com/qlik-oss/sense-installer/pkg/api"
)
func Test_Initalize(t *testing.T) {
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Log(err)
t.FailNow()
}
pf := NewPreflightConfig(tempDir)
if err := pf.Initialize(); err != nil {
t.Log()
t.FailNow()
}
p := &PreflightConfig{
QliksenseHomePath: tempDir,
}
if err := api.ReadFromFile(p, pf.GetConfigFilePath()); err != nil {
t.Log(err)
t.FailNow()
}
if p.GetMinK8sVersion() != "1.15" {
t.Log("expected k8 version: 1.15, but got " + p.GetMinK8sVersion())
t.Fail()
}
p.AddImage("test", "testimage")
if err := p.Write(); err != nil {
t.Log(err)
t.Fail()
}
p2 := NewPreflightConfig(tempDir)
if p2.GetImageName("test") != "testimage" {
t.Log("expected image name: testimage, got: " + p2.GetImageName("test"))
}
}

View File

@@ -0,0 +1,642 @@
package preflight
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/kubectl/pkg/scheme"
"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"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
var gracePeriod int64 = 0
type QliksensePreflight struct {
Q *qliksense.Qliksense
}
func (qp *QliksensePreflight) GetPreflightConfigObj() *PreflightConfig {
return 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")
fmt.Println(err)
return nil, nil, err
}
} else {
config, err := clientcmd.Load(kubeconfig)
if err != nil {
err = errors.Wrap(err, "Unable to load kubeconfig")
fmt.Println(err)
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")
fmt.Println(err)
return nil, nil, err
}
}
clientset, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
err = errors.Wrap(err, "Unable to create clientset")
fmt.Println(err)
return nil, nil, err
}
return clientset, clientConfig, nil
}
func 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, "error: unable to create deployments in the %s namespace", namespace)
fmt.Println(err)
return nil, err
}
fmt.Printf("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, "error: unable to get deployments in the %s namespace", namespace)
api.LogDebugMessage("%v\n", err)
return nil, err
}
return deployment, nil
}
func 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 {
fmt.Println(err)
return err
}
if err := waitForDeploymentToDelete(clientset, namespace, name); err != nil {
return err
}
fmt.Printf("Deleted deployment: %s\n", name)
return nil
}
func 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 {
fmt.Println(err)
return nil, err
}
fmt.Printf("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)
fmt.Println(err)
return nil, err
}
return svc, nil
}
func 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
}
fmt.Printf("Deleted service: %s\n", name)
return nil
}
func 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 {
fmt.Println(err)
return err
}
if err := waitForPodToDelete(clientset, namespace, name); err != nil {
return err
}
fmt.Printf("Deleted pod: %s\n", name)
return nil
}
func createPreflightTestPod(clientset *kubernetes.Clientset, namespace string, podName string, imageName 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{
Containers: []apiv1.Container{
{
Name: "cnt",
Image: imageName,
ImagePullPolicy: apiv1.PullIfNotPresent,
Command: []string{
"sleep",
"3600",
},
},
},
},
}
// 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 {
fmt.Println(err)
return nil, err
}
fmt.Printf("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 execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool) error {
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
if err != nil {
return err
}
return exec.Stream(remotecommand.StreamOptions{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
Tty: tty,
})
}
func executeRemoteCommand(clientset *kubernetes.Clientset, config *rest.Config, podName, containerName, namespace string, command []string) (string, string, error) {
tty := false
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name(podName).
Namespace(namespace).
SubResource("exec").
Param("container", containerName)
req.VersionedParams(&apiv1.PodExecOptions{
Container: containerName,
Command: command,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: tty,
}, scheme.ParameterCodec)
var stdout, stderr bytes.Buffer
err := execute("POST", req.URL(), config, nil, &stdout, &stderr, tty)
return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err
}
func waitForDeployment(clientset *kubernetes.Clientset, namespace string, pfDeployment *appsv1.Deployment) error {
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
var d *appsv1.Deployment
var err error
WAIT:
for {
d, err = getDeployment(clientset, namespace, pfDeployment.GetName())
if err != nil {
err = fmt.Errorf("error: unable to retrieve deployment: %s\n", pfDeployment.GetName())
fmt.Println(err)
return err
}
select {
case <-timeout.C:
break WAIT
default:
if int(d.Status.ReadyReplicas) > 0 {
break WAIT
}
}
time.Sleep(5 * time.Second)
}
if int(d.Status.ReadyReplicas) == 0 {
err = fmt.Errorf("error: deployment took longer than expected to spin up pods")
fmt.Println(err)
return err
}
return nil
}
func waitForPod(clientset *kubernetes.Clientset, namespace string, pod *apiv1.Pod) error {
var err error
if len(pod.Spec.Containers) > 0 {
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
podName := pod.Name
OUT:
for {
pod, err = getPod(clientset, namespace, podName)
if err != nil {
err = fmt.Errorf("error: unable to retrieve %s pod by name", podName)
fmt.Println(err)
return err
}
select {
case <-timeout.C:
break OUT
default:
if len(pod.Status.ContainerStatuses) > 0 && pod.Status.ContainerStatuses[0].Ready {
break OUT
}
}
time.Sleep(5 * time.Second)
}
if len(pod.Status.ContainerStatuses) == 0 || !pod.Status.ContainerStatuses[0].Ready {
err = fmt.Errorf("error: container is taking much longer than expected")
fmt.Println(err)
return err
}
return nil
}
err = fmt.Errorf("error: there are no containers in the pod")
fmt.Println(err)
return err
}
func waitForPodToDelete(clientset *kubernetes.Clientset, namespace, podName string) error {
var err error
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
OUT:
for {
_, err = getPod(clientset, namespace, podName)
if err != nil {
return nil
}
select {
case <-timeout.C:
break OUT
default:
}
time.Sleep(5 * time.Second)
}
err = fmt.Errorf("error: delete pod is taking unusually long")
fmt.Println(err)
return err
}
func waitForDeploymentToDelete(clientset *kubernetes.Clientset, namespace, deploymentName string) error {
var err error
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
OUT:
for {
_, err = getDeployment(clientset, namespace, deploymentName)
if err != nil {
return nil
}
select {
case <-timeout.C:
break OUT
default:
}
time.Sleep(5 * time.Second)
}
err = fmt.Errorf("error: delete deployment is taking unusually long")
fmt.Println(err)
return err
}
func 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 {
fmt.Println(err)
return nil, err
}
fmt.Printf("Created role: %s\n", role.Name)
return role, nil
}
func 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)
}
fmt.Printf("Deleted role: %s\n\n", role.Name)
}
func 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": "demo",
},
},
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 {
fmt.Println(err)
return nil, err
}
fmt.Printf("Created RoleBinding: %s\n", roleBindingSpec.Name)
return roleBinding, nil
}
func 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)
}
fmt.Printf("Deleted RoleBinding: %s\n\n", roleBinding.Name)
}
func 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": "demo",
},
},
}
// 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 {
fmt.Println(err)
return nil, err
}
fmt.Printf("Created Service Account: %s\n", serviceAccountSpec.Name)
return serviceAccount, nil
}
func 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)
}
fmt.Printf("Deleted ServiceAccount: %s\n\n", serviceAccount.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)
}
})
}
}

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

@@ -0,0 +1,131 @@
package preflight
import (
"fmt"
"path/filepath"
"strings"
"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"
)
func (qp *QliksensePreflight) CheckCreateRole(namespace string) error {
// create a Role
fmt.Printf("Preflight role check: \n")
err := qp.checkCreateEntity(namespace, "Role")
if err != nil {
return err
}
fmt.Println("Completed preflight role check")
return nil
}
func (qp *QliksensePreflight) CheckCreateRoleBinding(namespace string) error {
// create a RoleBinding
fmt.Printf("Preflight rolebinding check: \n")
err := qp.checkCreateEntity(namespace, "RoleBinding")
if err != nil {
return err
}
fmt.Println("Completed preflight rolebinding check")
return nil
}
func (qp *QliksensePreflight) CheckCreateServiceAccount(namespace string) error {
// create a service account
fmt.Printf("Preflight serviceaccount check: \n")
err := qp.checkCreateEntity(namespace, "ServiceAccount")
if err != nil {
return err
}
fmt.Println("Completed preflight serviceaccount check")
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 {
fmt.Printf("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 {
fmt.Printf("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)
}
resultYamlString, err := qliksense.ExecuteKustomizeBuild(kusDir)
if err != nil {
fmt.Printf("Unable to retrieve manifests from executing kustomize: %v\n", err)
return err
}
sa := qliksense.GetYamlsFromMultiDoc(string(resultYamlString), entityToTest)
if sa != "" {
sa = strings.ReplaceAll(sa, "namespace: default\n", fmt.Sprintf("namespace: %s\n", namespace))
} else {
err := fmt.Errorf("Unable to retrieve yamls to apply on cluster")
fmt.Println(err)
return err
}
err = api.KubectlApply(sa, namespace)
if err != nil {
err := fmt.Errorf("Failed to create entity on the cluster")
fmt.Println(err)
return err
}
fmt.Printf("Preflight %s check: PASSED\n", entityToTest)
fmt.Println("Cleaning up resources")
err = api.KubectlDelete(sa, namespace)
if err != nil {
fmt.Println("Preflight cleanup failed!")
return err
}
return nil
}
func (qp *QliksensePreflight) CheckCreateRB(namespace string, kubeConfigContents []byte) error {
// create a role
fmt.Printf("Preflight createRole check: \n")
err := qp.checkCreateEntity(namespace, "Role")
if err != nil {
fmt.Println("Preflight role check: FAILED")
}
fmt.Printf("Completed preflight role check\n\n")
// create a roleBinding
fmt.Printf("Preflight rolebinding check: \n")
err = qp.checkCreateEntity(namespace, "RoleBinding")
if err != nil {
fmt.Println("Preflight rolebinding check: FAILED")
}
fmt.Printf("Completed preflight rolebinding check\n\n")
// create a service account
fmt.Printf("Preflight serviceaccount check: \n")
err = qp.checkCreateEntity(namespace, "ServiceAccount")
if err != nil {
fmt.Println("Preflight serviceaccount check: FAILED")
}
fmt.Printf("Completed preflight serviceaccount check\n\n")
fmt.Println("Preflight RB check: PASSED")
fmt.Println("Completed preflight CreateRB check")
return nil
}

View File

@@ -0,0 +1,56 @@
package preflight
import (
"fmt"
"github.com/Masterminds/semver/v3"
"k8s.io/apimachinery/pkg/version"
)
func (qp *QliksensePreflight) CheckK8sVersion(namespace string, kubeConfigContents []byte) error {
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)
//fmt.Println(err)
return err
}
fmt.Printf("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)
//fmt.Println(err)
return err
}
//fmt.Printf("Current K8s 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)
fmt.Println(err)
return err
}
if currentVersion.GreaterThan(minK8sVersionSemver) {
//fmt.Printf("\n\nCurrent %s Component version: %s is less than minimum required version:%s\n", component, currentComponentVersion, componentVersionFromDependenciesYaml)
fmt.Printf("Current %s is greater than minimum required version:%s\n", currentVersion, minK8sVersionSemver)
fmt.Println("Preflight minimum kubernetes version check: PASSED")
} else {
fmt.Printf("Current %s is less than minimum required version:%s\n", currentVersion, minK8sVersionSemver)
fmt.Println("Preflight minimum kubernetes version check: FAILED")
}
fmt.Printf("Completed Preflight kubernetes minimum version check\n")
return nil
}

BIN
pkg/qliksense/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -58,42 +58,33 @@ 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 (q *Qliksense) About(gitRef, profile string) (*VersionOutput, error) {
configDirectory, isTemporary, profile, err := q.getConfigDirectory(defaultGitUrl, gitRef, profile)
configDirectory, isTemporary, profile, err := q.getConfigDirectory(defaultConfigRepoGitUrl, gitRef, profile)
if err != nil {
return nil, err
}
if isTemporary {
} else if isTemporary {
defer os.RemoveAll(configDirectory)
}
return q.AboutDir(configDirectory, profile)
}
func (q *Qliksense) AboutDir(configDirectory, profile string) (*VersionOutput, error) {
chartVersion, err := getChartVersion(filepath.Join(configDirectory, "transformers", "qseokversion.yaml"), "qliksense")
if err != nil {
if chartVersion, err := getChartVersion(filepath.Join(configDirectory, "transformers", "qseokversion.yaml"), "qliksense"); err != nil {
return nil, err
}
kuzManifest, err := executeKustomizeBuildWithStdoutProgress(filepath.Join(configDirectory, "manifests", profile))
if err != nil {
} else if kuzManifest, err := executeKustomizeBuildWithStdoutProgress(filepath.Join(configDirectory, "manifests", profile)); err != nil {
return nil, err
}
images, err := getImageList(kuzManifest)
if err != nil {
} else if images, err := getImageList(kuzManifest); err != nil {
return nil, err
} else {
return &VersionOutput{
QliksenseVersion: chartVersion,
Images: images,
}, nil
}
return &VersionOutput{
QliksenseVersion: chartVersion,
Images: images,
}, nil
}
func (q *Qliksense) getConfigDirectory(gitUrl, gitRef, profileEntered string) (dir string, isTemporary bool, profile string, err error) {
@@ -103,7 +94,7 @@ func (q *Qliksense) getConfigDirectory(gitUrl, gitRef, profileEntered string) (d
}
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
@@ -129,14 +120,15 @@ func (q *Qliksense) getConfigDirectory(gitUrl, gitRef, profileEntered string) (d
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 {

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

@@ -0,0 +1,43 @@
package qliksense
import (
"io"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) ApplyCRFromReader(r io.Reader, opts *InstallCommandOptions, keepPatchFiles, overwriteExistingContext 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
}
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 err := q.FetchQK8s(cr.GetLabelFromCr("version")); err != nil {
return err
}
}
return q.UpgradeQK8s(keepPatchFiles)
}
return q.InstallQK8s(cr.GetLabelFromCr("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,15 +1,16 @@
package qliksense
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v2"
"github.com/qlik-oss/k-apis/pkg/cr"
"github.com/qlik-oss/sense-installer/pkg/api"
@@ -17,7 +18,11 @@ import (
)
const (
Q_INIT_CRD_PATH = "manifests/base/manifests/qliksense-init"
Q_INIT_CRD_PATH = "manifests/base/manifests/qliksense-init"
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 {
@@ -29,22 +34,47 @@ func (q *Qliksense) ConfigApplyQK8s() error {
fmt.Println("cannot get the current-context cr", err)
return err
}
// 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
return q.applyCR(qcr.Spec.NameSpace)
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
return q.applyCR(dcr)
}
}
return q.applyConfigToK8s(qcr)
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else {
return q.applyConfigToK8s(dcr)
}
}
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
}
func (q *Qliksense) applyConfigToK8s(qcr *qapi.QliksenseCR) error {
if qcr.Spec.RotateKeys != "None" {
if err := os.Unsetenv("EJSON_KEY"); err != nil {
fmt.Printf("error unsetting EJSON_KEY environment variable: %v\n", err)
return err
}
if err := os.Setenv("EJSON_KEYDIR", q.QliksenseEjsonKeyDir); err != nil {
fmt.Printf("error setting EJSON_KEYDIR environment variable: %v\n", err)
if err := q.configEjson(); err != nil {
return err
}
}
@@ -54,16 +84,17 @@ func (q *Qliksense) applyConfigToK8s(qcr *qapi.QliksenseCR) error {
return err
}
fmt.Println("Manifests root: " + qcr.Spec.GetManifestsRoot())
qcr.SetNamespace(qapi.GetKubectlNamespace())
// generate patches
cr.GeneratePatches(qcr.Spec, path.Join(userHomeDir, ".kube", "config"))
cr.GeneratePatches(&qcr.KApiCr, path.Join(userHomeDir, ".kube", "config"))
// apply generated manifests
profilePath := filepath.Join(qcr.Spec.GetManifestsRoot(), qcr.Spec.GetProfileDir())
mByte, err := executeKustomizeBuild(profilePath)
mByte, err := ExecuteKustomizeBuild(profilePath)
if err != nil {
fmt.Println("cannot generate manifests for "+profilePath, err)
return err
}
if err = qapi.KubectlApply(string(mByte), qcr.Spec.NameSpace); err != nil {
if err = qapi.KubectlApply(string(mByte), qcr.GetNamespace()); err != nil {
return err
}
@@ -76,7 +107,11 @@ func (q *Qliksense) ConfigViewCR() error {
if err != nil {
return err
}
fmt.Println(r)
oth, err := q.getCurrentCrDependentResourceAsString()
if err != nil {
return err
}
fmt.Println(r + "\n" + oth)
return nil
}
@@ -92,18 +127,28 @@ func (q *Qliksense) getCRString(contextName string) (string, error) {
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
crString.Write(out)
for svcName, v := range qcr.Spec.Secrets {
for _, item := range v {
if item.ValueFrom != nil && item.ValueFrom.SecretKeyRef != nil {
secretFilePath := filepath.Join(q.QliksenseHome, QliksenseContextsDir, qcr.Metadata.Name, QliksenseSecretsDir, svcName+".yaml")
secretFilePath := filepath.Join(q.QliksenseHome, QliksenseContextsDir, qcr.GetName(), QliksenseSecretsDir, svcName+".yaml")
if api.FileExists(secretFilePath) {
secretFile, err := ioutil.ReadFile(secretFilePath)
@@ -116,5 +161,57 @@ func (q *Qliksense) getCRString(contextName string) (string, error) {
}
}
}
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

@@ -3,32 +3,35 @@ package qliksense
import (
"crypto/rsa"
"fmt"
"github.com/qlik-oss/k-apis/pkg/config"
"github.com/robfig/cron/v3"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"text/tabwriter"
"encoding/base64"
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
QliksenseConfigHome = "/.qliksense"
QliksenseConfigContextHome = "/.qliksense/contexts"
QliksenseConfigFile = "config.yaml"
QliksenseContextsDir = "contexts"
DefaultQliksenseContext = "qlik-default"
MaxContextNameLength = 17
QliksenseSecretsDir = "secrets"
QliksenseConfigFile = "config.yaml"
QliksenseContextsDir = "contexts"
DefaultQliksenseContext = "qlik-default"
MaxContextNameLength = 17
QliksenseSecretsDir = "secrets"
imageRegistryConfigKey = "imageRegistry"
pullSecretName = "artifactory-docker-secret"
@@ -37,34 +40,32 @@ const (
// SetSecrets - set-secrets <key>=<value> commands
func (q *Qliksense) SetSecrets(args []string, isSecretSet bool) error {
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
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.Metadata.Name)
api.LogDebugMessage("Current context: %s", qliksenseCR.GetName())
rsaPublicKey, _, err := qConfig.GetCurrentContextEncryptionKeyPair()
if err != nil {
return err
}
resultArgs, err := api.ProcessConfigArgs(args)
if err != nil {
return err
}
for _, ra := range resultArgs {
api.LogDebugMessage("value args to be encrypted: %s", ra.Value)
if err := q.processSecret(ra, rsaPublicKey, qliksenseCR, isSecretSet); err != nil {
return err
}
}
// write modified content into context-yaml
api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
return nil
return qConfig.WriteCR(qliksenseCR)
}
func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, rsaPublicKey *rsa.PublicKey, qliksenseCR api.QliksenseCR, isSecretSet bool) error {
func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, rsaPublicKey *rsa.PublicKey, qliksenseCR *api.QliksenseCR, isSecretSet bool) error {
// encrypt value with RSA key pair
valueBytes := []byte(ra.Value)
cipherText, e2 := api.Encrypt(valueBytes, rsaPublicKey)
@@ -74,10 +75,10 @@ func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, rsaPublicKey *rsa.Pub
base64EncodedSecret := b64.StdEncoding.EncodeToString(cipherText)
secretName := ""
if isSecretSet {
secretFolder := filepath.Join(q.QliksenseHome, QliksenseContextsDir, qliksenseCR.Metadata.Name, QliksenseSecretsDir)
secretFolder := qliksenseCR.GetK8sSecretsFolder(q.QliksenseHome)
secretFileName := filepath.Join(secretFolder, ra.SvcName+".yaml")
secretName = fmt.Sprintf("%s-%s-%s", qliksenseCR.Metadata.Name, ra.SvcName, "sense_installer")
secretName = fmt.Sprintf("%s-%s-%s", qliksenseCR.GetName(), ra.SvcName, "senseinstaller")
api.LogDebugMessage("Constructed secret name: %s", secretName)
k8sSecret := v1.Secret{
@@ -115,7 +116,6 @@ func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, rsaPublicKey *rsa.Pub
api.LogDebugMessage("Error while writing K8s secret to file")
return err
}
// api.WriteToFile(&k8sSecret, secretFileName)
api.LogDebugMessage("Created a Kubernetes secret")
// Prepare args to update CR in the next step
@@ -130,7 +130,8 @@ func (q *Qliksense) processSecret(ra *api.ServiceKeyValue, rsaPublicKey *rsa.Pub
// SetConfigs - set-configs <key>=<value> commands
func (q *Qliksense) SetConfigs(args []string) error {
// retieve current context from config.yaml
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
@@ -143,54 +144,66 @@ func (q *Qliksense) SetConfigs(args []string) error {
qliksenseCR.Spec.AddToConfigs(ra.SvcName, ra.Key, ra.Value)
}
// write modified content into context.yaml
api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
return nil
return qConfig.WriteCR(qliksenseCR)
}
func retrieveCurrentContextInfo(q *Qliksense) (api.QliksenseCR, string, error) {
var qliksenseConfig api.QliksenseConfig
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
if err := api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return api.QliksenseCR{}, "", err
}
currentContext := qliksenseConfig.Spec.CurrentContext
api.LogDebugMessage("Current-context from config.yaml: %s", currentContext)
if currentContext == "" {
// current-context is empty
err := fmt.Errorf(`Please run the "qliksense config set-context <context-name>" first before viewing the current context info`)
log.Println(err)
return api.QliksenseCR{}, "", err
}
// read the context.yaml file
var qliksenseCR api.QliksenseCR
if currentContext == "" {
// current-context is empty
err := fmt.Errorf(`Please run the "qliksense config set-context <context-name>" first before viewing the current context info`)
log.Println(err)
return api.QliksenseCR{}, "", err
}
qliksenseContextsFile := filepath.Join(q.QliksenseHome, QliksenseContextsDir, currentContext, currentContext+".yaml")
if !api.FileExists(qliksenseContextsFile) {
err := fmt.Errorf("Context file does not exist.\nPlease try re-running `qliksense config set-context <context-name>` and then `qliksense config view` again")
log.Println(err)
return api.QliksenseCR{}, "", err
}
if err := api.ReadFromFile(&qliksenseCR, qliksenseContextsFile); err != nil {
log.Println(err)
return api.QliksenseCR{}, "", err
}
api.LogDebugMessage("Read context file: %s, Read QliksenseCR: %v", qliksenseContextsFile, qliksenseCR)
return qliksenseCR, qliksenseContextsFile, nil
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 })
}
// SetOtherConfigs - set profile/namespace/storageclassname/git.repository commands
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
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
@@ -204,42 +217,49 @@ func (q *Qliksense) SetOtherConfigs(args []string) error {
for _, arg := range args {
argsString := strings.Split(arg, "=")
switch argsString[0] {
case "profile":
qliksenseCR.Spec.Profile = argsString[1]
api.LogDebugMessage("Current profile after modification: %s ", qliksenseCR.Spec.Profile)
case "namespace":
qliksenseCR.Spec.NameSpace = argsString[1]
api.LogDebugMessage("Current namespace after modification: %s ", qliksenseCR.Spec.NameSpace)
case "git.repository":
qliksenseCR.Spec.Git.Repository = argsString[1]
api.LogDebugMessage("Current git repository after modification: %s ", qliksenseCR.Spec.Git.Repository)
case "storageClassName":
qliksenseCR.Spec.StorageClassName = argsString[1]
api.LogDebugMessage("Current StorageClassName after modification: %s ", qliksenseCR.Spec.StorageClassName)
case "manifestsRoot":
qliksenseCR.Spec.ManifestsRoot = argsString[1]
case "rotateKeys":
rotateKeys, err := validateInput(argsString[1])
if err != nil {
return err
}
qliksenseCR.Spec.RotateKeys = rotateKeys
api.LogDebugMessage("Current rotateKeys after modification: %s ", qliksenseCR.Spec.RotateKeys)
default:
log.Println("As part of the `qliksense config set` command, please enter one of: profile, namespace, storageClassName,rotateKeys or git.repository arguments")
key := strings.ToLower(argsString[0])
value := argsString[1]
// check if key is for git or gitops (sub objects)
keySplit := strings.Split(key, ".")
key = keySplit[0]
keySub := ""
if len(keySplit) == 2 {
keySub = strings.ToLower(keySplit[1])
}
valid := true
valid, qliksenseCR = validateCR(key, keySub, value, qliksenseCR)
field := caseInsenstiveFieldByName(reflect.Indirect(reflect.ValueOf(qliksenseCR.Spec)), key)
if !valid {
err := fmt.Errorf("Please enter one of: profile, storageClassName,rotateKeys, manifestRoot, git.repository or gitops arguments to configure the current context")
return err
} else if strings.EqualFold("", keySub) {
// set spec for everything excluding git and gitops
if field.CanSet() {
field.SetString(value)
}
} else {
// set spec for git or gitops
subField := caseInsenstiveFieldByName(reflect.Indirect(field), keySub)
if subField.CanSet() {
subField.SetString(value)
}
}
fmt.Println(chalk.Green.Color("Successfully added to Custom Resource Spec"))
}
// write modified content into context.yaml
api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
return nil
return qConfig.WriteCR(qliksenseCR)
}
// SetContextConfig - set the context for qliksense kubernetes resources to live in
func (q *Qliksense) SetContextConfig(args []string) error {
if len(args) == 1 {
q.SetUpQliksenseContext(args[0], false)
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)
@@ -261,7 +281,7 @@ func (q *Qliksense) ListContextConfigs() error {
w.Flush()
if len(qliksenseConfig.Spec.Contexts) > 0 {
for _, cont := range qliksenseConfig.Spec.Contexts {
fmt.Fprintln(w, cont.Name, "\t", cont.CrFile, "\t")
fmt.Fprintln(w, cont.Name, "\t", qliksenseConfig.GetCRFilePath(cont.Name), "\t")
}
w.Flush()
fmt.Fprintln(out, "")
@@ -272,13 +292,84 @@ func (q *Qliksense) ListContextConfigs() error {
return nil
}
func (q *Qliksense) DeleteContextConfig(args []string) 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 {
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 {
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 {
return q.SetUpQliksenseContext(DefaultQliksenseContext, true)
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, isDefaultContext bool) error {
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")
@@ -287,78 +378,30 @@ func (q *Qliksense) SetUpQliksenseContext(contextName string, isDefaultContext b
}
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
var qliksenseConfig api.QliksenseConfig
configFileTrack := false
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 isDefaultContext { // if config file exits but a default context is requested, we want to prevent writing to config file
configFileTrack = true
}
}
// creating a file in the name of the context if it does not exist/ opening it to append/modify content if it already exists
qliksenseContextsDir1 := filepath.Join(q.QliksenseHome, QliksenseContextsDir)
if !api.DirExists(qliksenseContextsDir1) {
if err := os.Mkdir(qliksenseContextsDir1, os.ModePerm); err != nil {
err = fmt.Errorf("Not able to create %s dir: %v", qliksenseContextsDir1, err)
log.Println(err)
return err
}
}
api.LogDebugMessage("%s exists", qliksenseContextsDir1)
// creating contexts/qlik-default/qlik-default.yaml file
qliksenseContextFile := filepath.Join(qliksenseContextsDir1, contextName, contextName+".yaml")
var qliksenseCR api.QliksenseCR
defaultContextsDir := filepath.Join(qliksenseContextsDir1, contextName)
if !api.DirExists(defaultContextsDir) {
if err := os.Mkdir(defaultContextsDir, os.ModePerm); err != nil {
err = fmt.Errorf("Not able to create %s: %v", defaultContextsDir, err)
log.Println(err)
return err
}
}
api.LogDebugMessage("%s exists", defaultContextsDir)
if !api.FileExists(qliksenseContextFile) {
qliksenseCR.AddCommonConfig(contextName)
api.LogDebugMessage("Added Context: %s", contextName)
} else {
if err := api.ReadFromFile(&qliksenseCR, qliksenseContextFile); err != nil {
if err := api.ReadFromFile(qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return err
}
}
api.WriteToFile(&qliksenseCR, qliksenseContextFile)
ctxTrack := false
if len(qliksenseConfig.Spec.Contexts) > 0 {
for _, ctx := range qliksenseConfig.Spec.Contexts {
if ctx.Name == contextName {
ctx.CrFile = qliksenseContextFile
ctxTrack = true
break
}
}
}
if !ctxTrack {
qliksenseConfig.Spec.Contexts = append(qliksenseConfig.Spec.Contexts, api.Context{
Name: contextName,
CrFile: qliksenseContextFile,
})
if qliksenseConfig.IsContextExist(contextName) {
qliksenseConfig.Spec.CurrentContext = contextName
return qliksenseConfig.Write()
}
qliksenseCR := &api.QliksenseCR{}
qliksenseCR.AddCommonConfig(contextName)
qliksenseConfig.Spec.CurrentContext = contextName
if !configFileTrack {
api.WriteToFile(&qliksenseConfig, qliksenseConfigFile)
if err := qliksenseConfig.CreateOrWriteCrAndContext(qliksenseCR); err != nil {
return err
}
return nil
// set the encrypted default mongo
return q.SetSecrets([]string{`qliksense.mongoDbUri="mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"`}, false)
}
func validateInput(input string) (string, error) {
@@ -380,19 +423,19 @@ func validateInput(input string) (string, error) {
}
// PrepareK8sSecret decodes and decrypts the secret value in the secret.yaml file and returns a B64encoded string
func (q *Qliksense) PrepareK8sSecret(qliksenseCR api.QliksenseCR, targetFile string) (string, error) {
qConfig := api.NewQConfig(q.QliksenseHome)
_, rsaPrivateKey, err := qConfig.GetCurrentContextEncryptionKeyPair()
if err != nil {
return "", err
}
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)
_, rsaPrivateKey, err := qConfig.GetCurrentContextEncryptionKeyPair()
if err != nil {
return "", err
}
// read the target file
k8sSecret, err := readTargetfile(targetFile)
if err != nil {
@@ -403,34 +446,28 @@ func (q *Qliksense) PrepareK8sSecret(qliksenseCR api.QliksenseCR, targetFile str
if err != nil {
return "", err
}
dataMap := k8sSecret1.Data
var base64EncodedSecret string
var resultMap = make(map[string][]byte)
for k, v := range dataMap {
// base64 decode every value
decodedStr, _ := base64.StdEncoding.DecodeString(string(v))
decryptedString, err := api.Decrypt(decodedStr, rsaPrivateKey)
ba, err := b64.StdEncoding.DecodeString(string(v))
if err != nil {
err := fmt.Errorf("Not able to decrypt message")
err := fmt.Errorf("Not able to decode message: %v", err)
return "", err
}
// base64 encode the values
base64EncodedSecret = b64.StdEncoding.EncodeToString(decryptedString)
resultMap[k] = []byte(base64EncodedSecret)
decryptedString, err := api.Decrypt(ba, rsaPrivateKey)
if err != nil {
err := fmt.Errorf("Not able to decrypt message: %v", err)
return "", err
}
resultMap[k] = decryptedString
}
api.LogDebugMessage("B64 encoded Map: %v\n", resultMap)
// putting the above map back into the k8sSecret struct
k8sSecret1.Data = resultMap
k8sSecretBytes, err := api.K8sSecretToYaml(k8sSecret1)
api.LogDebugMessage("Final Yaml: %v\n", string(k8sSecretBytes))
if err != nil {
return "", err
}
return string(k8sSecretBytes), nil
}
@@ -446,7 +483,7 @@ func readTargetfile(targetFile string) ([]byte, error) {
func (q *Qliksense) SetImageRegistry(registry, pushUsername, pushPassword, pullUsername, pullPassword string) error {
qConfig := api.NewQConfig(q.QliksenseHome)
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
qliksenseCR, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
@@ -458,16 +495,33 @@ func (q *Qliksense) SetImageRegistry(registry, pushUsername, pushPassword, pullU
}); err != nil {
return err
} else if err := qConfig.SetPullDockerConfigJsonSecret(&api.DockerConfigJsonSecret{
Name: pullSecretName,
Namespace: qliksenseCR.Spec.NameSpace,
Uri: registry,
Username: pullUsername,
Password: pullPassword,
Email: pullUsername,
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 api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
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

@@ -1,23 +1,158 @@
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"
)
var (
testDir = "./tests"
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 encText = "SFpVZ2t5SGsrN2lLQjlTYm9rbFUxSDFRcmVYdUxhTW9MUHlQOGtGditxMEcwZTlIZDl1dVRrV0tEYm5qUURSWFp3dStuNklueGk3anI2c1djSVdsbWlKTHdWQUJwdUg0a1NXd3llMUlMa2oxK3FRSFlMM2dQUExvN1pBYkVDeDROMUVvam12M0t0NmQwbkdhSXlWWEpmWWJUVVFDM1Y4L0ZTVXBVN0JUb0l4OVZWdmlPam5HTHk4RlF2a3RUaHJxWTUvZEh2N3pVUmhiOTc2Q2YwbEovZ3I2L2NwRk9RMUFXVXdodVhrTG9lYjVzNFdtTEZzNldqT3k0bWlKM1J6VllLaWVUSFJ2SE85eDB6dUthanRwSGEzWEZkaE5QNnpySVJJNTRFalUyblVYYUNlYXVnWnZEOUxjdWluOFhFcjExbkFINURCUDAycXhoZk5BejVoMlV2eFNWVmR0aW1QTDBhMVBJTUxGQTgyWUkrQkFOQkhkSUNnZGU5SkxIRFBoTzR6c0llaE1LRmhVQkNoOUhQa3kyRnhTeDJ3YWp3M1UycEsvcFJVZUxDazRUbkhmL25LN3h5ekdpV3dSUFFFZHdsWE5JbUhjVlVPV3gvNWh4WlJCUTZtb3pGYk1HbXR1Mkh5Z3RVV2gzNFYzd1BhS01TNFRsa0hyODFjRjVCWVpxenBFK1pKWnVyLy8zbzJsU0tFMjMxTG1pcGk1K0FqbXZvUVcyWHBocjFNVWJQY1pXUkJFRkkyQXBCM0FhQXFPa0k1MkRqNG43Mko5bCtaMzdydTk1aHk5K1lzY0FxMjZVbExYRlc0S3RUUkRLSjlMNnVmdlIrUUNudER3em5UTFRHUnEwZU5COWt6S0Q4MFlUdXozeHNXK3cxdjlHbDJaMnBZMTZWTCtEV1k9"
var decText = "mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"
func setupTargetFileAndPrivateKey() ([]byte, []byte, string, error) {
targetFileString := fmt.Sprintf(targetFileStringTemplate, encText)
privKeyBytes := []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEApFf3qCQhAr2QLRRZdhLyB8exLjrQiXLr8hwDe0xHSLJX3w7v
5z4ujJWrHUulQ2/hvS8uffxMVrp2YqeA4sjy/ku1KqZVQv/WNTdL2v9Z1ewbRnBj
DQvmkWDKZ+cP8VPdHGzQ4iM2z6BZ4RQTkdQMKqsVwUsLO9amI2TOny/M696eFRW0
pk4+W3QZZRawT0HqJPvKKXKqoO2+62W54rOV8glJi29Do06e4S6CZUl1hBUy0VlL
trLlRSOHTois0dF2a9f7+GGgU11MHO6w1k1NesSlfZ0vnrrkW8WFLqewk+Jj+w1x
eQnYHOeHjx6zi9f9DC96eSylSB3iJ71NmXcMc0IEZ2LiQIqTL83BLOgMBCsK3FSl
GMakQUR2GJ+M0I/selYkRMhid6eOmhlsTNMPbpcTHxZ+ReIzS+5B1X0FZ7RIL+jS
L9mFcxmD3dxurrrt/DkLpXcuWdi1s1bpVn3jIQhU0+bgA6hT0k8Kj2f3Q9QnvkHS
Eff+XyLvLwQeSsSAcnM+1I7fNSPEo2cq0au6ZtjHcirXmMminAQ6cKW1XrEvJBef
HHibtjJjIM4bHH7MKRA5H3km/J4CCwI1VogSTcE05Z6ypAFU2TCrnec9c9VXkRDP
94h0GuoG8sdhmQudqvghr/8T7CV+sGRQbdeqrXwanfnGPrjcMVIO/dSOxOUCAwEA
AQKCAgA5b2TmJnpC8u0IVCxPz582iNurRHLNFpTPMGsnFCl1hp6fHiFJt7mc+FGt
E1rWjqtd6rdc4Gfth40IPXIV0BTcOqk+FpOFrtO2FXU1PDixQqrlmzGCxb324NTc
KyyvMpf77yuxXI0zUt8WgmW0eV8nKlOYEhoC96lohTqQ96uuY0bsJ4HS/VVdsN2P
Lra/fFHQSw8EHUb0pyIqMoscZ5bn18cUK/Z/hGKSYCbCL0Iavy3bbFHBsBPgbeJD
2BBN4953Iiy1Sak2eUy4b9LtkmaZmVAc7mpOFxLn38gD3icgB+bZPoGBw6b7sw71
Pc2R+hI9x/oNj0TUR11adhZApBJ9RhBbnSCUt8OUt9U5prNj+9qs8cHJGywtz1da
ZT1M6mn2MFSsaOyOlJPzGUzSf4AhI7HiouDpLHtHDqLmc8Vv4rZUqrcFw6kZTCY5
564yE8hh/UimOgQr7467/hADHZ0kBsupFEDWRqQ3qTIikHmGhTYZehDrSGL/3BMG
rvsFdv0krUHyW4FfHqPN09jfP3LTqd5vVbzRhxcGsoGmP/1kXIDtO8Cp1s/K6Mse
tInRCRla8ttZ3CZZ+Vf8HLi/n8XSRfbnMGYi7lVVxnp6kNsTEBgosbdU/r1zbMRJ
8mMMHygyugaRLHmOD/8fkWLfyR88cxPH8u9NufTfvJgiFpZboQKCAQEA0uSU6IGZ
pXIVZdmDWt4mxpS5T2UYarw3V9z/Isd3kkUU5YrC2XrvPRwmx5Jh9GXl9WENYJAR
wH7PaJT0HpBwpxJa1RqHHDSka7DnDcy44oRXyM7e2AmcW8QvcDty/0HPo4oZq4IT
m/+ot1R+bIpmJOweGRhVauzxJEUlQyt+kiH/ad8GiOS6LwqFPq42alnUxPQ106wF
EZZ2WQdzkyV6tF9aMG18AT1fJsGwNjCLRxJ52t/aEUP5mYwlL2UTT5Acn8KbtrTO
fFLAxGuB9LDdT1tGgIpzsXmxAaaeuPvSK4TDFdQyLAUdQJdz0GD9j9ciMPQH3UPe
Vjt6qtpfY6QK2wKCAQEAx36Vys5BlQI0TG6qORI0fiOYpLG1GqmdbCNRgBUsMj5T
LFe7uSd4qnDvGmns4MdkSSOlpF17bQiWhWKbjKRQpT0U/46zcIT4pWyajXJe+i9H
M/DpSRkMq2kGkx6KX2u9L66QBzcxJjtS17amdSpDAfsrvJgOWkxxInzw9n1u6aTe
ZjRDXdVX0KjPebEPOaoJToxne21Od3t+47TnDsQPsO1dvvrXX76IfH8cAlD5+0C/
b2YvDqWDmh9ICjKShwuDWgi4KjCV5PMHCIxH0FQ2L6mSbwIb9YgGin3wjN3KbWqz
dgEu7MeDxEwxZSSg4OstYVLQVgM39G/2ZA4YVJEbPwKCAQAo9FjymhBzb6c2Izp+
D/wpvkIKaBCI0cpRlso5P9E5p466UOsr/tKs5GWnhgbdxlgVAebuJKw93KJ8pciO
kvA9kbPwBHnOgW6Ytz73kBUrcBX4GixueddSftPTkMfxSB+Bm9UGWHlkZw6lo5P1
kh7p9qyVpQMZg7AEoiTtWWn4CQAn2DbVqM17Syi7Fmvc1VsbcG1vkM1fMAAFpAvO
vI2Kr6W9F9XoC7oJtb15mI3DnJPrbGNVzQSQzAWAoblRTyQv5kQFBDHBNPTYcCRJ
l3sy6P/VAI4dHgvAzVGvjL+w0dRszct8fvXCUGceRWeYYmfyZ8GLN53a0ywsN8Ik
gHvXAoIBACee5HEa9bt6bJihgf1DuFk1CKPtB2L8PN+1RAKEMfrolexAoG/tfvGa
7GH6l6ks8KX2BnfWeST2h66GHw6Xs8ydjQYUeV7nidqQ70EYbfSSXznZpvt1liaU
/VFKx4CcDT7jFIfaVlCZh6KADB9I/XXvRIh4SqF0fSO0XMcXsmeE7watapPAQ2iV
nl804yk4tBB9oi/JTcQ9Kr5et2UfW15wRiYf+5ZwaPsQ46cyHfPgsCSXztDB3plF
jTE5ShC4IKZJBQqcC6kk+0ifU8P0da6RpxuU96iUE3h9+sB/bCy+/FV7dq5gEbNy
znygAbOqAaFKqUXr7bkGY5ELm5lwGFECggEACcyaF9mMqLGghR55ew+cMmdeYdK3
meMLi5nrgtbQpVLlz+IV7Vdmrv7lZjeTr4nvU/5miU+p+If14CCFBiSucGq3Kmyp
OSM5cNCjDhw8uIDfY2qWCrZ2NSMR3qaAoBAQyQ2ER1IL98TDF/Qui0ZatbPiM4Ns
GErhkBZh3MCDSt24yiVKcUB79BxatWB4K7h7y8wqpX4Rj7rpfJMF7wz/I1cgyuCE
7XFpRwj7F1B2MmXnvV3KAgAD0EqrJDLeM0vIlDhpOUEaFUkuqmQyeB8qQkWfyXbD
jzloS3cNq0MBijB8oixwD2b4dVhBM7z8vQMX6OntN+97luWgO8OIukoYAg==
-----END RSA PRIVATE KEY-----
`)
publicKeyBytes := []byte(`-----BEGIN RSA PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApFf3qCQhAr2QLRRZdhLy
B8exLjrQiXLr8hwDe0xHSLJX3w7v5z4ujJWrHUulQ2/hvS8uffxMVrp2YqeA4sjy
/ku1KqZVQv/WNTdL2v9Z1ewbRnBjDQvmkWDKZ+cP8VPdHGzQ4iM2z6BZ4RQTkdQM
KqsVwUsLO9amI2TOny/M696eFRW0pk4+W3QZZRawT0HqJPvKKXKqoO2+62W54rOV
8glJi29Do06e4S6CZUl1hBUy0VlLtrLlRSOHTois0dF2a9f7+GGgU11MHO6w1k1N
esSlfZ0vnrrkW8WFLqewk+Jj+w1xeQnYHOeHjx6zi9f9DC96eSylSB3iJ71NmXcM
c0IEZ2LiQIqTL83BLOgMBCsK3FSlGMakQUR2GJ+M0I/selYkRMhid6eOmhlsTNMP
bpcTHxZ+ReIzS+5B1X0FZ7RIL+jSL9mFcxmD3dxurrrt/DkLpXcuWdi1s1bpVn3j
IQhU0+bgA6hT0k8Kj2f3Q9QnvkHSEff+XyLvLwQeSsSAcnM+1I7fNSPEo2cq0au6
ZtjHcirXmMminAQ6cKW1XrEvJBefHHibtjJjIM4bHH7MKRA5H3km/J4CCwI1VogS
TcE05Z6ypAFU2TCrnec9c9VXkRDP94h0GuoG8sdhmQudqvghr/8T7CV+sGRQbdeq
rXwanfnGPrjcMVIO/dSOxOUCAwEAAQ==
-----END RSA PUBLIC KEY-----
`)
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 nil, nil, "", err
}
secretKeyPairDir := filepath.Join(testDir, secrets, contexts, qlikDefaultContext, 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)
privKeyFile := filepath.Join(secretKeyPairDir, "qliksensePriv")
// construct and write priv key file into secretsDir location
err = ioutil.WriteFile(privKeyFile, privKeyBytes, 0777)
if err != nil {
log.Printf("Error while creating file: %v", err)
return nil, nil, "", err
}
pubKeyFile := filepath.Join(secretKeyPairDir, "qliksensePub")
api.LogDebugMessage("Test setup - \npub key path: %s\n, priv key path: %s\n", pubKeyFile, privKeyFile)
// construct and write pub key file into secretsDir location
err = ioutil.WriteFile(pubKeyFile, publicKeyBytes, 0777)
if err != nil {
log.Printf("Error while creating file: %v", err)
return nil, nil, "", err
}
return publicKeyBytes, privKeyBytes, targetFile, nil
}
func removePrivateKey() {
err := os.Remove(filepath.Join(testDir, secrets, contexts, qlikDefaultContext, secrets, "qliksensePriv"))
if err != nil {
log.Fatalf("Could not delete private key %v", err)
}
return
}
func setup() func() {
// create tests dir
os.RemoveAll(testDir)
if err := os.Mkdir(testDir, 0777); err != nil {
log.Printf("\nError occurred: %v", err)
}
@@ -30,7 +165,7 @@ metadata:
spec:
contexts:
- name: qlik-default
crLocation: /root/.qliksense/contexts/qlik-default.yaml
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`
configFile := filepath.Join(testDir, "config.yaml")
@@ -65,6 +200,22 @@ spec:
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()
@@ -73,7 +224,8 @@ func Test_retrieveCurrentContextInfo(t *testing.T) {
q := &Qliksense{
QliksenseHome: testDir,
}
_, _, err := retrieveCurrentContextInfo(q)
qConfig := api.NewQConfig(q.QliksenseHome)
_, err := qConfig.GetCurrentCR()
if err != nil {
t.FailNow()
}
@@ -108,18 +260,23 @@ func TestSetUpQliksenseContext(t *testing.T) {
},
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, err := New(tt.args.qlikSenseHome)
if err != nil {
t.Errorf("unable to create a qliksense instance")
return
}
if err := q.SetUpQliksenseContext(tt.args.contextName, tt.args.isDefaultContext); (err != nil) != tt.wantErr {
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)
}
})
@@ -147,11 +304,7 @@ func TestSetUpQliksenseDefaultContext(t *testing.T) {
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, err := New(tt.args.qlikSenseHome)
if err != nil {
t.Errorf("unable to create a qliksense instance")
return
}
q := New(tt.args.qlikSenseHome)
if err := q.SetUpQliksenseDefaultContext(); (err != nil) != tt.wantErr {
t.Errorf("SetUpQliksenseDefaultContext() error = %v, wantErr %v", err, tt.wantErr)
}
@@ -175,10 +328,30 @@ func TestSetOtherConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"profile=minikube", "namespace=qliksense", "storageClassName=efs"},
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()
@@ -207,7 +380,7 @@ func TestSetConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"qliksense[name=acceptEULA]=\"yes\"", "qliksense[name=mongoDbUri]=\"mongo://mongo:3307\""},
args: []string{"qliksense.acceptEULA=\"yes\"", "qliksense.mongoDbUri=\"mongo://mongo:3307\""},
},
wantErr: false,
},
@@ -225,7 +398,7 @@ func TestSetConfigs(t *testing.T) {
func TestSetImageRegistry(t *testing.T) {
getQlikSense := func(tmpQlikSenseHome string) (*Qliksense, error) {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(fmt.Sprintf(`
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
@@ -233,9 +406,9 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: %s/contexts/qlik-default/qlik-default.yaml
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`, tmpQlikSenseHome)), os.ModePerm); err != nil {
`), os.ModePerm); err != nil {
return nil, err
}
@@ -318,20 +491,448 @@ spec:
pushSecret.Username != testCase.pushUsername || pushSecret.Password != testCase.pushPassword {
t.Fatalf("unexpected push secret content: %v", pushSecret)
}
if pullSecret, err := qConfig.GetDockerConfigJsonSecret("image-registry-pull-secret.yaml"); err != nil {
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.Namespace != "some-namespace" ||
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.GetDockerConfigJsonSecret("image-registry-pull-secret.yaml"); err == nil {
} else if _, err := qConfig.GetPullDockerConfigJsonSecret(); err == nil {
t.Fatal("unexpected image-registry-pull-secret.yaml")
}
}
})
}
}
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()
removePrivateKey()
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
}
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: "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()
_, privateKeyBytes, _, err := setupTargetFileAndPrivateKey()
if err != nil {
t.FailNow()
}
defer tearDown()
privKey, err := api.DecodeToPrivateKey(privateKeyBytes)
if err != nil {
t.FailNow()
}
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); (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], "\"", "")
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()
}
decodedValue, err := b64.StdEncoding.DecodeString(valToBeEncrypted)
if err != nil {
err := fmt.Errorf("Error occurred while decoding: %v", err)
log.Printf("decode error: %v", err)
t.FailNow()
}
decryptedVal, err := api.Decrypt(decodedValue, privKey)
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 != "" {
return item.Value, nil
}
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); (err != nil) != tt.wantErr {
t.Errorf("DeleteContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -1,10 +1,10 @@
package qliksense
import (
"errors"
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"path/filepath"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
type CrdCommandOptions struct {
@@ -19,12 +19,24 @@ func (q *Qliksense) ViewCrds(opts *CrdCommandOptions) error {
fmt.Println("cannot get the current-context cr", err)
return err
}
if engineCRD, err := getQliksenseInitCrd(qcr); err != nil {
engineCRD, err := getQliksenseInitCrd(qcr)
if err != nil {
return err
} else if opts.All {
fmt.Printf("%s\n%s", q.GetOperatorCRDString(), engineCRD)
} else {
fmt.Printf("%s", engineCRD)
}
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
}
@@ -40,11 +52,19 @@ func (q *Qliksense) InstallCrds(opts *CrdCommandOptions) error {
if engineCRD, err := getQliksenseInitCrd(qcr); err != nil {
return err
} else if err = qapi.KubectlApply(engineCRD, qcr.Spec.NameSpace); err != nil {
} 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(), qcr.Spec.NameSpace); err != nil {
if err := qapi.KubectlApply(q.GetOperatorCRDString(), ""); err != nil {
fmt.Println("cannot do kubectl apply on opeartor CRD", err)
return err
}
@@ -53,17 +73,35 @@ func (q *Qliksense) InstallCrds(opts *CrdCommandOptions) error {
}
func getQliksenseInitCrd(qcr *qapi.QliksenseCR) (string, error) {
var repoPath string
var err error
if qcr.Spec.GetManifestsRoot() == "" {
return "", errors.New("Cannot find manifests root. Please use `qliksense fetch <version>`")
if qcr.Spec.GetManifestsRoot() != "" {
repoPath = qcr.Spec.GetManifestsRoot()
} else {
if repoPath, err = DownloadFromGitRepoToTmpDir(defaultConfigRepoGitUrl, "master"); err != nil {
return "", err
}
}
qInitMsPath := filepath.Join(qcr.Spec.GetManifestsRoot(), Q_INIT_CRD_PATH)
qInitByte, err := executeKustomizeBuild(qInitMsPath)
qInitMsPath := filepath.Join(repoPath, Q_INIT_CRD_PATH)
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,6 +1,7 @@
package qliksense
import (
"errors"
"fmt"
"io/ioutil"
"os"
@@ -27,6 +28,29 @@ const (
imageSharedBlobsDirName = "blobs"
)
func (q *Qliksense) PullImages(version, profile string) error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
if version != "" {
if err := q.FetchQK8s(version); err != nil {
return 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 e := qConfig.WriteCR(qcr); e != nil {
return e
}
}
return q.PullImagesForCurrentCR()
}
// PullImages ...
func (q *Qliksense) PullImagesForCurrentCR() error {
qConfig := qapi.NewQConfig(q.QliksenseHome)
@@ -48,7 +72,12 @@ func (q *Qliksense) PullImagesForCurrentCR() error {
return err
}
for _, image := range versionOut.Images {
images := versionOut.Images
if err := q.appendOperatorImages(&images); err != nil {
return err
}
for _, image := range images {
if err := pullImage(image, imagesDir); err != nil {
fmt.Printf("%v\n", err)
return err
@@ -64,6 +93,15 @@ func (q *Qliksense) PullImagesForCurrentCR() error {
return nil
}
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
}
}
func pullImage(image, imagesDir string) error {
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%v", image))
if err != nil {
@@ -134,7 +172,12 @@ func (q *Qliksense) PushImagesForCurrentCR() error {
return err
}
for _, image := range versionOut.Images {
images := versionOut.Images
if err := q.appendOperatorImages(&images); err != nil {
return err
}
for _, image := range images {
if err = pushImage(image, imagesDir, dockerConfigJsonSecret); err != nil {
fmt.Printf("%v\n", err)
return err

View File

@@ -22,6 +22,8 @@ import (
"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"
@@ -47,7 +49,9 @@ func Test_locateDockerRegistryBinary(t *testing.T) {
}
func Test_getSelfSignedCertAndKey(t *testing.T) {
selfSignedCert, key, err := getSelfSignedCertAndKey()
host := "andriy.registry.com"
validity := time.Hour * 24 * 365
selfSignedCert, key, err := getSelfSignedCertAndKey(host, validity)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -125,7 +129,10 @@ func Test_Pull_Push_ImagesForCurrentCR(t *testing.T) {
if err := setupQlikSenseHome(t, tmpQlikSenseHome, registry, testCase.clientAuth); err != nil {
t.Fatalf("unexpected error setting up qliksense home: %v", err)
}
q := &Qliksense{QliksenseHome: tmpQlikSenseHome}
q := &Qliksense{
QliksenseHome: tmpQlikSenseHome,
CrdBox: packr.New("crds", "./crds"),
}
var versionOut VersionOutput
if err := q.PullImagesForCurrentCR(); err != nil {
@@ -135,7 +142,7 @@ func Test_Pull_Push_ImagesForCurrentCR(t *testing.T) {
} 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")
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 {
@@ -164,7 +171,7 @@ func Test_Pull_Push_ImagesForCurrentCR(t *testing.T) {
}
func setupQlikSenseHome(t *testing.T, tmpQlikSenseHome string, registry *testRegistryV2, clientAuth clientAuthType) error {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(fmt.Sprintf(`
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
@@ -172,9 +179,9 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: %s/contexts/qlik-default/qlik-default.yaml
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`, tmpQlikSenseHome)), os.ModePerm); err != nil {
`), os.ModePerm); err != nil {
return err
}
@@ -345,7 +352,7 @@ http:
)
var env []string
if auth {
if certificate, key, err := getSelfSignedCertAndKey(); err != nil {
if certificate, key, err := getSelfSignedCertAndKey("localhost", time.Hour*24); err != nil {
return nil, err
} else {
certPath := filepath.Join(tmp, "domain.crt")
@@ -464,7 +471,7 @@ func consumeAndLogOutputs(id string, cmd *exec.Cmd) (*bytes.Buffer, *bytes.Buffe
return consumeAndLogOutputStream(id+" stdout", stdout), consumeAndLogOutputStream(id+" stderr", stderr), nil
}
func getSelfSignedCertAndKey() (certificate, key []byte, err error) {
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
@@ -477,13 +484,14 @@ func getSelfSignedCertAndKey() (certificate, key []byte, err error) {
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
Organization: []string{"self-signed"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24),
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)

View File

@@ -0,0 +1,35 @@
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()
}
}

View File

@@ -0,0 +1,88 @@
package qliksense
import (
"errors"
"fmt"
"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
}

View File

@@ -3,19 +3,21 @@ package qliksense
import (
"errors"
"fmt"
"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"
)
type InstallCommandOptions struct {
AcceptEULA string
Namespace string
StorageClass string
MongoDbUri string
RotateKeys string
}
func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions) error {
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)
@@ -24,6 +26,13 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions) err
// 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 {
@@ -31,40 +40,66 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions) err
return err
}
if opts.AcceptEULA != "" {
qcr.Spec.AddToConfigs("qliksense", "acceptEULA", opts.AcceptEULA)
}
qcr.SetEULA("yes")
if opts.MongoDbUri != "" {
qcr.Spec.AddToSecrets("qliksense", "mongoDbUri", opts.MongoDbUri, "")
}
if opts.StorageClass != "" {
qcr.Spec.StorageClassName = opts.StorageClass
}
if opts.Namespace != "" {
qcr.Spec.NameSpace = opts.Namespace
}
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
}
// 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 err := qapi.KubectlApply(q.GetOperatorControllerString(), qcr.Spec.NameSpace); err != nil {
operatorControllerString := q.GetOperatorControllerString()
if imageRegistry := qcr.GetImageRegistry(); imageRegistry != "" {
operatorControllerString, err = kustomizeForImageRegistry(operatorControllerString, pullSecretName,
"qlik/qliksense-operator", fmt.Sprintf("%v/qliksense-operator", imageRegistry))
if err != nil {
return err
}
}
if err := qapi.KubectlApply(operatorControllerString, ""); err != nil {
fmt.Println("cannot do kubectl apply on opeartor controller", err)
return err
}
if qcr.Spec.Git.Repository != "" {
// fetching and applying manifest will be in the operator controller
return q.applyCR(qcr.Spec.NameSpace)
// create patch dependent resoruces
fmt.Println("Installing resoruces used kuztomize patch")
if err := q.createK8sResoruceBeforePatch(qcr); err != nil {
return err
}
if version != "" { // no need to fetch manifest root already set by some other way
if err := fetchAndUpdateCR(qConfig, version); err != nil {
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 version == "" {
version = qcr.GetLabelFromCr("version")
}
if err := fetchAndUpdateCR(qConfig, version); err != nil {
return err
}
qcr, err = qConfig.GetCurrentCR()
if err != nil {
@@ -76,25 +111,100 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions) err
// install generated manifests into cluster
fmt.Println("Installing generated manifests into cluster")
if err := q.applyConfigToK8s(qcr); err != nil {
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else if err := q.applyConfigToK8s(dcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
} else {
return q.applyCR(dcr)
}
return q.applyCR(qcr.Spec.NameSpace)
}
func (q *Qliksense) applyCR(ns string) error {
func installOrRemoveImagePullSecret(qConfig *qapi.QliksenseConfig) error {
if pullDockerConfigJsonSecret, err := qConfig.GetPullDockerConfigJsonSecret(); err == nil {
if dockerConfigJsonSecretYaml, err := pullDockerConfigJsonSecret.ToYaml(nil); 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(nil); 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) {
fSys := filesys.MakeFsInMemory()
if err := fSys.WriteFile("/resources.yaml", []byte(resources)); err != nil {
return "", err
} else if err := fSys.WriteFile("/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))); err != nil {
return "", err
} else if err := fSys.WriteFile("/kustomization.yaml", []byte(fmt.Sprintf(`
resources:
- resources.yaml
transformers:
- addImagePullSecrets.yaml
images:
- name: %s
newName: %s
`, name, newName))); err != nil {
return "", err
} else if out, err := executeKustomizeBuildForFileSystem("/", fSys); 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 := q.getCurrentCRString()
r, err := cr.GetString()
if err != nil {
return err
}
if err := qapi.KubectlApply(r, ns); err != nil {
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,65 @@
package qliksense
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
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()
}

View File

@@ -1,24 +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,
@@ -35,10 +42,26 @@ func executeKustomizeBuild(directory string) ([]byte, error) {
func executeKustomizeBuildWithStdoutProgress(path string) (kuzManifest []byte, err error) {
result, err := api.ExecuteTaskWithBlinkingStdoutFeedback(func() (interface{}, error) {
return executeKustomizeBuild(path)
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 strings.TrimSpace(scanner.Text()) == "kind: "+kind {
resultDocs = resultDocs + "\n---\n" + doc
break
}
}
}
return resultDocs
}

View File

@@ -20,7 +20,7 @@ import (
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)
@@ -41,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)
}
@@ -66,7 +66,7 @@ func Test_executeKustomizeBuild_onQlikConfig_regenerateKeys(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 {
t.Fatalf("unexpected error: %v\n", err)
@@ -86,7 +86,7 @@ func Test_executeKustomizeBuild_onQlikConfig_regenerateKeys(t *testing.T) {
generateKeys(cr, "won't-use")
yamlResources, err := executeKustomizeBuild(path.Join(configPath, "manifests", "base", "resources", "users"))
yamlResources, err := ExecuteKustomizeBuild(path.Join(configPath, "manifests", "base", "resources", "users"))
if err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
@@ -139,3 +139,97 @@ func getEjsonKeyDir(defaultKeyDir string) string {
}
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)
}
}

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

@@ -0,0 +1,89 @@
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
rsaPublicKey, _, err := qConfig.GetContextEncryptionKeyPair(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, rsaPublicKey, cr, false); err != nil {
return cr.GetName(), 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

@@ -2,29 +2,21 @@
package qliksense
import (
"os"
"path"
"github.com/gobuffalo/packr/v2"
)
// Qliksense is the logic behind the qliksense client
type Qliksense struct {
QliksenseHome string
QliksenseEjsonKeyDir string
CrdBox *packr.Box ``
QliksenseHome string
CrdBox *packr.Box ``
}
// New qliksense client, initialized with useful defaults.
func New(qliksenseHome string) (*Qliksense, error) {
func New(qliksenseHome string) *Qliksense {
qliksenseClient := &Qliksense{
QliksenseHome: qliksenseHome,
CrdBox: packr.New("crds", "./crds"),
}
qliksenseClient.QliksenseEjsonKeyDir = path.Join(qliksenseHome, "ejson", "keys")
if err := os.MkdirAll(qliksenseClient.QliksenseEjsonKeyDir, os.ModePerm); err != nil {
return nil, err
}
return qliksenseClient, nil
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

@@ -13,13 +13,9 @@ func (q *Qliksense) UninstallQK8s(contextName string) error {
} else if !qConfig.IsContextExist(contextName) {
return errors.New("context name [ " + contextName + " ] not found")
}
cr, err := qConfig.GetCurrentCR()
if err != nil {
return err
}
str, err := q.getCRString(contextName)
if err != nil {
return err
}
return qapi.KubectlDelete(str, cr.Spec.NameSpace)
return qapi.KubectlDelete(str, "")
}

View File

@@ -6,7 +6,7 @@ import (
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) UpgradeQK8s() error {
func (q *Qliksense) UpgradeQK8s(keepPatchFiles bool) error {
// step1: get CR
// step2: run kustomize
@@ -14,6 +14,13 @@ func (q *Qliksense) UpgradeQK8s() error {
// 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 {
@@ -21,19 +28,20 @@ func (q *Qliksense) UpgradeQK8s() error {
return err
}
qcr.Spec.RotateKeys = "no"
if err := q.applyConfigToK8s(qcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
}
fmt.Println("Install operator CR into cluster")
r, err := qcr.GetString()
dcr, err := qConfig.GetDecryptedCr(qcr)
if err != nil {
return err
}
if err := qapi.KubectlApply(r, qcr.Spec.NameSpace); err != nil {
fmt.Println("cannot do kubectl apply on operator CR")
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)
}
return nil
err = q.applyConfigToK8s(dcr)
if err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
}
return q.applyCR(dcr)
}