Compare commits

...

105 Commits

Author SHA1 Message Date
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
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
56 changed files with 2750 additions and 658 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

View File

@@ -4,8 +4,8 @@ on:
branches:
- master
paths:
- docs/
- mkdocs.yml
- 'docs/**'
- 'mkdocs.yml'
jobs:
build:

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/**/*

3
.gitignore vendored
View File

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

View File

@@ -46,28 +46,35 @@ build: clean generate
.PHONY: 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
-rm -rf $(TMP)/docker-distribution
$(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 ./...
@@ -84,12 +91,16 @@ 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
-rm -rf $(TMP)/operator
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

133
README.md
View File

@@ -1,17 +1,9 @@
# (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 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:
@@ -31,120 +23,3 @@ For each version of a qliksense edge build there should be a corresponding relea
- backup/restore operations
- fully support airgap installation of QSEoK
- restore unwanted deletion of kubernetes resources
## 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 -h
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 as key-value pairs
set-context Sets the context in which the Kubernetes cluster and resources live in
set-secrets set secrets configurations into the qliksense context as key-value pairs
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

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,34 +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 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
```

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

@@ -20,7 +20,7 @@ func configCmd(q *qliksense.Qliksense) *cobra.Command {
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 {

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

@@ -14,18 +14,19 @@ func installCmd(q *qliksense.Qliksense) *cobra.Command {
Long: `install a qliksense release`,
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, keepPatchFiles)
version := ""
if len(args) != 0 {
version = args[0]
}
return q.InstallQK8s(args[0], opts, keepPatchFiles)
return q.InstallQK8s(version, opts, keepPatchFiles)
},
}
f := c.Flags()
f.StringVarP(&opts.AcceptEULA, "acceptEULA", "a", "", "AcceptEULA for 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)
}
}

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

@@ -0,0 +1,102 @@
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 preflightCheckDnsCmd(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 preflightCheckK8sVersionCmd(q *qliksense.Qliksense) *cobra.Command {
var preflightCheckK8sVersionCmd = &cobra.Command{
Use: "k8s-version",
Short: "check k8s version",
Long: `check minimum valid k8s version on the cluster`,
Example: `qliksense preflight k8s-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 preflightAllChecksCmd(q *qliksense.Qliksense) *cobra.Command {
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)
return nil
},
}
return preflightAllChecksCmd
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
@@ -19,7 +18,7 @@ import (
"github.com/ttacon/chalk"
)
// To run this project in ddebug mode, run:
// To run this project in debug mode, run:
// export QLIKSENSE_DEBUG=true
// qliksense <command>
@@ -42,18 +41,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
}
@@ -83,30 +78,60 @@ 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)
}
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))
@@ -151,14 +176,12 @@ 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))
@@ -169,40 +192,21 @@ 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(preflightCheckDnsCmd(p))
preflightCmd.AddCommand(preflightCheckK8sVersionCmd(p))
preflightCmd.AddCommand(preflightAllChecksCmd(p))
//preflightCmd.AddCommand(preflightCheckMongoCmd(p))
//preflightCmd.AddCommand(preflightCheckAllCmd(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
@@ -231,28 +235,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 {

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

View File

@@ -1,6 +1,6 @@
# How qliksense cli works
At the initialization `qliksense` cli create few files in the director `~/.qliksene` and it contains following files
At the initialization, `qliksense` cli creates few files in the director `~/.qliksene` and it contains following files:
```console
.qliksense
@@ -12,7 +12,7 @@ At the initialization `qliksense` cli create few files in the director `~/.qliks
└── keys
```
`qlik-default.yaml` is a default CR has been created with some default values like this
`qlik-default.yaml` is a default CR created with some default values like:
```yaml
apiVersion: qlik.com/v1
@@ -29,55 +29,69 @@ spec:
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.
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
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 as key-value pairs
set-context Sets the context in which the Kubernetes cluster and resources live in
set-secrets set secrets configurations into the qliksense context as key-value pairs
view view the qliksense operator CR
Flags:
-h, --help help for config
Use "qliksense config [command] --help" for more information about a command.
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
- 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`.
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 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.
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
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
Create a fork or clone of [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) and push it to your git repo/server
- 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
To add your repo into CR, perform the following:
This is how repo info is provided into the CR
```console
```bash
qliksense config set git.repository="https://github.com/my-org/qliksense-k8s"
qliksense config set git.accessToken=blablalaala
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
....
```

View File

@@ -2,29 +2,52 @@
## 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)
- 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_
## Download
## 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)
- 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`.
## 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
qliksense fetch v0.0.2
```
- To install CRDs for QSEoK and qliksense operator into the kubernetes cluster.
```shell
$qliksense crds install --all
qliksense crds install --all
```
- To install QSEoK into a namespace in the kubernetes cluster where `kubectl` is pointing to.
```shell
$qliksense install --acceptEULA="yes"
```
qliksense install --acceptEULA="yes"
```

View File

@@ -1,19 +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:
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
- 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.
!!! 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
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)
!!! info ""
See QlikSense [edge releases on qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s/releases) repository
## Future Direction
- More operations:
- Expand preflight checks
- backup/restore operations
- fully support airgap installation of QSEoK
- restore unwanted deletion of kubernetes resources
Operations:
- Expand preflight checks
- Backup/restore operations
- Fully support airgap installation of QSEoK
- Restore unwanted deletion of kubernetes resources

98
docs/preflight_checks.md Normal file
View File

@@ -0,0 +1,98 @@
##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
```
### Running all checks
Run the command shown below to execute all preflight checks.
```console
$ 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
```

12
go.mod
View File

@@ -16,6 +16,7 @@ replace (
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
@@ -37,24 +38,29 @@ require (
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.17
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-20200302210943-78000ba7a073 // 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-20200226121028-0de0cce0169b
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
golang.org/x/tools v0.0.0-20200309202150-20ab64c0d93f // 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
)

40
go.sum
View File

@@ -696,9 +696,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=
@@ -843,14 +847,36 @@ 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.17 h1:tOdrEe9gfb9CXq0+uowFnXIsI781qz/zgeN8xqupXYw=
github.com/qlik-oss/k-apis v0.0.17/go.mod h1:KOFzKVIdRqp47ytnHg3+9zb8fTlnrQjO6aKiwcrCJUE=
github.com/qlik-oss/k-apis v0.0.22 h1:tntQEeRqDYkBi2Ku5+xt7ABGMeFPck7+DOKrHUnpzwI=
github.com/qlik-oss/k-apis v0.0.22/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.23 h1:w4lj2PHDTtKkukgoT2a6/jAY3NkkI/V0vNetkRzEXXY=
github.com/qlik-oss/k-apis v0.0.23/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.24 h1:cBXggOMeEaUpxO91TfI2jQToh5r/ojBPcjuJM7cBRIM=
github.com/qlik-oss/k-apis v0.0.24/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.25 h1:OF4YZDklkNyvPKKLOQ4a8JMQaFaqD/vevl6wOOzlhek=
github.com/qlik-oss/k-apis v0.0.25/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.27 h1:W9z4ysAqi4TNEOxJZKUqrEYd0u/KyGe0/hKvdOuNT7A=
github.com/qlik-oss/k-apis v0.0.27/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.28 h1:c+4fTb3q2eiP7FoABV/LKQtPhABd1Auk1SFGhMeNeBg=
github.com/qlik-oss/k-apis v0.0.28/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.29 h1:TS3X/jxEuckSMKHEQA9L88qTa/t2VwbIQY3UliO7oBI=
github.com/qlik-oss/k-apis v0.0.29/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.30 h1:JTITSQj1A6Pbz7URnMHW8mdZscRKJ2suL8E7yRTmOfk=
github.com/qlik-oss/k-apis v0.0.30/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.31 h1:mPdxSRVViY1lP0MUlaz5Ki4TQb4IIV89HHekeYE8q0w=
github.com/qlik-oss/k-apis v0.0.31/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
github.com/qlik-oss/k-apis v0.0.32 h1:N6Pw4g4ktwkNzAWR7ZgfNlBPZ7LOVAGrPuj04kTjkLs=
github.com/qlik-oss/k-apis v0.0.32/go.mod h1:DNiWYqCqPIN216l7+1rccArNIYPb1Le7kYDcPSyNp+Q=
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.20200206224201-2e697eccbad9 h1:iqeqTS4zjp6rPEaxmFB7pemA2CMjOEN5dYSXZaQ82uw=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200206224201-2e697eccbad9/go.mod h1:OCt7FTrRVHj4kmR2xLJJUIqu00BTr6GeF09hSmM17Kw=
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=
@@ -1040,10 +1066,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-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1205,8 +1229,8 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK
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-20200309202150-20ab64c0d93f h1:NbrfHxef+IfdI86qCgO/1Siq1BuMH2xG0NqgvCguRhQ=
golang.org/x/tools v0.0.0-20200309202150-20ab64c0d93f/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
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=

View File

@@ -10,9 +10,15 @@ 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

@@ -22,34 +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 {
qc, err := NewQConfigE(qsHome)
if err != nil {
fmt.Println("yaml unmarshalling error ", err)
os.Exit(1)
}
return qc
}
func NewQConfigE(qsHome string) (*QliksenseConfig, error) {
configFile := filepath.Join(qsHome, "config.yaml")
qc := &QliksenseConfig{}
err := ReadFromFile(qc, configFile)
if err != nil {
fmt.Println("yaml unmarshalling error ", err)
os.Exit(1)
return nil, err
}
qc.QliksenseHomePath = qsHome
return qc
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,7 +86,8 @@ func (qc *QliksenseConfig) SetCrLocation(contextName, filepath string) (*Qliksen
return nil, errors.New("cannot find the context")
}
func getCRObject(crfile string) (*QliksenseCR, error) {
// GetCRObject create a qliksense CR object from file
func GetCRObject(crfile string) (*QliksenseCR, error) {
cr := &QliksenseCR{}
err := ReadFromFile(cr, crfile)
if err != nil {
@@ -87,11 +98,36 @@ func getCRObject(crfile string) (*QliksenseCR, error) {
return cr, nil
}
func (qc *QliksenseConfig) getCRFilePath(contextName string) string {
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 := ReadFromStream(cr, strings.NewReader(crContent))
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return nil, err
}
return cr, nil
}
func (qc *QliksenseConfig) GetCRFilePath(contextName string) string {
crFilePath := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = ctx.CrFile
crFilePath = filepath.Join(qc.QliksenseHomePath, ctx.CrFile)
break
}
}
@@ -116,23 +152,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")
}
return WriteToFile(cr, crf)
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 {
@@ -221,6 +305,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") != "" {
@@ -229,18 +322,34 @@ 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.GetObjectMeta().GetName(), 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
}
@@ -329,6 +438,10 @@ func (cr *QliksenseCR) IsEULA() bool {
return false
}
func (cr *QliksenseCR) SetEULA(value string) {
cr.Spec.AddToConfigs("qliksense", "acceptEULA", value)
}
// GetDecryptedCr it decrypts all the encrypted value and return a new CR
func (qc *QliksenseConfig) GetDecryptedCr(cr *QliksenseCR) (*QliksenseCR, error) {
newCr := &QliksenseCR{}
@@ -361,3 +474,45 @@ func (qc *QliksenseConfig) GetDecryptedCr(cr *QliksenseCR) (*QliksenseCR, error)
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

@@ -74,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()
@@ -100,7 +100,7 @@ func TestGetDecryptedCr(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()

View File

@@ -3,8 +3,10 @@ package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"github.com/qlik-oss/k-apis/pkg/config"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -103,14 +105,22 @@ func ReadFromFile(content interface{}, sourceFile string) error {
if content == nil || sourceFile == "" {
return nil
}
contents, err := ioutil.ReadFile(sourceFile)
file, e := os.Open(sourceFile)
if e != nil {
return e
}
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 file: %s, %v", sourceFile, err)
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)
dec.Decode(content)
return nil
return dec.Decode(content)
}

View File

@@ -97,3 +97,22 @@ func kubectlOperation(manifests string, oprName string, namespace string) error
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
}

View File

@@ -1,6 +1,8 @@
package api
import (
"fmt"
"strings"
"testing"
)
@@ -15,3 +17,16 @@ func TestGetKubectlNamespace(t *testing.T) {
}
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,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 {
@@ -62,7 +70,7 @@ func ProcessConfigArgs(args []string) ([]*ServiceKeyValue, error) {
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)
@@ -109,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
}

View File

@@ -0,0 +1,34 @@
package preflight
import (
"fmt"
)
func (qp *QliksensePreflight) RunAllPreflightChecks(namespace string, kubeConfigContents []byte) {
checkCount := 0
// 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++
}
// 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++
}
if checkCount == 2 {
fmt.Printf("All preflight checks have PASSED\n")
} else {
fmt.Printf("1 or more preflight checks have FAILED\n")
}
fmt.Println("Completed running all preflight checks")
}

115
pkg/preflight/dns_check.go Normal file
View File

@@ -0,0 +1,115 @@
package preflight
import (
"fmt"
"strings"
"time"
)
const (
nginxImage = "nginx"
netcatImage = "subfuzion/netcat:latest"
)
func (qp *QliksensePreflight) CheckDns(namespace string, kubeConfigContents []byte) error {
clientset, clientConfig, err := getK8SClientSet(kubeConfigContents, "")
if err != nil {
err = fmt.Errorf("Kube config error: %v\n", err)
fmt.Print(err)
return err
}
// creating deployment
depName := "dep-dns-preflight-check"
dnsDeployment, err := createPreflightTestDeployment(clientset, namespace, depName, nginxImage)
if err != nil {
err = fmt.Errorf("Unable to create deployment: %v\n", err)
return err
}
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
WAIT:
for {
d, err := getDeployment(dnsDeployment.GetName(), clientset, namespace)
if err != nil {
err = fmt.Errorf("Unable to retrieve deployment: %s\n", depName)
return err
}
select {
case <-timeout.C:
break WAIT
default:
if int(d.Status.ReadyReplicas) > 0 {
break WAIT
}
}
time.Sleep(5 * time.Second)
}
defer deleteDeployment(clientset, namespace, depName)
// creating service
serviceName := "svc-dns-pf-check"
dnsService, err := createPreflightTestService(clientset, namespace, serviceName)
if err != nil {
err = fmt.Errorf("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, netcatImage)
if err != nil {
err = fmt.Errorf("Unable to create pod : %s\n", podName)
return err
}
defer deletePod(clientset, namespace, podName)
if len(dnsPod.Spec.Containers) > 0 {
timeout := time.NewTicker(2 * time.Minute)
defer timeout.Stop()
OUT:
for {
dnsPod, err = getPod(clientset, namespace, dnsPod.Name)
if err != nil {
err = fmt.Errorf("Unable to retrieve service by name: %s\n", podName)
fmt.Println(err)
return err
}
select {
case <-timeout.C:
break OUT
default:
if len(dnsPod.Status.ContainerStatuses) > 0 && dnsPod.Status.ContainerStatuses[0].Ready {
break OUT
}
}
time.Sleep(5 * time.Second)
}
if len(dnsPod.Status.ContainerStatuses) == 0 || !dnsPod.Status.ContainerStatuses[0].Ready {
err = fmt.Errorf("container is taking much longer than expected")
fmt.Println(err)
return err
}
fmt.Println("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("An error occurred while executing remote command in container: %v", err)
fmt.Println(err)
return err
}
//fmt.Printf("stdout: %s\n", stdout)
//fmt.Printf("stderr: %s\n", stderr)
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,379 @@
package preflight
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"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"
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"
)
type QliksensePreflight struct {
Q *qliksense.Qliksense
}
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, "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(depName string, clientset *kubernetes.Clientset, namespace string) (*appsv1.Deployment, error) {
deploymentsClient := clientset.AppsV1().Deployments(namespace)
var deployment *appsv1.Deployment
if err := retryOnError(func() (err error) {
deployment, err = deploymentsClient.Get(depName, v1.GetOptions{})
return err
}); err != nil {
err = errors.Wrapf(err, "unable to get deployments in the %s namespace", namespace)
fmt.Println(err)
return nil, err
}
return deployment, nil
}
func deleteDeployment(clientset *kubernetes.Clientset, namespace, name string) {
deploymentsClient := clientset.AppsV1().Deployments(namespace)
// Create Deployment
deletePolicy := v1.DeletePropagationForeground
deleteOptions := v1.DeleteOptions{
PropagationPolicy: &deletePolicy,
}
if err := retryOnError(func() (err error) {
return deploymentsClient.Delete(name, &deleteOptions)
}); err != nil {
fmt.Println(err)
return
}
fmt.Printf("Deleted deployment: %s\n", name)
}
func createPreflightTestService(clientset *kubernetes.Clientset, namespace string, svcName string) (*apiv1.Service, error) {
//fmt.Println("Creating Service...")
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,
}
if err := retryOnError(func() (err error) {
return podsClient.Delete(name, &deleteOptions)
}); err != nil {
fmt.Println(err)
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": "demo",
},
},
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) {
fmt.Printf("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 {
fmt.Println(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
}

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

View File

@@ -0,0 +1,58 @@
package preflight
import (
"fmt"
"github.com/Masterminds/semver/v3"
"k8s.io/apimachinery/pkg/version"
)
const minK8sVersion = "1.11.0"
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(minK8sVersion)
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, hence good to go\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\n")
return nil
}

BIN
pkg/qliksense/.DS_Store vendored Normal file

Binary file not shown.

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

@@ -59,14 +59,21 @@ func (q *Qliksense) ConfigApplyQK8s() error {
}
}
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
}
}
@@ -99,7 +106,6 @@ func (q *Qliksense) ConfigViewCR() error {
if err != nil {
return err
}
fmt.Println(r)
oth, err := q.getCurrentCrDependentResourceAsString()
if err != nil {
return err

View File

@@ -3,16 +3,20 @@ 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"
"github.com/qlik-oss/k-apis/pkg/config"
b64 "encoding/base64"
ansi "github.com/mattn/go-colorable"
"github.com/qlik-oss/sense-installer/pkg/api"
"github.com/ttacon/chalk"
@@ -36,7 +40,7 @@ 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
}
@@ -58,9 +62,7 @@ func (q *Qliksense) SetSecrets(args []string, isSecretSet bool) error {
}
}
// 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 {
@@ -128,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
}
@@ -141,55 +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)
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 })
}
if err := api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return nil, "", err
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
}
}
}
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 nil, "", err
// 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{}
}
}
}
// read the context.yaml file
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 nil, "", 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 nil, "", err
}
if err := api.ReadFromFile(qliksenseCR, qliksenseContextsFile); err != nil {
log.Println(err)
return nil, "", err
}
api.LogDebugMessage("Read context file: %s, Read QliksenseCR: %v", qliksenseContextsFile, qliksenseCR)
return qliksenseCR, qliksenseContextsFile, nil
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
}
@@ -203,44 +217,46 @@ 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 "git.repository":
if qliksenseCR.Spec.Git == nil {
qliksenseCR.Spec.Git = &config.Repo{}
}
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:
err := fmt.Errorf("Please enter one of: profile, storageClassName,rotateKeys, manifestRoot or git.repository arguments to configure the current context")
log.Println(err)
return err
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 {
err := q.SetUpQliksenseContext(args[0], false)
err := q.SetUpQliksenseContext(args[0])
if err != nil {
return err
}
@@ -265,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, "")
@@ -338,11 +354,17 @@ func (q *Qliksense) DeleteContextConfig(args []string) error {
// 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)
@@ -356,83 +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)
if err := api.ReadFromFile(qliksenseConfig, qliksenseConfigFile); err != nil {
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 := &api.QliksenseCR{}
qliksenseCR.AddCommonConfig(contextName)
api.WriteToFile(&qliksenseCR, qliksenseContextFile)
api.LogDebugMessage("Added Context: %s", contextName)
}
// else {
// if err := api.ReadFromFile(&qliksenseCR, qliksenseContextFile); 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
}
// set the encrypted default mongo
q.SetSecrets([]string{`qliksense.mongoDbUri="mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"`}, false)
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) {
@@ -514,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
}
@@ -541,5 +510,18 @@ func (q *Qliksense) SetImageRegistry(registry, pushUsername, pushPassword, pullU
}
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

@@ -152,6 +152,7 @@ func removePrivateKey() {
func setup() func() {
// create tests dir
os.RemoveAll(testDir)
if err := os.Mkdir(testDir, 0777); err != nil {
log.Printf("\nError occurred: %v", err)
}
@@ -164,7 +165,7 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: /root/.qliksense/contexts/qlik-default.yaml
crFile: contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`
configFile := filepath.Join(testDir, "config.yaml")
@@ -223,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()
}
@@ -273,12 +275,8 @@ func TestSetUpQliksenseContext(t *testing.T) {
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)
}
})
@@ -306,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)
}
@@ -334,7 +328,7 @@ func TestSetOtherConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"profile=minikube", "rotateKeys=yes", "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,
},
@@ -344,7 +338,7 @@ func TestSetOtherConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"someconfig=somevalue"},
args: []string{"someconfig=somevalue, gitops.schedule=bar", "gitops.enabled=bar", "git.foo=bar", "rotatekeys=bar"},
},
wantErr: true,
},
@@ -404,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:
@@ -412,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
}
@@ -806,11 +800,11 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: /root/.qliksense/contexts/qlik-default.yaml
crFile: contexts/qlik-default.yaml
- name: qlik1
crFile: /root/.qliksense/contexts/qlik1.yaml
crFile: contexts/qlik1.yaml
- name: qlik2
crFile: /root/.qliksense/contexts/qlik2.yaml
crFile: contexts/qlik2.yaml
currentContext: qlik1
`
configFile := filepath.Join(testDir, "config.yaml")
@@ -836,7 +830,7 @@ spec:
log.Fatal(err)
}
contextYaml1 :=
`
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
@@ -846,8 +840,8 @@ spec:
rotateKeys: "yes"
releaseName: qlik1`
contextYaml2 :=
`
contextYaml2 :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
@@ -932,11 +926,7 @@ func TestDeleteContexts(t *testing.T) {
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)
var arg []string
arg = append(arg, tt.args.contextName)
if err := q.DeleteContextConfig(arg); (err != nil) != tt.wantErr {

View File

@@ -171,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:
@@ -179,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
}

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

@@ -12,7 +12,6 @@ import (
)
type InstallCommandOptions struct {
AcceptEULA string
StorageClass string
MongoDbUri string
RotateKeys string
@@ -41,9 +40,7 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions, kee
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, "")
}
@@ -97,10 +94,11 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions, kee
return q.applyCR(dcr)
}
}
if version != "" { // no need to fetch manifest root already set by some other way
if err := fetchAndUpdateCR(qConfig, version); err != nil {
return err
}
if version == "" {
version = qcr.GetLabelFromCr("version")
}
if err := fetchAndUpdateCR(qConfig, version); err != nil {
return err
}
qcr, err = qConfig.GetCurrentCR()

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

@@ -6,6 +6,7 @@ import (
"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"

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
}

View File

@@ -28,19 +28,20 @@ func (q *Qliksense) UpgradeQK8s(keepPatchFiles bool) 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, ""); 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)
}