Compare commits

..

2 Commits

Author SHA1 Message Date
bearium
f78aca62b6 added testing 2020-03-19 12:26:53 -04:00
bearium
4b4348df38 unset and unset-config commands 2020-03-19 11:59:06 -04:00
58 changed files with 717 additions and 2604 deletions

43
.circleci/config.yml Normal file
View File

@@ -0,0 +1,43 @@
# 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
View File

@@ -1,5 +0,0 @@
# 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

View File

@@ -1,27 +0,0 @@
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

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

View File

@@ -1,37 +0,0 @@
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/**/*

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@ pkg/qliksense/packrd
pkg/qliksense/qliksense-packr.go
pkg/qliksense/docker-registry
/pkg/qliksense/tests
.DS_Store

View File

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

View File

@@ -46,35 +46,28 @@ build: clean generate
.PHONY: test
test: clean generate
ifeq ($(shell ${WHICH} docker-registry 2>${DEVNUL}),)
$(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)
$(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
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)
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)
#tar -C $(BINDIR)/$(VERSION)/ -cvf $(BINDIR)/$(VERSION)/$(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH).tar.gz $(MIXIN)-$(CLIENT_PLATFORM)-$(CLIENT_ARCH)$(FILE_EXT)
generate: get-crds packr2
go generate ./...
@@ -91,16 +84,12 @@ clean-packr: packr2
cd pkg/qliksense && packr2 clean
get-crds:
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
$(eval TMP := $(shell mktemp -d))
git clone git@github.com:qlik-oss/qliksense-operator.git -b ms-3 $(TMP)/operator
mkdir -p pkg/qliksense/crds/cr
mkdir -p pkg/qliksense/crds/crd
mkdir -p pkg/qliksense/crds/crd-deploy
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
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

133
README.md
View File

@@ -1,9 +1,17 @@
# (WIP) Qlik Sense installation and operations CLI
## Documentation
To learn more about Qlik Sense installer go to https://qlik-oss.github.io/sense-installer/
- [Qlik Sense installation and operations CLI](#qlik-sense-installation-and-operations-cli)
- [About](#about)
- [Future Direction](#future-direction)
- [Getting Started](#getting-started)
- [Requirements](#requirements)
- [Download](#download)
- [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)
## 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:
@@ -23,3 +31,120 @@ 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

41
action_about.md Normal file
View File

@@ -0,0 +1,41 @@
# 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

34
action_config.md Normal file
View File

@@ -0,0 +1,34 @@
# 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,37 +0,0 @@
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 patches and apply manifests to k8s",
Short: "generate the patchs 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

@@ -64,6 +64,25 @@ qliksense config set <key>=<value>
return cmd
}
func unsetOtherConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
cmd = &cobra.Command{
Use: "un-set",
Short: "remove a key value pair in the current context",
Example: `
qliksense config unset <key>
- The above configuration will be removed in the CR
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.UnsetOtherConfigs(args)
},
}
return cmd
}
func setConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
@@ -82,6 +101,24 @@ qliksense config set-configs <service_name>.<attribute>="<value>"
}
return cmd
}
func unsetConfigsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
cmd = &cobra.Command{
Use: "unset-configs",
Short: "unset configurations in the qliksense context as key-value pairs",
Example: `
qliksense config unset-configs <service_name>.<attribute>="<value>"
- The above configuration will be removed in the CR
`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.UnsetConfigs(args)
},
}
return cmd
}
func setSecretsCmd(q *qliksense.Qliksense) *cobra.Command {
var (
@@ -115,18 +152,15 @@ func deleteContextConfigCmd(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
skipConfirmation := false
cmd = &cobra.Command{
Use: "delete-context",
Short: "deletes a specific context locally (not in-cluster)",
Example: `qliksense config delete-contexts <context_name>`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.DeleteContextConfig(args, skipConfirmation)
return q.DeleteContextConfig(args)
},
}
f := cmd.Flags()
f.BoolVar(&skipConfirmation, "yes", skipConfirmation, "skips confirmation")
return cmd
}

View File

@@ -1,106 +0,0 @@
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()
fmt.Printf("%v\n", answer)
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.ReadRune()
if err != nil {
panic(err)
}
return string(answer)
}

View File

@@ -1,29 +0,0 @@
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,19 +14,18 @@ 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 {
version := ""
if len(args) != 0 {
version = args[0]
if len(args) == 0 {
return q.InstallQK8s("", opts, keepPatchFiles)
}
return q.InstallQK8s(version, opts, keepPatchFiles)
return q.InstallQK8s(args[0], 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
}

View File

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

View File

@@ -1,81 +0,0 @@
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}
err := qp.DownloadPreflight()
if err != nil {
err = fmt.Errorf("There has been an error downloading preflight: %+v", err)
log.Println(err)
return err
}
return qp.CheckDns()
},
}
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}
err := qp.DownloadPreflight()
if err != nil {
err = fmt.Errorf("There has been an error downloading preflight: %+v", err)
log.Println(err)
return err
}
return qp.CheckK8sVersion()
},
}
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}
err := qp.DownloadPreflight()
if err != nil {
err = fmt.Errorf("There has been an error downloading preflight: %+v", err)
log.Println(err)
return err
}
return qp.RunAllPreflightChecks()
},
}
return preflightAllChecksCmd
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
@@ -18,7 +19,7 @@ import (
"github.com/ttacon/chalk"
)
// To run this project in debug mode, run:
// To run this project in ddebug mode, run:
// export QLIKSENSE_DEBUG=true
// qliksense <command>
@@ -41,14 +42,18 @@ func initAndExecute() error {
// create dirs and appropriate files for setting up contexts
api.LogDebugMessage("QliksenseHomeDir: %s", qlikSenseHome)
qliksenseClient := qliksense.New(qlikSenseHome)
cmd := rootCmd(qliksenseClient)
if err := cmd.Execute(); err != nil {
//levenstein checks (auto-suggestions)
levenstein(cmd)
qliksenseClient, err := qliksense.New(qlikSenseHome)
if err != nil {
return err
}
qliksenseClient.SetUpQliksenseDefaultContext()
cmd := rootCmd(qliksenseClient)
//levenstein checks
if levenstein(cmd) == false {
if err := cmd.Execute(); err != nil {
return err
}
}
return nil
}
@@ -78,60 +83,30 @@ func setUpPaths() (string, error) {
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of qliksense cli",
Long: "Print the version number of qliksense cli",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s (%s, %s)\n", pkg.Version, pkg.Commit, pkg.CommitDate)
},
}
func commandUsesContext(commandName string) bool {
return commandName != "" && commandName != "qliksense" && commandName != "help" && commandName != "version"
}
func rootCmd(p *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
)
func getRootCmd(p *qliksense.Qliksense) *cobra.Command {
cmd := &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)
cmd.AddCommand(getInstallableVersionsCmd(p))
// For qliksense overrides/commands
cmd.AddCommand(pullQliksenseImages(p))
cmd.AddCommand(pushQliksenseImages(p))
cmd.AddCommand(about(p))
@@ -176,12 +151,14 @@ 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))
@@ -192,21 +169,40 @@ 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
@@ -235,21 +231,28 @@ func copy(src, dst string) (int64, error) {
return nBytes, err
}
func levenstein(cmd *cobra.Command) {
cmd.SuggestionsMinimumDistance = 2
func levenstein(cmd *cobra.Command) bool {
cmd.SuggestionsMinimumDistance = 4
if len(os.Args) > 1 {
args := os.Args[1]
suggest := cmd.SuggestionsFor(args)
for _, ctx := range cmd.Commands() {
val := *ctx
if args == val.Name() {
//found command
return false
}
}
suggest := cmd.SuggestionsFor(os.Args[1])
if len(suggest) > 0 {
arg := []string{}
for _, cm := range os.Args {
arg = append(arg, cm)
}
if !strings.EqualFold(arg[1], suggest[0]) {
arg[1] = suggest[0]
out := ansi.NewColorableStdout()
fmt.Fprintln(out, chalk.Green.Color("Did you mean: "), chalk.Bold.TextStyle(strings.Join(arg, " ")), "?")
}
arg[1] = suggest[0]
out := ansi.NewColorableStdout()
fmt.Fprintln(out, chalk.Green.Color("Did you mean: "), chalk.Bold.TextStyle(strings.Join(arg, " ")), "?")
return true
}
}
return false
}

View File

@@ -1,6 +1,7 @@
package main
import (
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
@@ -8,7 +9,7 @@ import (
func uninstallCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "uninstall",
Short: "Uninstall the deployed qliksense.",
Short: "Uninstall the deployed qliksense with release name [ " + qapi.NewQConfig(q.QliksenseHome).Spec.CurrentContext + " ]",
Long: `Uninstall the deployed qliksense. By default uninstall the current context`,
Example: `qliksense uninstall <context-name>`,
RunE: func(cmd *cobra.Command, args []string) error {

View File

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
# Overview
The Qlik Sense installer CLI (`qliksense`) provides an imperative interface to many of the configurations that need to be applied against the declarative structure described in [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s). This cli facilitates:
- Installation of QSEoK
- Installation of qliksense operator to manage QSEoK
- Air gapped installation of QSEoK
!!! info ""
This is a technology preview that uses Qlik modified [kustomize](https://github.com/qlik-oss/kustomize) for kubernetes manifests on [qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s) repository
!!! info ""
See QlikSense [edge releases on qliksense-k8s](https://github.com/qlik-oss/qliksense-k8s/releases) repository
## Future Direction
Operations:
- Expand preflight checks
- Backup/restore operations
- Fully support airgap installation of QSEoK
- Restore unwanted deletion of kubernetes resources

View File

@@ -1,100 +0,0 @@
##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
Creating resources to run preflight checks
deployment.apps/qnginx001 created
service/qnginx001 created
pod/qnginx001-6db5fc95c5-s9sl2 condition met
Running Preflight checks ⠇
--- PASS DNS check
--- DNS check passed
--- PASS cluster-preflight-checks
PASS
DNS check completed, cleaning up resources now
service "qnginx001" deleted
deployment.extensions "qnginx001" deleted
```
### 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
Minimum Kubernetes version supported: 1.11.0
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-13T18:08:14Z", GoVersion:"go1.13.8", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.5", GitCommit:"20c265fef0741dd71a66480e35bd69f18351daea", GitTreeState:"clean", BuildDate:"2019-10-15T19:07:57Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
Running Preflight checks ⠇
--- PASS Required Kubernetes Version
--- Good to go.
--- PASS cluster-preflight-checks
PASS
Minimum kubernetes version check completed
```
### Running all checks
Run the command shown below to execute all preflight checks.
```console
$ qliksense preflight all
Running all preflight checks
Running DNS check...
Creating resources to run preflight checks
deployment.apps/qnginx001 created
service/qnginx001 created
pod/qnginx001-6db5fc95c5-grwv2 condition met
Running Preflight checks ⠇
--- PASS DNS check
--- DNS check passed
--- PASS cluster-preflight-checks
PASS
DNS check completed, cleaning up resources now
service "qnginx001" deleted
deployment.extensions "qnginx001" deleted
Running minimum kubernetes version check...
Minimum Kubernetes version supported: 1.11.0
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.3", GitCommit:"06ad960bfd03b39c8310aaf92d1e7c12ce618213", GitTreeState:"clean", BuildDate:"2020-02-13T18:08:14Z", GoVersion:"go1.13.8", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.5", GitCommit:"20c265fef0741dd71a66480e35bd69f18351daea", GitTreeState:"clean", BuildDate:"2019-10-15T19:07:57Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
Running Preflight checks ⠧
--- PASS Required Kubernetes Version
--- Good to go.
--- PASS cluster-preflight-checks
PASS
Minimum kubernetes version check completed
Completed running all preflight checks
```

10
go.mod
View File

@@ -37,28 +37,24 @@ 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/pkg/errors v0.8.1
github.com/qlik-oss/k-apis v0.0.22
github.com/robfig/cron/v3 v3.0.1
github.com/qlik-oss/k-apis v0.0.17
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-20200311171314-f7b00557c8c4 // indirect
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // 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-20200312194400-c312e98713c2 // indirect
golang.org/x/tools v0.0.0-20200309202150-20ab64c0d93f // 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
sigs.k8s.io/kustomize/api v0.3.2
sigs.k8s.io/yaml v1.1.0
)

20
go.sum
View File

@@ -696,13 +696,9 @@ 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=
@@ -847,16 +843,14 @@ 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.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.17 h1:tOdrEe9gfb9CXq0+uowFnXIsI781qz/zgeN8xqupXYw=
github.com/qlik-oss/k-apis v0.0.17/go.mod h1:KOFzKVIdRqp47ytnHg3+9zb8fTlnrQjO6aKiwcrCJUE=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200206224201-2e697eccbad9 h1:iqeqTS4zjp6rPEaxmFB7pemA2CMjOEN5dYSXZaQ82uw=
github.com/qlik-oss/kustomize/api v0.3.3-0.20200206224201-2e697eccbad9/go.mod h1:OCt7FTrRVHj4kmR2xLJJUIqu00BTr6GeF09hSmM17Kw=
github.com/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=
@@ -1046,8 +1040,10 @@ 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-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/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/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=
@@ -1209,8 +1205,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-20200312194400-c312e98713c2 h1:6TB4+MaZlkcSsJDu+BS5yxSEuZIYhjWz+jhbSLEZylI=
golang.org/x/tools v0.0.0-20200312194400-c312e98713c2/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
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/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

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

BIN
pkg/.DS_Store vendored

Binary file not shown.

View File

@@ -22,44 +22,34 @@ 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 {
return nil, err
fmt.Println("yaml unmarshalling error ", err)
os.Exit(1)
}
qc.QliksenseHomePath = qsHome
return qc, nil
}
func NewQConfigEmpty(qsHome string) *QliksenseConfig {
return &QliksenseConfig{
QliksenseHomePath: qsHome,
}
return qc
}
// 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 qc.GetAndTransformCrObject(crFilePath)
return getCRObject(crFilePath)
}
func getUnencryptedCR() {
}
// GetCurrentCR create a QliksenseCR object for current context
@@ -68,14 +58,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}...)
@@ -86,8 +76,7 @@ func (qc *QliksenseConfig) SetCrLocation(contextName, filePath string) (*Qliksen
return nil, errors.New("cannot find the context")
}
// GetCRObject create a qliksense CR object from file
func GetCRObject(crfile string) (*QliksenseCR, error) {
func getCRObject(crfile string) (*QliksenseCR, error) {
cr := &QliksenseCR{}
err := ReadFromFile(cr, crfile)
if err != nil {
@@ -98,36 +87,11 @@ func GetCRObject(crfile string) (*QliksenseCR, error) {
return cr, nil
}
func (qc *QliksenseConfig) GetAndTransformCrObject(crfile string) (*QliksenseCR, error) {
cr, err := GetCRObject(crfile)
if err != nil {
return nil, err
}
if cr.Spec.ManifestsRoot != "" && !filepath.IsAbs(cr.Spec.ManifestsRoot) {
cr.Spec.ManifestsRoot = filepath.Join(qc.QliksenseHomePath, cr.Spec.ManifestsRoot)
}
return cr, nil
}
//CreateCRObjectFromString create a QliksenseCR from string content
func CreateCRObjectFromString(crContent string) (*QliksenseCR, error) {
if crContent == "" {
return nil, errors.New("empty string cannot qliksensecr")
}
cr := &QliksenseCR{}
err := 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 {
func (qc *QliksenseConfig) getCRFilePath(contextName string) string {
crFilePath := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = filepath.Join(qc.QliksenseHomePath, ctx.CrFile)
crFilePath = ctx.CrFile
break
}
}
@@ -152,7 +116,7 @@ func (qc *QliksenseConfig) BuildRepoPath(version string) string {
}
func (qc *QliksenseConfig) BuildRepoPathForContext(contextName, version string) string {
return filepath.Join(qc.GetContextPath(contextName), "qlik-k8s", version)
return filepath.Join(qc.QliksenseHomePath, qliksenseContextsDirName, contextName, "qlik-k8s", version)
}
func (qc *QliksenseConfig) BuildCurrentManifestsRoot(version string) string {
@@ -160,59 +124,13 @@ func (qc *QliksenseConfig) BuildCurrentManifestsRoot(version string) string {
}
func (qc *QliksenseConfig) WriteCR(cr *QliksenseCR, contextName string) error {
crf := qc.GetCRFilePath(contextName)
crf := qc.getCRFilePath(contextName)
if crf == "" {
return errors.New("context name " + contextName + " not found")
}
return qc.TransformAndWriteCr(cr, crf)
return WriteToFile(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: 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)
}
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)
}
@@ -303,15 +221,6 @@ 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") != "" {
@@ -320,34 +229,18 @@ func (qc *QliksenseConfig) getContextEncryptionKeyPairLocation(contextName strin
} else {
// QLIKSENSE_KEY_LOCATION has not been set, hence storing key pair in default location:
// /.qliksense/secrets/contexts/<current-context>/secrets/
secretKeyPairLocation = filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, contextName, qliksenseSecretsDirName)
if qcr, err := qc.GetCurrentCR(); err != nil {
return "", err
} else {
secretKeyPairLocation = filepath.Join(qc.QliksenseHomePath, qliksenseSecretsDirName, qliksenseContextsDirName, qcr.GetObjectMeta().GetName(), 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) {
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)
secretKeyPairLocation, err := qc.getCurrentContextEncryptionKeyPairLocation()
if err != nil {
return nil, nil, err
}
@@ -436,10 +329,6 @@ 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{}
@@ -472,45 +361,3 @@ 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("contexts", "contx1", "contx1.yaml")
crFile := filepath.Join(dir, "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("contexts", "contx1", "contx1.yaml")
crFile := filepath.Join(dir, "contexts", "contx1", "contx1.yaml")
qct, e := qc.SetCrLocation("contx1", crFile)
if e != nil {
t.Fail()

View File

@@ -3,10 +3,8 @@ package api
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"github.com/qlik-oss/k-apis/pkg/config"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -105,22 +103,14 @@ func ReadFromFile(content interface{}, sourceFile string) error {
if content == nil || sourceFile == "" {
return nil
}
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)
contents, err := ioutil.ReadFile(sourceFile)
if err != nil {
err = fmt.Errorf("There was an error reading from reader: %v", err)
err = fmt.Errorf("There was an error reading from file: %s, %v", sourceFile, err)
return err
}
// reading k8s style object
// https://stackoverflow.com/questions/44306554/how-to-deserialize-kubernetes-yaml-file
dec := machine_yaml.NewYAMLOrJSONDecoder(bytes.NewReader(contents), 10000)
return dec.Decode(content)
dec.Decode(content)
return nil
}

View File

@@ -97,21 +97,3 @@ func kubectlOperation(manifests string, oprName string, namespace string) error
os.Remove(tempYaml.Name())
return nil
}
func KubectlDirectOps(opr []string, namespace string) error {
arguments := []string{}
if namespace != "" {
arguments = append(arguments, "-n", namespace)
}
arguments = append(arguments, opr...)
cmd := exec.Command("kubectl", arguments...)
LogDebugMessage("Kubectl command: %s %v\n", "kubectl", arguments)
sterrBuffer := &bytes.Buffer{}
cmd.Stderr = sterrBuffer
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("kubectl %v failed with: %v, %v\n", opr, err, sterrBuffer.String())
}
return nil
}

View File

@@ -1,8 +1,6 @@
package api
import (
"fmt"
"strings"
"testing"
)
@@ -17,16 +15,3 @@ 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,21 +1,13 @@
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 {
@@ -88,6 +80,31 @@ func ProcessConfigArgs(args []string) ([]*ServiceKeyValue, error) {
return resultSvcKV, nil
}
// ProcessUnsetConfigArgs processes args and returns an service, key, nil slice
func ProcessUnsetConfigArgs(args []string) ([]*ServiceKeyValue, error) {
if len(args) == 0 {
err := fmt.Errorf("No args were provided. Please provide args to configure the current context")
return nil, err
}
resultSvcKV := make([]*ServiceKeyValue, len(args))
re1 := regexp.MustCompile(`(\w{1,}).(\w{1,})`)
for i, arg := range args {
LogDebugMessage("Arg received: %s", arg)
result := re1.FindStringSubmatch(arg)
// check if result array's length is == 3 (index 0 - is the full match & indices 1,2,- are the fields we need)
if len(result) != 3 {
err := fmt.Errorf("Please provide valid args for this command")
return nil, err
}
resultSvcKV[i] = &ServiceKeyValue{
SvcName: result[1],
Key: result[2],
Value: "",
}
}
return resultSvcKV, nil
}
func ExecuteTaskWithBlinkingStdoutFeedback(task func() (interface{}, error), feedback string) (result interface{}, err error) {
taskDone := make(chan bool)
go func() {
@@ -117,151 +134,3 @@ 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

@@ -1,16 +0,0 @@
package preflight
import (
"fmt"
)
func (qp *QliksensePreflight) RunAllPreflightChecks() error {
//run all preflight checks
fmt.Println("Running all preflight checks")
fmt.Printf("\nRunning DNS check...\n")
qp.CheckDns()
fmt.Printf("\nRunning minimum kubernetes version check...\n")
qp.CheckK8sVersion()
fmt.Println("Completed running all preflight checks")
return nil
}

View File

@@ -1,128 +0,0 @@
package preflight
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"path/filepath"
"github.com/qlik-oss/sense-installer/pkg/api"
)
const dnsCheckYAML = `
apiVersion: troubleshoot.replicated.com/v1beta1
kind: Preflight
metadata:
name: cluster-preflight-checks
namespace: {{ . }}
spec:
collectors:
- run:
collectorName: spin-up-pod
args: ["-z", "-v", "-w 1", "qnginx001", "80"]
command: ["nc"]
image: subfuzion/netcat:latest
imagePullPolicy: IfNotPresent
name: spin-up-pod-check-dns
namespace: {{ . }}
timeout: 30s
analyzers:
- textAnalyze:
checkName: DNS check
collectorName: spin-up-pod-check-dns
fileName: spin-up-pod.txt
regex: succeeded
outcomes:
- fail:
message: DNS check failed
- pass:
message: DNS check passed
`
func (qp *QliksensePreflight) CheckDns() error {
// retrieve namespace
namespace := api.GetKubectlNamespace()
api.LogDebugMessage("Namespace: %s\n", namespace)
tmpl, err := template.New("dnsCheckYAML").Parse(dnsCheckYAML)
if err != nil {
fmt.Printf("cannot parse template: %v", err)
return err
}
tempYaml, err := ioutil.TempFile("", "")
if err != nil {
fmt.Printf("cannot create file: %v", err)
return err
}
api.LogDebugMessage("Temp Yaml file: %s\n", tempYaml.Name())
b := bytes.Buffer{}
err = tmpl.Execute(&b, namespace)
if err != nil {
fmt.Println(err)
return err
}
tempYaml.WriteString(b.String())
// creating Kubectl resources
appName := "qnginx001"
const PreflightChecksDirName = "preflight_checks"
fmt.Println("Creating resources to run preflight checks")
// kubectl create deployment
opr := fmt.Sprintf("create deployment %s --image=nginx", appName)
err = initiateK8sOps(opr, namespace)
if err != nil {
fmt.Println(err)
return err
}
defer func() {
// Deleting deployment..
opr = fmt.Sprintf("delete deployment %s", appName)
// we want to delete the k8s resource here, we dont care a lot about an error here
_ = initiateK8sOps(opr, namespace)
api.LogDebugMessage("delete deployment executed")
}()
// create service
opr = fmt.Sprintf("create service clusterip %s --tcp=80:80", appName)
err = initiateK8sOps(opr, namespace)
if err != nil {
fmt.Println(err)
return err
}
defer func() {
// delete service
opr = fmt.Sprintf("delete service %s", appName)
// we want to delete the k8s resource here, we dont care a lot about an error here
_ = initiateK8sOps(opr, namespace)
api.LogDebugMessage("delete service executed")
}()
//kubectl -n $namespace wait --for=condition=ready pod -l app=$appName --timeout=120s
opr = fmt.Sprintf("wait --for=condition=ready pod -l app=%s --timeout=120s", appName)
err = initiateK8sOps(opr, namespace)
if err != nil {
fmt.Println(err)
return err
}
api.LogDebugMessage("kubectl wait executed")
// call preflight
preflightCommand := filepath.Join(qp.Q.QliksenseHome, PreflightChecksDirName, preflightFileName)
err = invokePreflight(preflightCommand, tempYaml)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println("DNS check completed, cleaning up resources now")
return nil
}

View File

@@ -1,187 +0,0 @@
package preflight
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/qlik-oss/sense-installer/pkg/api"
)
type QliksensePreflight struct {
Q *qliksense.Qliksense
}
const (
// preflight releases have the same version
preflightRelease = "v0.9.26"
preflightLinuxFile = "preflight_linux_amd64.tar.gz"
preflightMacFile = "preflight_darwin_amd64.tar.gz"
preflightWindowsFile = "preflight_windows_amd64.zip"
PreflightChecksDirName = "preflight_checks"
preflightFileName = "preflight"
)
var preflightBaseURL = fmt.Sprintf("https://github.com/replicatedhq/troubleshoot/releases/download/%s/", preflightRelease)
func (qp *QliksensePreflight) DownloadPreflight() error {
const preflightExecutable = "preflight"
preflightInstallDir := filepath.Join(qp.Q.QliksenseHome, PreflightChecksDirName)
platform := runtime.GOOS
exists, err := checkInstalled(preflightInstallDir, preflightExecutable)
if err != nil {
err = fmt.Errorf("There has been an error when trying to determine if preflight installer exists")
log.Println(err)
return err
}
if exists {
// preflight exist, no need to download again.
api.LogDebugMessage("Preflight already exists, proceeding to perform checks")
return nil
}
// Create the Preflight-check directory, download and install preflight
if !api.DirExists(preflightInstallDir) {
api.LogDebugMessage("%s does not exist, creating now\n", preflightInstallDir)
if err := os.Mkdir(preflightInstallDir, os.ModePerm); err != nil {
err = fmt.Errorf("Not able to create %s dir: %v", preflightInstallDir, err)
log.Println(err)
return nil
}
}
api.LogDebugMessage("Preflight-checks install Dir: %s exists", preflightInstallDir)
preflightUrl, preflightFile, err := determinePlatformSpecificUrls(platform)
if err != nil {
err = fmt.Errorf("There was an error when trying to determine platform specific paths")
return err
}
// Download Preflight
err = downloadAndExplode(preflightUrl, preflightInstallDir, preflightFile)
if err != nil {
return err
}
fmt.Println("Downloaded Preflight")
return nil
}
func checkInstalled(preflightInstallDir, preflightExecutable string) (bool, error) {
installerExists := true
preflightInstaller := filepath.Join(preflightInstallDir, preflightExecutable)
if api.DirExists(preflightInstallDir) {
if !api.FileExists(preflightInstaller) {
installerExists = false
api.LogDebugMessage("Preflight install directory exists, but preflight installer does not exist")
}
} else {
installerExists = false
}
return installerExists, nil
}
func downloadAndExplode(url, installDir, file string) error {
err := api.DownloadFile(url, installDir, file)
if err != nil {
return err
}
api.LogDebugMessage("Downloaded File: %s", file)
fileToUntar := filepath.Join(installDir, file)
api.LogDebugMessage("File to explode: %s", file)
err = api.ExplodePackage(installDir, fileToUntar)
if err != nil {
return err
}
return nil
}
func determinePlatformSpecificUrls(platform string) (string, string, error) {
var preflightUrl, preflightFile string
if runtime.GOARCH != `amd64` {
err := fmt.Errorf("%s architecture is not supported", runtime.GOARCH)
return "", "", err
}
switch platform {
case "windows":
preflightFile = preflightWindowsFile
case "darwin":
preflightFile = preflightMacFile
case "linux":
preflightFile = preflightLinuxFile
default:
err := fmt.Errorf("Unable to download the preflight executable for the underlying platform\n")
return "", "", err
}
preflightUrl = fmt.Sprintf("%s%s", preflightBaseURL, preflightFile)
return preflightUrl, preflightFile, 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 invokePreflight(preflightCommand string, yamlFile *os.File) error {
arguments := []string{}
arguments = append(arguments, yamlFile.Name(), "--interactive=false")
cmd := exec.Command(preflightCommand, arguments...)
sterrBuffer := &bytes.Buffer{}
cmd.Stdout = sterrBuffer
cmd.Stderr = sterrBuffer
if err := cmd.Run(); err != nil {
return fmt.Errorf("Error when running preflight command: %v\n", err)
}
ind := strings.Index(sterrBuffer.String(), "---")
output := sterrBuffer.String()
if ind > -1 {
output = fmt.Sprintf("%s\n%s", output[:ind], output[ind:])
}
fmt.Printf("%v\n", output)
// Maybe good to retain this part in case we need to process the output in future.
// We are going to look for the first occurance of PASS or FAIL from the end
// there are also some space-like deceiving characters which are being hard to get by
//outputArr := strings.Fields(strings.TrimSpace(output))
//trackSuccess := false
//trackPrg := false
//for i := len(outputArr) - 1; i >= 0; i-- {
// if strings.TrimSpace(outputArr[i]) != "" {
// if outputArr[i] == "PASS" {
// trackSuccess = true
// trackPrg = true
// } else if outputArr[i] == "FAIL" {
// trackPrg = true
// }
// }
// if trackPrg {
// break
// }
//}
return nil
}

View File

@@ -1,90 +0,0 @@
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: "ash-ns",
},
wantErr: false,
},
{
name: "invalid case",
args: args{
opr: fmt.Sprintf("versions"),
namespace: "ash-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)
}
})
}
}
func Test_determinePlatformSpecificUrls(t *testing.T) {
type args struct {
platform string
}
tests := []struct {
name string
args args
want string
want1 string
wantErr bool
}{
{
name: "valid platform",
args: args{
platform: "windows",
},
want: fmt.Sprintf("%s%s", preflightBaseURL, preflightWindowsFile),
want1: preflightWindowsFile,
wantErr: false,
},
{
name: "invalid platform",
args: args{
platform: "unix",
},
want: "",
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := determinePlatformSpecificUrls(tt.args.platform)
if (err != nil) != tt.wantErr {
t.Errorf("determinePlatformSpecificUrls() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("determinePlatformSpecificUrls() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("determinePlatformSpecificUrls() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@@ -1,84 +0,0 @@
package preflight
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"text/template"
"github.com/qlik-oss/sense-installer/pkg/api"
)
const minK8sVersion = "1.11.0"
const checkVersionYAML = `
apiVersion: troubleshoot.replicated.com/v1beta1
kind: Preflight
metadata:
name: cluster-preflight-checks
namespace: {{ .namespace }}
spec:
analyzers:
- clusterVersion:
outcomes:
- fail:
when: "< {{ .minK8sVersion }}"
message: The application requires at least Kubernetes {{ .minK8sVersion }} or later.
uri: https://www.kubernetes.io
- pass:
when: ">= {{ .minK8sVersion }}"
message: Good to go.
`
func (qp *QliksensePreflight) CheckK8sVersion() error {
// retrieve namespace
namespace := api.GetKubectlNamespace()
api.LogDebugMessage("Namespace: %s\n", namespace)
tmpl, err := template.New("checkVersionYAML").Parse(checkVersionYAML)
if err != nil {
fmt.Printf("cannot parse template: %v", err)
return err
}
tempYaml, err := ioutil.TempFile("", "")
if err != nil {
fmt.Printf("cannot create file: %v", err)
return err
}
api.LogDebugMessage("Temp Yaml file: %s\n", tempYaml.Name())
b := bytes.Buffer{}
err = tmpl.Execute(&b, map[string]string{
"namespace": namespace,
"minK8sVersion": minK8sVersion,
})
if err != nil {
fmt.Println(err)
return err
}
tempYaml.WriteString(b.String())
//api.LogDebugMessage("Temp yaml contents: %s", b.String())
fmt.Printf("Minimum Kubernetes version supported: %s\n", minK8sVersion)
// current kubectl version
opr := fmt.Sprintf("version")
err = initiateK8sOps(opr, namespace)
if err != nil {
fmt.Println(err)
return err
}
// call preflight
preflightCommand := filepath.Join(qp.Q.QliksenseHome, PreflightChecksDirName, preflightFileName)
err = invokePreflight(preflightCommand, tempYaml)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println("Minimum kubernetes version check completed")
return nil
}

Binary file not shown.

View File

@@ -1,22 +0,0 @@
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 err := q.InstallQK8s(cr.GetLabelFromCr("version"), opts, keepPatchFiles); err != nil {
return err
}
return nil
}

View File

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

View File

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

View File

@@ -10,8 +10,6 @@ import (
"strings"
"text/tabwriter"
"github.com/robfig/cron/v3"
"github.com/qlik-oss/k-apis/pkg/config"
b64 "encoding/base64"
@@ -149,9 +147,31 @@ func (q *Qliksense) SetConfigs(args []string) error {
return nil
}
// SetConfigs - set-configs <key>=<value> commands
func (q *Qliksense) UnsetConfigs(args []string) error {
// retieve current context from config.yaml
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
if err != nil {
return err
}
resultArgs, err := api.ProcessUnsetConfigArgs(args)
if err != nil {
return err
}
for _, ra := range resultArgs {
qliksenseCR.Spec.AddToConfigs(ra.SvcName, ra.Key, ra.Value)
}
// write modified content into context.yaml
api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
return nil
}
func retrieveCurrentContextInfo(q *Qliksense) (*api.QliksenseCR, string, error) {
var qliksenseConfig api.QliksenseConfig
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
if err := api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return nil, "", err
@@ -227,42 +247,51 @@ func (q *Qliksense) SetOtherConfigs(args []string) error {
}
qliksenseCR.Spec.RotateKeys = rotateKeys
api.LogDebugMessage("Current rotateKeys after modification: %s ", qliksenseCR.Spec.RotateKeys)
case "gitops.enabled":
if qliksenseCR.Spec.GitOps == nil {
qliksenseCR.Spec.GitOps = &config.GitOps{}
}
if strings.EqualFold(argsString[1], "yes") || strings.EqualFold(argsString[1], "no") {
qliksenseCR.Spec.GitOps.Enabled = argsString[1]
api.LogDebugMessage("Current gitOps enabled status : %s ", qliksenseCR.Spec.GitOps.Enabled)
} else {
err := fmt.Errorf("Please use yes or no")
log.Println(err)
return err
}
case "gitops.schedule":
if qliksenseCR.Spec.GitOps == nil {
qliksenseCR.Spec.GitOps = &config.GitOps{}
}
if _, err := cron.ParseStandard(argsString[1]); err != nil {
err := fmt.Errorf("Please enter string with standard cron scheduling syntax ")
return err
}
qliksenseCR.Spec.GitOps.Schedule = argsString[1]
api.LogDebugMessage("Current gitOps schedule is : %s ", qliksenseCR.Spec.GitOps.Schedule)
case "gitops.watchbranch":
if qliksenseCR.Spec.GitOps == nil {
qliksenseCR.Spec.GitOps = &config.GitOps{}
}
qliksenseCR.Spec.GitOps.WatchBranch = argsString[1]
api.LogDebugMessage("Current gitOps watchbranch is : %s ", qliksenseCR.Spec.GitOps.WatchBranch)
case "gitops.image":
if qliksenseCR.Spec.GitOps == nil {
qliksenseCR.Spec.GitOps = &config.GitOps{}
}
qliksenseCR.Spec.GitOps.Image = argsString[1]
api.LogDebugMessage("Current gitOps image is : %s ", qliksenseCR.Spec.GitOps.Image)
default:
err := fmt.Errorf("Please enter one of: profile, storageClassName,rotateKeys, manifestRoot, git.repository or gitops arguments to configure the current context")
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
}
}
// write modified content into context.yaml
api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
return nil
}
// SetOtherConfigs - set profile/storageclassname/git.repository/manifestRoot commands
func (q *Qliksense) UnsetOtherConfigs(args []string) error {
// retieve current context from config.yaml
qliksenseCR, qliksenseContextsFile, err := retrieveCurrentContextInfo(q)
if err != nil {
return err
}
// modify appropriate fields
if len(args) == 0 {
err := fmt.Errorf("No args were provided. Please provide args to configure the current context")
log.Println(err)
return err
}
for _, arg := range args {
switch arg {
case "profile":
qliksenseCR.Spec.Profile = ""
api.LogDebugMessage("Current profile after modification: %s ", qliksenseCR.Spec.Profile)
case "git.repository":
qliksenseCR.Spec.Git.Repository = ""
api.LogDebugMessage("Current git repository after modification: %s ", qliksenseCR.Spec.Git.Repository)
case "storageClassName":
qliksenseCR.Spec.StorageClassName = ""
api.LogDebugMessage("Current StorageClassName after modification: %s ", qliksenseCR.Spec.StorageClassName)
case "manifestsRoot":
qliksenseCR.Spec.ManifestsRoot = ""
case "rotateKeys":
qliksenseCR.Spec.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
}
@@ -276,7 +305,7 @@ func (q *Qliksense) SetOtherConfigs(args []string) error {
// 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])
err := q.SetUpQliksenseContext(args[0], false)
if err != nil {
return err
}
@@ -301,7 +330,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", qliksenseConfig.GetCRFilePath(cont.Name), "\t")
fmt.Fprintln(w, cont.Name, "\t", cont.CrFile, "\t")
}
w.Flush()
fmt.Fprintln(out, "")
@@ -312,7 +341,7 @@ func (q *Qliksense) ListContextConfigs() error {
return nil
}
func (q *Qliksense) DeleteContextConfig(args []string, flag bool) error {
func (q *Qliksense) DeleteContextConfig(args []string) error {
if len(args) == 1 {
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
var qliksenseConfig api.QliksenseConfig
@@ -320,12 +349,10 @@ func (q *Qliksense) DeleteContextConfig(args []string, flag bool) error {
out := ansi.NewColorableStdout()
switch args[0] {
case qliksenseConfig.Spec.CurrentContext:
fmt.Fprintln(out, chalk.Red.Color("Error: Cannot delete current context -"), chalk.Bold.TextStyle(qliksenseConfig.Spec.CurrentContext))
fmt.Fprintln(out, chalk.Yellow.Color("Please switch contexts to be able to delete this context."))
err := fmt.Errorf(chalk.Red.Color("Cannot delete current context - %s"), chalk.White.Color(chalk.Bold.TextStyle(qliksenseConfig.Spec.CurrentContext)))
return err
case DefaultQliksenseContext:
err := fmt.Errorf(chalk.Red.Color("Cannot delete default qliksense context"))
return err
fmt.Fprintln(out, chalk.Red.Color("Error: Cannot delete default qliksense context"))
default:
qliksenseContextsDir1 := filepath.Join(q.QliksenseHome, QliksenseContextsDir)
qliksenseContextFile := filepath.Join(qliksenseContextsDir1, args[0])
@@ -354,20 +381,11 @@ func (q *Qliksense) DeleteContextConfig(args []string, flag bool) error {
}
newLength := len(qliksenseConfig.Spec.Contexts)
if currentLength != newLength {
ans := flag
if ans == false {
ans = AskForConfirmation("Are You Sure? ")
}
if ans == true {
api.WriteToFile(&qliksenseConfig, qliksenseConfigFile)
fmt.Fprintln(out, chalk.Yellow.Color(chalk.Underline.TextStyle("Warning: Active resources may still be running in-cluster")))
fmt.Fprintln(out, chalk.Green.Color("Successfully deleted context: "), chalk.Bold.TextStyle(args[0]))
} else {
return nil
}
api.WriteToFile(&qliksenseConfig, qliksenseConfigFile)
fmt.Fprintln(out, chalk.Yellow.Color(chalk.Underline.TextStyle("Warning: Active resources may still be running in-cluster")))
fmt.Fprintln(out, chalk.Green.Color("Successfully deleted context: "), chalk.Bold.TextStyle(args[0]))
} else {
err := fmt.Errorf(chalk.Red.Color("Context not found"))
return err
fmt.Fprintln(out, chalk.Red.Color("Error: Context not found"))
}
}
}
@@ -382,11 +400,11 @@ func (q *Qliksense) DeleteContextConfig(args []string, flag bool) error {
// SetUpQliksenseDefaultContext - to setup dir structure for default qliksense context
func (q *Qliksense) SetUpQliksenseDefaultContext() error {
return q.SetUpQliksenseContext(DefaultQliksenseContext)
return q.SetUpQliksenseContext(DefaultQliksenseContext, true)
}
// SetUpQliksenseContext - to setup qliksense context
func (q *Qliksense) SetUpQliksenseContext(contextName string) error {
func (q *Qliksense) SetUpQliksenseContext(contextName string, isDefaultContext bool) error {
if contextName == "" {
err := fmt.Errorf("Please enter a non-empty context-name")
log.Println(err)
@@ -400,29 +418,83 @@ func (q *Qliksense) SetUpQliksenseContext(contextName string) error {
}
qliksenseConfigFile := filepath.Join(q.QliksenseHome, QliksenseConfigFile)
qliksenseConfig := api.NewQConfigEmpty(q.QliksenseHome)
var qliksenseConfig api.QliksenseConfig
configFileTrack := false
if !api.FileExists(qliksenseConfigFile) {
qliksenseConfig.AddBaseQliksenseConfigs(contextName)
} else {
if err := api.ReadFromFile(qliksenseConfig, qliksenseConfigFile); err != nil {
if err := api.ReadFromFile(&qliksenseConfig, qliksenseConfigFile); err != nil {
log.Println(err)
return err
}
if isDefaultContext { // if config file exits but a default context is requested, we want to prevent writing to config file
configFileTrack = true
}
}
// creating a file in the name of the context if it does not exist/ opening it to append/modify content if it already exists
qliksenseContextsDir1 := filepath.Join(q.QliksenseHome, QliksenseContextsDir)
if !api.DirExists(qliksenseContextsDir1) {
if err := os.Mkdir(qliksenseContextsDir1, os.ModePerm); err != nil {
err = fmt.Errorf("Not able to create %s dir: %v", qliksenseContextsDir1, err)
log.Println(err)
return err
}
}
api.LogDebugMessage("%s exists", qliksenseContextsDir1)
if qliksenseConfig.IsContextExist(contextName) {
return nil
// 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,
})
}
qliksenseCR := &api.QliksenseCR{}
qliksenseCR.AddCommonConfig(contextName)
qliksenseConfig.Spec.CurrentContext = contextName
if err := qliksenseConfig.CreateOrWriteCrAndContext(qliksenseCR); err != nil {
return err
if !configFileTrack {
api.WriteToFile(&qliksenseConfig, qliksenseConfigFile)
}
// set the encrypted default mongo
return q.SetSecrets([]string{`qliksense.mongoDbUri="mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"`}, false)
q.SetSecrets([]string{`qliksense.mongoDbUri="mongodb://qlik-default-mongodb:27017/qliksense?ssl=false"`}, false)
return nil
}
func validateInput(input string) (string, error) {
@@ -533,16 +605,3 @@ func (q *Qliksense) SetImageRegistry(registry, pushUsername, pushPassword, pullU
qliksenseCR.Spec.AddToConfigs("qliksense", imageRegistryConfigKey, registry)
return api.WriteToFile(&qliksenseCR, qliksenseContextsFile)
}
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,7 +152,6 @@ 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)
}
@@ -165,7 +164,7 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
crFile: /root/.qliksense/contexts/qlik-default.yaml
currentContext: qlik-default
`
configFile := filepath.Join(testDir, "config.yaml")
@@ -274,8 +273,12 @@ func TestSetUpQliksenseContext(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
if err := q.SetUpQliksenseContext(tt.args.contextName); (err != nil) != tt.wantErr {
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 {
t.Errorf("SetUpQliksenseContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
@@ -303,7 +306,11 @@ func TestSetUpQliksenseDefaultContext(t *testing.T) {
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
q, err := New(tt.args.qlikSenseHome)
if err != nil {
t.Errorf("unable to create a qliksense instance")
return
}
if err := q.SetUpQliksenseDefaultContext(); (err != nil) != tt.wantErr {
t.Errorf("SetUpQliksenseDefaultContext() error = %v, wantErr %v", err, tt.wantErr)
}
@@ -327,7 +334,7 @@ func TestSetOtherConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"profile=minikube", "rotateKeys=yes", "storageClassName=efs", "gitops.enabled=yes", "gitops.schedule=30 * * * *"},
args: []string{"profile=minikube", "rotateKeys=yes", "storageClassName=efs"},
},
wantErr: false,
},
@@ -337,7 +344,7 @@ func TestSetOtherConfigs(t *testing.T) {
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"someconfig=somevalue, gitops.schedule=bar", "gitops.enabled=bar"},
args: []string{"someconfig=somevalue"},
},
wantErr: true,
},
@@ -363,6 +370,60 @@ func TestSetOtherConfigs(t *testing.T) {
}
}
func TestUnsetOtherConfigs(t *testing.T) {
type args struct {
q *Qliksense
args []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"profile", "rotateKeys", "storageClassName"},
},
wantErr: false,
},
{
name: "invalid configs",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"someconfig"},
},
wantErr: true,
},
{
name: "empty configs",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{},
},
wantErr: true,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//set arguments to be removed
_ = tt.args.q.SetOtherConfigs([]string{"profile=minikube", "rotateKeys=yes", "storageClassName=efs"})
if err := tt.args.q.UnsetOtherConfigs(tt.args.args); (err != nil) != tt.wantErr {
t.Errorf("SetOtherConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetConfigs(t *testing.T) {
type args struct {
q *Qliksense
@@ -395,9 +456,43 @@ func TestSetConfigs(t *testing.T) {
}
}
func TestUnsetConfigs(t *testing.T) {
type args struct {
q *Qliksense
args []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid case",
args: args{
q: &Qliksense{
QliksenseHome: testDir,
},
args: []string{"qliksense.acceptEULA", "qliksense.mongoDbUri"},
},
wantErr: false,
},
}
tearDown := setup()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//set arguments to be removed
_ = tt.args.q.SetConfigs([]string{"qliksense.acceptEULA=\"yes\"", "qliksense.mongoDbUri=\"mongo://mongo:3307\""})
if err := tt.args.q.UnsetConfigs(tt.args.args); (err != nil) != tt.wantErr {
t.Errorf("SetConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSetImageRegistry(t *testing.T) {
getQlikSense := func(tmpQlikSenseHome string) (*Qliksense, error) {
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(`
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(fmt.Sprintf(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
@@ -405,9 +500,9 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
crFile: %s/contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`), os.ModePerm); err != nil {
`, tmpQlikSenseHome)), os.ModePerm); err != nil {
return nil, err
}
@@ -785,154 +880,3 @@ func getValueToBeDecodedForSetSecrets(item config.NameValue, qliksenseCR *api.Ql
err := fmt.Errorf("Both Value and ValueFrom are empty")
return "", err
}
func setupDeleteContext() func() {
if err := os.Mkdir(testDir, 0777); err != nil {
log.Printf("\nError occurred: %v", err)
}
config :=
`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: qliksenseConfig
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default.yaml
- name: qlik1
crFile: contexts/qlik1.yaml
- name: qlik2
crFile: contexts/qlik2.yaml
currentContext: qlik1
`
configFile := filepath.Join(testDir, "config.yaml")
// tests/config.yaml exists
ioutil.WriteFile(configFile, []byte(config), 0777)
contextYaml :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik-default
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik-default
`
qlikDefaultContext := "qlik-default"
// create contexts/qlik-default/ under tests/
contexts := "contexts"
contextsDir1 := filepath.Join(testDir, contexts, qlikDefaultContext)
if err := os.MkdirAll(contextsDir1, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextYaml1 :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik1
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik1`
contextYaml2 :=
`
apiVersion: qlik.com/v1
kind: Qliksense
metadata:
name: qlik2
spec:
profile: docker-desktop
rotateKeys: "yes"
releaseName: qlik2`
contextsDir := filepath.Join(testDir, contexts, "qlik1")
if err := os.MkdirAll(contextsDir, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextsDir2 := filepath.Join(testDir, contexts, "qlik2")
if err := os.MkdirAll(contextsDir2, 0777); err != nil {
err = fmt.Errorf("Not able to create directories")
log.Fatal(err)
}
contextFile := filepath.Join(contextsDir, "qlik1.yaml")
ioutil.WriteFile(contextFile, []byte(contextYaml1), 0777)
contextFile2 := filepath.Join(contextsDir2, "qlik2.yaml")
ioutil.WriteFile(contextFile2, []byte(contextYaml2), 0777)
contextFile1 := filepath.Join(contextsDir1, "qlik-default.yaml")
ioutil.WriteFile(contextFile1, []byte(contextYaml), 0777)
tearDown := func() {
os.RemoveAll(testDir)
}
return tearDown
}
func TestDeleteContexts(t *testing.T) {
type args struct {
qlikSenseHome string
contextName string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik2",
},
wantErr: false,
},
{
name: "default context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik-default",
},
wantErr: true,
},
{
name: "non-existent context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik3",
},
wantErr: true,
},
{
name: "current context",
args: args{
qlikSenseHome: testDir,
contextName: "qlik1",
},
wantErr: true,
},
}
tearDown := setupDeleteContext()
defer tearDown()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := New(tt.args.qlikSenseHome)
var arg []string
arg = append(arg, tt.args.contextName)
if err := q.DeleteContextConfig(arg, true); (err != nil) != tt.wantErr {
t.Errorf("DeleteContext() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

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(`
if err := ioutil.WriteFile(path.Join(tmpQlikSenseHome, "config.yaml"), []byte(fmt.Sprintf(`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
@@ -179,9 +179,9 @@ metadata:
spec:
contexts:
- name: qlik-default
crFile: contexts/qlik-default/qlik-default.yaml
crFile: %s/contexts/qlik-default/qlik-default.yaml
currentContext: qlik-default
`), os.ModePerm); err != nil {
`, tmpQlikSenseHome)), os.ModePerm); err != nil {
return err
}

View File

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

@@ -1,88 +0,0 @@
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,6 +12,7 @@ import (
)
type InstallCommandOptions struct {
AcceptEULA string
StorageClass string
MongoDbUri string
RotateKeys string
@@ -40,7 +41,9 @@ func (q *Qliksense) InstallQK8s(version string, opts *InstallCommandOptions, kee
return err
}
qcr.SetEULA("yes")
if opts.AcceptEULA != "" {
qcr.Spec.AddToConfigs("qliksense", "acceptEULA", opts.AcceptEULA)
}
if opts.MongoDbUri != "" {
qcr.Spec.AddToSecrets("qliksense", "mongoDbUri", opts.MongoDbUri, "")
}

View File

@@ -1,65 +0,0 @@
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,7 +6,6 @@ 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"

View File

@@ -1,81 +0,0 @@
package qliksense
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
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
}
}
}
}
// write to disk
if err = qConfig.CreateOrWriteCrAndContext(cr); err != nil {
return "", err
}
qConfig.SetCurrentContextName(cr.GetName())
qConfig.Write()
return cr.GetName(), nil
}

View File

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

View File

@@ -28,9 +28,7 @@ func (q *Qliksense) UpgradeQK8s(keepPatchFiles bool) error {
return err
}
qcr.Spec.RotateKeys = "no"
if dcr, err := qConfig.GetDecryptedCr(qcr); err != nil {
return err
} else if err := q.applyConfigToK8s(dcr); err != nil {
if err := q.applyConfigToK8s(qcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
}