Compare commits

..

11 Commits

Author SHA1 Message Date
Sanat Nayar
e26ca77b05 linked to custom-cr 2020-02-04 14:57:12 -05:00
Andriy Bulynko
644498ddb8 upgrading k-apis to v0.0.2 2020-02-04 13:39:08 -05:00
Andriy Bulynko
bcf2b1ab4b Using k-apis and underlying go-git for the about command (#56) 2020-02-04 12:16:58 -05:00
Foysal Iqbal
0545fd7d16 Config view apply (#55) 2020-02-04 10:03:09 -05:00
Foysal Iqbal
ea240ce3f1 Clean porter (#53) 2020-02-03 12:01:38 -05:00
Andriy Bulynko
d6a16cea8b About command (#50) 2020-02-03 10:07:25 -05:00
Foysal Iqbal
ee557c2068 bring static files into cli (#49) 2020-01-30 15:37:17 -05:00
Andriy Bulynko
fb14d30328 kustomize API in-process (#48) 2020-01-30 13:05:45 -05:00
Boris Kuschel
ca83942fbe Pull defaults for no version and no directory 2020-01-30 06:48:16 -05:00
Foysal Iqbal
7f68dad586 About doc (#46) 2020-01-29 13:37:42 -05:00
Boris Kuschel
fdc2877174 Make pull/push Docker engineless (#42) 2020-01-28 18:05:49 -05:00
36 changed files with 3053 additions and 2678 deletions

View File

@@ -9,6 +9,7 @@ jobs:
working_directory: /go/src/github.com/qlik-oss/sense-installer
steps:
- checkout
- run: make test
- run: make build
build_release:
docker:
@@ -16,6 +17,7 @@ jobs:
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"
@@ -28,21 +30,14 @@ workflows:
jobs:
- build:
filters:
branches:
only: master
tags:
ignore: /^v.*/
build_release:
jobs:
- build:
filters:
branches:
ignore: /.*/
tags:
only: /v.*/
- build_release:
requires:
- build
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
tags:
only: /v.*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,13 +4,14 @@ PKG = github.com/qlik-oss/sense-installer
MAKE_OPTS ?= --no-print-directory
LDFLAGS = -w -X $(PKG)/pkg.Version=$(VERSION) -X $(PKG)/pkg.Commit=$(COMMIT) -X "$(PKG)/pkg.CommitDate=$(COMMIT_DATE)"
XBUILD = CGO_ENABLED=0 go build -a -tags netgo -ldflags '$(LDFLAGS)'
XBUILD = CGO_ENABLED=0 go build -a -tags "$(BUILDTAGS)" -ldflags '$(LDFLAGS)'
BINDIR = bin
COMMIT ?= $(shell git rev-parse --short HEAD)
COMMIT_DATE ?= $(shell git show --no-patch --no-notes --pretty='%cd' $(COMMIT) --date=iso)
VERSION ?= $(shell git describe --tags 2> /dev/null || echo v0)
PERMALINK ?= $(shell git describe --tags --exact-match &> /dev/null && echo latest || echo canary)
BUILDTAGS = netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp
CLIENT_PLATFORM ?= $(shell go env GOOS)
CLIENT_ARCH ?= $(shell go env GOARCH)
@@ -31,18 +32,48 @@ FILE_EXT=
endif
.PHONY: build
build:
build: clean generate
mkdir -p $(BINDIR)
go build -ldflags '$(LDFLAGS)' -o $(BINDIR)/$(MIXIN)$(FILE_EXT) ./cmd/$(MIXIN)
go build -ldflags '$(LDFLAGS)' -tags "$(BUILDTAGS)" -o $(BINDIR)/$(MIXIN)$(FILE_EXT) ./cmd/$(MIXIN)
$(MAKE) clean
xbuild-all:
.PHONY: test
test:
go test -short -count=1 -tags "$(BUILDTAGS)" -v ./...
xbuild-all: clean generate
$(foreach OS, $(SUPPORTED_PLATFORMS), \
$(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)
generate: get-crds packr2
go generate ./...
HAS_PACKR2 := $(shell command -v packr2)
packr2:
ifndef HAS_PACKR2
go get -u github.com/gobuffalo/packr/v2/packr2
endif
clean: clean-packr
-rm -rf /tmp/operator
-rm -fr pkg/qliksense/crds
clean-packr: packr2
cd pkg/qliksense && packr2 clean
get-crds:
git clone git@github.com:qlik-oss/qliksense-operator.git -b custom_cr /tmp/operator
mkdir -p pkg/qliksense/crds/cr
mkdir -p pkg/qliksense/crds/crd
mkdir -p pkg/qliksense/crds/crd-deploy
cp /tmp/operator/deploy/*.yaml pkg/qliksense/crds/crd-deploy
cp /tmp/operator/deploy/crds/*_crd.yaml pkg/qliksense/crds/crd
cp /tmp/operator/deploy/crds/*_cr.yaml pkg/qliksense/crds/cr

View File

@@ -160,3 +160,7 @@ qliksense build
```
Once built, all of the `porter` command that were used with `--tag` can be now be used without this flag provided that porter is executed with the `qliksense-k8s` directory. `porter` will automatically use the qliksense-k8s (and the porter.yaml) in the current directory.
## List of Commands
- [qliksense about](action_about.md)

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

65
cmd/qliksense/about.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import (
"errors"
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"strings"
)
type aboutCommandOptions struct {
Profile string
}
func about(q *qliksense.Qliksense) *cobra.Command {
opts := &aboutCommandOptions{}
c := &cobra.Command{
Use: "about ref",
Short: "About Qlik Sense",
Long: "Gives the version of QLik Sense on Kubernetes and versions of images.",
Example: `
qliksense about 1.0.0
- display default profile (docker-desktop) for Git ref 1.0.0 in the qliksense-k8s repo
qliksense about 1.0.0 --profile=docker-desktop
- specifying profile
qliksense about
qliksense about --profile=test
- if no Git ref is provided, then get version information from the configuration on disk:
- if user's current directory has a subdirectory "manifests/${profile}",
then get version information from that
- if using other supported commands the user has built a CR in ~/.qliksense,
then get version information based on the path derived like so:
- ${spec.manifestsRoot}/${spec.profile} # if no profile flag provided
- ${spec.manifestsRoot}/${profile} # if profile is provided using the --profile command flag
- if no config found on disk in locations described above,
then get version information based on the default profile in the qliksense-k8s repo master
`,
RunE: func(cmd *cobra.Command, args []string) error {
if gitRef, err := getAboutCommandGitRef(args); err != nil {
return err
} else if vout, err := q.About(gitRef, opts.Profile); err != nil {
return err
} else if out, err := yaml.Marshal(vout); err != nil {
return err
} else if _, err := fmt.Println(out); err != nil {
return err
}
return nil
},
}
f := c.Flags()
f.StringVar(&opts.Profile, "profile", "", "Configuration profile")
return c
}
func getAboutCommandGitRef(args []string) (string, error) {
if len(args) > 1 {
return "", errors.New("too many arguments, only 1 expected")
} else if len(args) == 1 {
return strings.TrimSpace(args[0]), nil
}
return "", nil
}

View File

@@ -1,423 +0,0 @@
package main
import (
"bufio"
"os"
"strings"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func buildAliasCommands(porterCmd *cobra.Command, q *qliksense.Qliksense) []*cobra.Command {
return []*cobra.Command{
buildBuildAlias(porterCmd, q),
buildInstallAlias(porterCmd, q),
buildUpgradeAlias(porterCmd, q),
buildAboutAlias(porterCmd, q),
buildPreflightAlias(porterCmd, q),
buildUninstallAlias(porterCmd, q),
}
}
func buildBuildAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
)
c = &cobra.Command{
Use: "build",
Short: "Build a bundle",
Long: "Builds the bundle in the current directory by generating a Dockerfile and a CNAB bundle.json, and then building the invocation image.",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
return porterCmd.RunE(porterCmd, append([]string{"build"}, args...))
},
Annotations: map[string]string{
"group": "alias",
},
}
return c
}
type paramOptions struct {
aboutOptions
Params []string
ParamFiles []string
Name string
InsecureRegistry bool
// CredentialIdentifiers is a list of credential names or paths to make available to the bundle.
CredentialIdentifiers []string
Driver string
Force bool
Insecure bool
}
func buildInstallAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
opts *paramOptions
registry *string
)
opts = &paramOptions{}
c = &cobra.Command{
Use: "install [INSTANCE]",
Short: "Install qliksense",
Long: `Install a new instance of a bundle.
The first argument is the bundle instance name to create for the installation. This defaults to the name of the bundle.
Porter uses the Docker driver as the default runtime for executing a bundle's invocation image, but an alternate driver may be supplied via '--driver/-d'.
For example, the 'debug' driver may be specified, which simply logs the info given to it and then exits.`,
Example: ` qliksense install
qliksense install --version v1.0.0
qliksense install --insecure
qliksense install qliksense --file qliksense/bundle.json
qliksense install --param-file base-values.txt --param-file dev-values.txt --param test-mode=true --param header-color=blue
qliksense install --cred kubernetes
qliksense install --driver debug
qliksense install MyAppFromTag --tag qlik/qliksense-cnab-bundle:v1.0.0
`,
PreRun: func(cmd *cobra.Command, args []string) {
checkMinVersion(opts.Tag, q)
},
//DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Push images here.
// TODO: Need to get the private reg from params
args = append(os.Args[2:], opts.getTagValue(args)...)
if registry = opts.findKey("dockerRegistry"); registry != nil {
if len(*registry) > 0 {
q.TagAndPushImages(*registry)
}
}
return porterCmd.RunE(porterCmd, append([]string{"install"}, args...))
},
Annotations: map[string]string{
"group": "alias",
},
}
f := c.Flags()
f.StringVarP(&opts.Version, "version", "v", "latest",
"Version of Qlik Sense to install")
f.BoolVar(&opts.Insecure, "insecure", true,
"Allow working with untrusted bundles")
f.StringVarP(&opts.File, "file", "f", "",
"Path to the porter manifest file. Defaults to the bundle in the current directory.")
f.StringVar(&opts.CNABFile, "cnab-file", "",
"Path to the CNAB bundle.json file.")
f.StringSliceVar(&opts.ParamFiles, "param-file", nil,
"Path to a parameters definition file for the bundle, each line in the form of NAME=VALUE. May be specified multiple times.")
f.StringSliceVar(&opts.Params, "param", nil,
"Define an individual parameter in the form NAME=VALUE. Overrides parameters set with the same name using --param-file. May be specified multiple times.")
f.StringSliceVarP(&opts.CredentialIdentifiers, "cred", "c", nil,
"Credential to use when installing the bundle. May be either a named set of credentials or a filepath, and specified multiple times.")
f.StringVarP(&opts.Driver, "driver", "d", "docker",
"Specify a driver to use. Allowed values: docker, debug")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.BoolVar(&opts.InsecureRegistry, "insecure-registry", false,
"Don't require TLS for the registry")
f.BoolVar(&opts.Force, "force", false,
"Force a fresh pull of the bundle and all dependencies")
return c
}
func buildUpgradeAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
opts *paramOptions
registry *string
)
opts = &paramOptions{}
c = &cobra.Command{
Use: "upgrade [INSTANCE]",
Short: "Upgrade qliksense",
Long: `Upgrade to a new instance of a bundle.
The first argument is the bundle instance name to upgrade for the installation. This defaults to the name of the bundle.
Porter uses the Docker driver as the default runtime for executing a bundle's invocation image, but an alternate driver may be supplied via '--driver/-d'.
For example, the 'debug' driver may be specified, which simply logs the info given to it and then exits.`,
Example: ` qliksense upgrade
qliksense upgrade --version v1.0.0
qliksense upgrade --insecure
qliksense upgrade qliksense --file qliksense/bundle.json
qliksense upgrade --param-file base-values.txt --param-file dev-values.txt --param test-mode=true --param header-color=blue
qliksense upgrade --cred kubernetes
qliksense upgrade --driver debug
qliksense upgrade MyAppFromTag --tag qlik/qliksense-cnab-bundle:v1.0.0
`,
PreRun: func(cmd *cobra.Command, args []string) {
checkMinVersion(opts.Tag, q)
},
//DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Push images here.
// TODO: Need to get the private reg from params
args = append(os.Args[2:], opts.getTagValue(args)...)
if registry = opts.findKey("dockerRegistry"); registry != nil {
if len(*registry) > 0 {
q.TagAndPushImages(*registry)
}
}
return porterCmd.RunE(porterCmd, append([]string{"upgrade"}, args...))
},
Annotations: map[string]string{
"group": "alias",
},
}
f := c.Flags()
f.StringVarP(&opts.Version, "version", "v", "latest",
"Version of Qlik Sense to upgrade to")
f.BoolVar(&opts.Insecure, "insecure", true,
"Allow working with untrusted bundles")
f.StringVarP(&opts.File, "file", "f", "",
"Path to the porter manifest file. Defaults to the bundle in the current directory.")
f.StringVar(&opts.CNABFile, "cnab-file", "",
"Path to the CNAB bundle.json file.")
f.StringSliceVar(&opts.ParamFiles, "param-file", nil,
"Path to a parameters definition file for the bundle, each line in the form of NAME=VALUE. May be specified multiple times.")
f.StringSliceVar(&opts.Params, "param", nil,
"Define an individual parameter in the form NAME=VALUE. Overrides parameters set with the same name using --param-file. May be specified multiple times.")
f.StringSliceVarP(&opts.CredentialIdentifiers, "cred", "c", nil,
"Credential to use when installing the bundle. May be either a named set of credentials or a filepath, and specified multiple times.")
f.StringVarP(&opts.Driver, "driver", "d", "docker",
"Specify a driver to use. Allowed values: docker, debug")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.BoolVar(&opts.InsecureRegistry, "insecure-registry", false,
"Don't require TLS for the registry")
f.BoolVar(&opts.Force, "force", false,
"Force a fresh pull of the bundle and all dependencies")
return c
}
func buildUninstallAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
opts *paramOptions
)
opts = &paramOptions{}
c = &cobra.Command{
Use: "uninstall [INSTANCE]",
Short: "Uninstall a bundle instance",
Long: `Uninstall a bundle instance
The first argument is the bundle instance name to uninstall. This defaults to the name of the bundle.
Porter uses the Docker driver as the default runtime for executing a bundle's invocation image, but an alternate driver may be supplied via '--driver/-d'.
For example, the 'debug' driver may be specified, which simply logs the info given to it and then exits.`,
Example: ` qliksense uninstall
qliksense uninstall --insecure
qliksense uninstall MyAppInDev --file myapp/bundle.json
qliksense uninstall --param-file base-values.txt --param-file dev-values.txt --param test-mode=true --param header-color=blue
qliksense uninstall --cred azure --cred kubernetes
qliksense uninstall --driver debug
qliksense uninstall MyAppFromTag --tag deislabs/porter-kube-bundle:v1.0
`,
RunE: func(cmd *cobra.Command, args []string) error {
return porterCmd.RunE(porterCmd, append([]string{"uninstall"}, os.Args[2:]...))
},
Annotations: map[string]string{
"group": "alias",
},
}
f := c.Flags()
f.BoolVar(&opts.Insecure, "insecure", true,
"Allow working with untrusted bundles")
f.StringVarP(&opts.File, "file", "f", "",
"Path to the porter manifest file. Defaults to the bundle in the current directory. Optional unless a newer version of the bundle should be used to uninstall the bundle.")
f.StringVar(&opts.CNABFile, "cnab-file", "",
"Path to the CNAB bundle.json file.")
f.StringSliceVar(&opts.ParamFiles, "param-file", nil,
"Path to a parameters definition file for the bundle, each line in the form of NAME=VALUE. May be specified multiple times.")
f.StringSliceVar(&opts.Params, "param", nil,
"Define an individual parameter in the form NAME=VALUE. Overrides parameters set with the same name using --param-file. May be specified multiple times.")
f.StringSliceVarP(&opts.CredentialIdentifiers, "cred", "c", nil,
"Credential to use when uninstalling the bundle. May be either a named set of credentials or a filepath, and specified multiple times.")
f.StringVarP(&opts.Driver, "driver", "d", "docker",
"Specify a driver to use. Allowed values: docker, debug")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.BoolVar(&opts.InsecureRegistry, "insecure-registry", false,
"Don't require TLS for the registry")
f.BoolVar(&opts.Force, "force", false,
"Force a fresh pull of the bundle and all dependencies")
return c
}
func (o *aboutOptions) getTagDefaults(args []string) []string {
args = append(args, o.getTagValue(args)...)
return args
}
func (o *aboutOptions) getTagValue(args []string) []string {
tagArr := []string{}
if len(o.Tag) > 1 {
tagArr = []string{"--tag", o.Tag}
}
if len(o.Tag) <= 0 && len(o.File) <= 0 && len(o.CNABFile) <= 0 {
if _, err := os.Stat("porter.yaml"); err != nil {
tagArr = []string{"--tag", "qlik/qliksense-cnab-bundle:" + o.Version}
}
}
return tagArr
}
type aboutOptions struct {
Version string
Tag string
File string
CNABFile string
}
func buildAboutAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
opts *aboutOptions
)
opts = &aboutOptions{}
c = &cobra.Command{
Use: "about",
Short: "About Qlik Sense",
Long: "Gives the verion of QLik Sense on Kuberntetes and versions of images.",
PreRun: func(cmd *cobra.Command, args []string) {
checkMinVersion(opts.Tag, q)
},
RunE: func(cmd *cobra.Command, args []string) error {
args = opts.getTagDefaults(args)
return porterCmd.RunE(porterCmd, append([]string{"invoke", "--action", "about"}, args...))
},
Annotations: map[string]string{
"group": "alias",
},
}
f := c.Flags()
f.StringVarP(&opts.Version, "version", "v", "latest",
"Version of Qlik Sense to install")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.StringVarP(&opts.File, "file", "f", "",
"Path to the porter manifest file. Defaults to the bundle in the current directory.")
f.StringVar(&opts.CNABFile, "cnab-file", "",
"Path to the CNAB bundle.json file.")
return c
}
func buildPreflightAlias(porterCmd *cobra.Command, q *qliksense.Qliksense) *cobra.Command {
var (
c *cobra.Command
opts *paramOptions
)
opts = &paramOptions{}
c = &cobra.Command{
Use: "preflight",
Short: "Preflight Checks",
Long: "Perform Preflight Checks",
PreRun: func(cmd *cobra.Command, args []string) {
checkMinVersion(opts.Tag, q)
},
RunE: func(cmd *cobra.Command, args []string) error {
args = append(os.Args[1:], opts.getTagDefaults(args)...)
return porterCmd.RunE(porterCmd, append([]string{"invoke", "--action", "preflight"}, args...))
},
Annotations: map[string]string{
"group": "alias",
},
}
f := c.Flags()
f.StringSliceVar(&opts.Params, "param", nil,
"Define an individual parameter in the form NAME=VALUE. Overrides parameters set with the same name using --param-file. May be specified multiple times.")
f.StringSliceVar(&opts.ParamFiles, "param-file", nil,
"Path to a parameters definition file for the bundle, each line in the form of NAME=VALUE. May be specified multiple times.")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.StringVarP(&opts.Version, "version", "v", "latest",
"Version of Qlik Sense to install")
f.StringSliceVarP(&opts.CredentialIdentifiers, "cred", "c", nil,
"Credential to use when installing the bundle. May be either a named set of credentials or a filepath, and specified multiple times.")
return c
}
func (o *paramOptions) findKey(param string) *string {
var (
value *string
)
if value = o.findParams(param); value != nil {
return value
}
if value = o.findParamFiles(param); value != nil {
return value
}
return nil
}
// parsedParams parses the variable assignments in Params.
func (o *paramOptions) findParams(param string) *string {
return o.findVariableKey(param, o.Params)
}
// parseParamFiles parses the variable assignments in ParamFiles.
func (o *paramOptions) findParamFiles(param string) *string {
var (
path string
retStr *string
)
for _, path = range o.ParamFiles {
retStr = o.findParamFile(param, path)
}
return retStr
}
func (o *paramOptions) findParamFile(param string, path string) *string {
var (
f *os.File
err error
scanner *bufio.Scanner
lines []string
retStr *string
)
if f, err = os.Open(path); err == nil {
defer f.Close()
scanner = bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
retStr = o.findVariableKey(param, lines)
}
return retStr
}
func (o *paramOptions) findVariableKey(param string, params []string) *string {
var (
variable, value string
)
for _, p := range params {
parts := strings.SplitN(p, "=", 2)
if len(parts) >= 2 {
variable = strings.TrimSpace(parts[0])
if variable == param {
value = strings.TrimSpace(parts[1])
return &value
}
}
}
return nil
}

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

@@ -0,0 +1,42 @@
package main
import (
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "do operations on/around CR",
Long: `do operations on/around CR`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Use like: config view or config apply")
},
}
func configApplyCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "apply",
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 {
return q.ConfigApplyQK8s()
},
}
return c
}
func configViewCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "view",
Short: "view the qliksense operator CR",
Long: `display the operator CR, that has been created for the current context`,
Example: `qliksense config view`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.ConfigViewCR()
},
}
return c
}

30
cmd/qliksense/fetch.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"errors"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func fetchCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "fetch",
Short: "fetch a release from qliksense-k8s repo",
Long: `fetch a release from qliksense-k8s repo`,
Example: `qliksense fetch <version>`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires a version (i.e. v1.0.0)")
}
if _, err := semver.NewVersion(args[0]); err != nil {
return errors.New("is it not a valid version. should be something like this v1.0.0")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
q.FetchQK8s(args[0])
},
}
return c
}

30
cmd/qliksense/install.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"errors"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func installCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "install",
Short: "install a qliksense release",
Long: `install a qliksesne release`,
Example: `qliksense install <version>`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("requires a version (i.e. v1.0.0)")
}
if _, err := semver.NewVersion(args[0]); err != nil {
return errors.New("is it not a valid version. should be something like this v1.0.0")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return q.InstallQK8s(args[0])
},
}
return c
}

28
cmd/qliksense/operator.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
var operatorCmd = &cobra.Command{
Use: "operator",
Short: "Configuration for operator",
Long: `Configuration for operator`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("User like: operator view")
},
}
func operatorViewCmd(q *qliksense.Qliksense) *cobra.Command {
c := &cobra.Command{
Use: "view",
Short: "View Configuration for operator",
Long: `View Configuration for operator`,
Run: func(cmd *cobra.Command, args []string) {
q.ViewOperatorCrd()
},
}
return c
}

View File

@@ -1,31 +0,0 @@
package main
import (
"fmt"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
"strings"
)
func porter(q *qliksense.Qliksense) *cobra.Command {
return &cobra.Command{
Use: "porter",
Short: "Execute a porter command",
DisableFlagParsing: true,
RunE: func(cobCmd *cobra.Command, args []string) error {
var (
err error
)
if _, err = q.CallPorter(args,
func(x string) (out *string) {
out = new(string)
*out = strings.ReplaceAll(x, "porter", "qliksense porter")
fmt.Println(*out)
return
}); err != nil {
return err
}
return nil
},
}
}

View File

@@ -1,33 +1,27 @@
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func pullQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
var (
cmd *cobra.Command
opts *aboutOptions
)
opts = &aboutOptions{}
cmd = &cobra.Command{
Use: "pull",
Short: "Pull docke images for offline install",
Example: `qliksense pull`,
RunE: func(cmd *cobra.Command, args []string) error {
return q.PullImages(opts.getTagDefaults(args))
},
}
f := cmd.Flags()
f.StringVarP(&opts.Version, "version", "v", "latest",
"From version of Qlik Sense The images will be pulled")
f.StringVarP(&opts.Tag, "tag", "t", "",
"Use a bundle in an OCI registry specified by the given tag")
f.StringVarP(&opts.File, "file", "f", "",
"Path to the porter manifest file. Defaults to the bundle in the current directory.")
f.StringVar(&opts.CNABFile, "cnab-file", "",
"Path to the CNAB bundle.json file.")
return cmd
}
package main
import (
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"github.com/spf13/cobra"
)
func pullQliksenseImages(q *qliksense.Qliksense) *cobra.Command {
opts := &aboutCommandOptions{}
cmd := &cobra.Command{
Use: "pull",
Short: "Pull docke images for offline install",
Example: `qliksense pull`,
RunE: func(cmd *cobra.Command, args []string) error {
if gitRef, err := getAboutCommandGitRef(args); err != nil {
return err
} else if err = q.PullImages(gitRef, opts.Profile, false); err != nil {
return err
}
return nil
},
}
f := cmd.Flags()
f.StringVar(&opts.Profile, "profile", "", "Configuration profile")
return cmd
}

View File

@@ -1,16 +1,12 @@
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/mitchellh/go-homedir"
@@ -25,88 +21,45 @@ import (
// qliksense <command>
const (
winOS = "porter-windows-amd64.exe"
linuxOS = "porter-linux-amd64"
macOS = "porter-darwin-amd64"
porterHomeVar = "PORTER_HOME"
qlikSenseHomeVar = "QLIKSENSE_HOME"
qlikSenseDirVar = ".qliksense"
mixinDirVar = "mixins"
porterRuntime = "porter-runtime"
)
func initAndExecute() error {
var (
porterExe, qlikSenseHome string
err error
qlikSenseHome string
err error
)
porterExe, qlikSenseHome, err = setUpPaths()
qlikSenseHome, err = setUpPaths()
if err != nil {
log.Fatal(err)
}
if err = rootCmd(qliksense.New(porterExe, qlikSenseHome)).Execute(); err != nil {
if err = rootCmd(qliksense.New(qlikSenseHome)).Execute(); err != nil {
return err
}
return nil
}
func setUpPaths() (string, string, error) {
func setUpPaths() (string, error) {
var (
porterExe, homeDir, qlikSenseHome string
err error
homeDir, qlikSenseHome string
err error
)
porterExe = "porter"
if runtime.GOOS == "windows" {
porterExe = porterExe + ".exe"
}
if qlikSenseHome = os.Getenv(qlikSenseHomeVar); qlikSenseHome == "" {
if homeDir, err = homedir.Dir(); err != nil {
return "", "", err
return "", err
}
if homeDir, err = homedir.Expand(homeDir); err != nil {
return "", "", err
return "", err
}
qlikSenseHome = filepath.Join(homeDir, qlikSenseDirVar)
}
os.Setenv(porterHomeVar, qlikSenseHome)
porterExe = filepath.Join(qlikSenseHome, porterExe)
return porterExe, qlikSenseHome, nil
}
func installPorter(qlikSenseHome, porterExe string) (string, error) {
var (
destination string
// downloadPorter = true
porterDownloadURL string
err error
)
// if downloadPorter {
os.Mkdir(qlikSenseHome, os.ModePerm)
destination = filepath.Join(qlikSenseHome, porterRuntime)
// construct url to download porter from
porterDownloadURL = constructPorterURL(runtime.GOOS)
if (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") || runtime.GOOS == "darwin" {
if err = downloadFile(porterDownloadURL, destination); err != nil {
return "", err
}
os.Chmod(destination, 0755)
if _, err = copy(destination, porterExe); err != nil {
return "", err
}
os.Chmod(porterExe, 0755)
} else if runtime.GOOS == "windows" {
if err = downloadFile(porterDownloadURL, porterExe); err != nil {
return "", err
}
os.Chmod(porterExe, 0755)
}
return porterExe, nil
return qlikSenseHome, nil
}
var versionCmd = &cobra.Command{
@@ -118,123 +71,9 @@ var versionCmd = &cobra.Command{
},
}
func constructPorterURL(runtimeOS string) string {
// FYI: Porter does not support other architectures other than amd64
const (
porterURLBase = "https://cdn.deislabs.io/porter/"
winOS = "porter-windows-amd64.exe"
linuxOS = "porter-linux-amd64"
macOS = "porter-darwin-amd64"
)
var url, version string
version = retrievePorterVersion()
if runtimeOS == "linux" {
url = porterURLBase + version + "/" + linuxOS
} else if runtimeOS == "windows" {
url = porterURLBase + version + "/" + winOS
} else if runtimeOS == "darwin" {
url = porterURLBase + version + "/" + macOS
}
return url
}
func retrievePorterVersion() string {
type apiInfo struct {
TagName string `json:"tag_name,omitempty"`
Name string `json:"name,omitempty"`
}
const porterRepoURL = "https://api.github.com/repos/deislabs/porter/releases/latest"
resp, err := http.Get(porterRepoURL)
if err != nil {
fmt.Printf("Error occurred while retrieving porter version info: %v\n", err)
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("response status was not OK while retrieving porter version info\n")
return ""
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error occurred while reading porter version info: %v\n", err)
return ""
}
result := &apiInfo{}
err = json.Unmarshal(body, result)
if err != nil {
fmt.Printf("Error occurred while unmarshalling porter version info: %v\n", err)
return ""
}
fmt.Printf("Porter Version: %s\n", result.Name)
return result.Name
}
func installMixins(porterExe, qlikSenseHome string) (string, error) {
var (
mixin, mixinOpts string
mixinsVar = map[string]string{
"kustomize": "-v 0.2-beta-3-0e19ca4 --url https://github.com/donmstewart/porter-kustomize/releases/download",
"qliksense": "-v v0.16.0 --url https://github.com/qlik-oss/porter-qliksense/releases/download",
"exec": "-v latest",
"kubernetes": "-v latest",
"helm": "-v latest",
"azure": "-v latest",
"terraform": "-v latest",
"az": "-v latest",
"aws": "-v latest",
"gcloud": "-v latest",
}
downloadMixins map[string]string
err error
)
if _, err = os.Stat(filepath.Join(qlikSenseHome, mixinDirVar)); err != nil {
if os.IsNotExist(err) {
downloadMixins = mixinsVar
} else {
return "", err
}
} else {
downloadMixins = make(map[string]string)
for mixin, mixinOpts = range mixinsVar {
if _, err = os.Stat(filepath.Join(qlikSenseHome, mixinDirVar, mixin)); err != nil {
if os.IsNotExist(err) {
downloadMixins[mixin] = mixinOpts
} else {
return "", err
}
}
}
}
for mixin, mixinOpts = range downloadMixins {
if _, err = installMixin(porterExe, mixin, mixinOpts); err != nil {
return "", err
}
}
return "", err
}
func installMixin(porterExe, mixin, mixinOpts string) (string, error) {
var cmd *exec.Cmd
args := []string{"mixin", "install", mixin}
if mixinOpts != "" {
args = append(args, strings.Fields(mixinOpts)...)
}
cmd = exec.Command(porterExe, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", err
}
return "", nil
}
func rootCmd(p *qliksense.Qliksense) *cobra.Command {
var (
cmd, porterCmd, alias *cobra.Command
cmd *cobra.Command
)
cmd = &cobra.Command{
@@ -253,13 +92,24 @@ func rootCmd(p *qliksense.Qliksense) *cobra.Command {
// For qliksense overrides/commands
cmd.AddCommand(pullQliksenseImages(p))
porterCmd = porter(p)
cmd.AddCommand(porterCmd)
for _, alias = range buildAliasCommands(porterCmd, p) {
cmd.AddCommand(alias)
}
cmd.AddCommand(about(p))
// add version command
cmd.AddCommand(versionCmd)
// add operator command
cmd.AddCommand(operatorCmd)
operatorCmd.AddCommand(operatorViewCmd(p))
//add fetch command
cmd.AddCommand(fetchCmd(p))
// add install command
cmd.AddCommand(installCmd(p))
// add config command
cmd.AddCommand(configCmd)
configCmd.AddCommand(configApplyCmd(p))
configCmd.AddCommand(configViewCmd(p))
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
return cmd
@@ -276,7 +126,6 @@ func downloadFile(url string, filepath string) error {
err error
resp *http.Response
)
logDebugMessage("Porter download link: %s\n", url)
// Create the file
if out, err = os.Create(filepath); err != nil {
return err

View File

@@ -1,292 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/qlik-oss/sense-installer/pkg"
"github.com/qlik-oss/sense-installer/pkg/qliksense"
"gopkg.in/yaml.v2"
)
var (
dependenciesFile = "dependencies.yaml"
updateMixin, updateComponent bool
currentPorterVersion string
mixinURLs = map[string]string{
"qliksense": "--url https://github.com/qlik-oss/porter-qliksense/releases/download",
"kustomize": "--url https://github.com/donmstewart/porter-kustomize/releases/",
}
mixinsVar = map[string]string{
"kustomize": "-v 0.2-beta-3-0e19ca4 --url https://github.com/donmstewart/porter-kustomize/releases/download",
"qliksense": "-v v0.14.0 --url https://github.com/qlik-oss/porter-qliksense/releases/download",
"exec": "-v latest",
"kubernetes": "-v latest",
"helm": "-v latest",
"azure": "-v latest",
"terraform": "-v latest",
"az": "-v latest",
"aws": "-v latest",
"gcloud": "-v latest",
}
)
func checkMinVersion(tag string, q *qliksense.Qliksense) {
logDebugMessage("Starting version checks\n")
dependencies := map[string]string{}
// check if tag is empty or not
var err error
if len(strings.TrimSpace(tag)) > 0 {
// --tag exists
logDebugMessage("Input tag: %s, %s\n", tag, strings.Replace(tag, "bundle", "invocation", 1))
// Pull the image and store labels in a map
dependencies, err = q.PullImage(strings.Replace(tag, "bundle", "invocation", 1))
logDebugMessage("\nDependencies map from the inspected image: %v\n", dependencies)
if err != nil {
log.Fatalf("unable to pull the requested image: %v", err)
}
} else {
// Tag is empty, hence looking for dependenciesFile.yaml, exit if this file is not present
if fileExists(dependenciesFile) {
// read the dependencies.yaml and store into a map
yamlFile, err := ioutil.ReadFile(dependenciesFile)
if err != nil {
logDebugMessage("Exit: CheckMinVersion()\n")
log.Fatalf("Error reading from source: %s\n", err)
}
err = yaml.Unmarshal(yamlFile, &dependencies)
if err != nil {
logDebugMessage("Exit: CheckMinVersion()\n")
log.Fatalf("Error when parsing from source: %s\n", err)
}
logDebugMessage("Dependencies map from the given yaml: %v\n", dependencies)
}
}
if len(dependencies) > 0 {
for k, v := range dependencies {
if strings.Contains(k, ".mixin.") {
dependencies[k] = fmt.Sprintf("-v %s", v)
}
}
// CLI check
checkCLIVersion(dependencies)
// Porter check
checkPorterVersion(dependencies, q)
// Mixins check
checkMixinVersion(dependencies, q)
} else {
log.Fatalf("Not able to infer dependencies, hence exiting")
}
logDebugMessage("Completed version checks\n")
}
func checkMixinVersion(dependencies map[string]string, q *qliksense.Qliksense) {
var tmp string
logDebugMessage("------------ Mixins version check -----------")
currentMixinVersions, err := retrieveCurrentInstalledMixinVersions(q)
if err != nil {
log.Fatal(err)
}
for k := range mixinsVar {
tmp, _ = dependencies[fmt.Sprintf("org.qlik.operator.mixin.%s.version.min", k)]
if tmp == "" {
continue
}
shouldUpdateMixin := false
mixinVersion, ok := currentMixinVersions[k]
if !ok {
shouldUpdateMixin = true
} else {
shouldUpdateMixin = versionCheck(fmt.Sprintf("Mixin %s", k), mixinVersion, tmp)
}
// if tmp is not empty and mixin requires download and install
if shouldUpdateMixin {
fmt.Println("Downloading a newer version of mixin:", k)
// download and install the new mixin
mURL, ok := mixinURLs[k]
if ok {
tmp = fmt.Sprintf("%s %s", tmp, mURL)
}
if _, err = installMixin(q.PorterExe, k, tmp); err != nil {
logDebugMessage("Completed version checks\n")
log.Fatalf("Error installing mixin %s: %s\n", k, err)
}
}
}
}
func checkPorterVersion(dependencies map[string]string, q *qliksense.Qliksense) {
// Infer info about the min porter version
var porterVersionFromDependencies, tmp string
var err error
logDebugMessage("------------ Porter version check -----------")
tmp, _ = dependencies["org.qlik.operator.cli.porter.version.min"]
if len(tmp) != 0 {
porterVersionFromDependencies = tmp
}
logDebugMessage("Porter version from dependencies map: %v\n", porterVersionFromDependencies)
// check porter version
currentPorterVersion, err = determineCurrentPorterVersion(q)
if err != nil {
log.Println("warning:", err)
}
logDebugMessage("Current Porter version: %v\n", currentPorterVersion)
updateComponent = true
if currentPorterVersion != "" {
updateComponent = versionCheck("Porter", currentPorterVersion, porterVersionFromDependencies)
}
if updateComponent {
fmt.Println("Downloading a newer version of Porter")
// Download and install newer version of porter and mixins
q.PorterExe, err = installPorter(q.QliksenseHome, q.PorterExe)
if err != nil {
logDebugMessage("Completed version checks")
log.Fatalf("error installing porter: %v", err)
}
if _, err = installMixins(q.PorterExe, q.QliksenseHome); err != nil {
logDebugMessage("Completed version checks")
log.Fatalf("error installing mixin: %v", err)
}
}
}
func checkCLIVersion(dependencies map[string]string) {
// Infer info about the minimum cli version
var cliVersionFromDependencies, tmp string
logDebugMessage("\n------------ CLI version check -----------\n")
tmp, _ = dependencies["org.qlik.operator.cli.sense-installer.version.min"]
if len(tmp) != 0 {
cliVersionFromDependencies = tmp
}
logDebugMessage("\nCLI version from dependencies map: %v\n", cliVersionFromDependencies)
// Checking version below
updateComponent = versionCheck("CLI", pkg.Version, cliVersionFromDependencies)
if updateComponent {
log.Fatalf("Please download a newer version of CLI and retry the operation, exiting now.")
}
}
func retrieveCurrentInstalledMixinVersions(q *qliksense.Qliksense) (map[string]string, error) {
if _, err := os.Stat(filepath.Join(q.QliksenseHome, mixinDirVar)); err != nil {
if os.IsNotExist(err) {
// if path doesnt exist, return empty map, and let porter take care of the rest
return map[string]string{}, nil
}
return nil, err
}
result := map[string]string{}
currentInstalledMixinVersions, err := q.CallPorter([]string{"mixins", "list"}, func(x string) (out *string) {
out = new(string)
*out = strings.ReplaceAll(x, "porter", "qliksense porter")
logDebugMessage("%s\n", *out)
return
})
if err != nil {
fmt.Printf("Error occurred when retrieving mixins list: %v", err)
return nil, err
}
currentInstalledMixinVersionsArr := strings.Split(currentInstalledMixinVersions, "\n")
for _, mix := range currentInstalledMixinVersionsArr {
mixRow := strings.Fields(mix)
mixRowLen := len(mixRow)
if mixRowLen > 0 && mixRow[0] == "Name" {
continue
}
// we handle the case of mixins like `kustomize` where version and author could be empty
if mixRowLen >= 2 {
_, err := semver.NewVersion(mixRow[1])
if err == nil {
result[mixRow[0]] = mixRow[1]
}
}
}
return result, nil
}
func determineVersion(versionString string) (string, error) {
versionSlice := strings.Fields(versionString)
var currentComponentVersionNumber *semver.Version
var err error
for _, value := range versionSlice {
currentComponentVersionNumber, err = semver.NewVersion(value)
if err == nil {
break
}
}
logDebugMessage("Version string: %v\n", currentComponentVersionNumber)
if currentComponentVersionNumber != nil {
return currentComponentVersionNumber.String(), nil
}
return "", fmt.Errorf("unable to extract version information")
}
func determineCurrentPorterVersion(q *qliksense.Qliksense) (string, error) {
// determine current porter version
currentPorterVersion, err := q.CallPorter([]string{"version"}, func(x string) (out *string) {
out = new(string)
*out = strings.ReplaceAll(x, "porter", "qliksense porter")
logDebugMessage(*out)
return
})
if err != nil {
fmt.Printf("Error occurred during porter call: %v", err)
return "", err
}
return determineVersion(currentPorterVersion)
}
func versionCheck(component string, currentVersion string, versionFromSourceOfTruth string) bool {
if strings.HasPrefix(versionFromSourceOfTruth, "-v ") {
versionFromSourceOfTruth = strings.Replace(versionFromSourceOfTruth, "-v ", "", 1)
}
componentVersionFromDependenciesYaml, err := semver.NewVersion(versionFromSourceOfTruth)
if err != nil {
fmt.Printf("There has been an error parsing version from source of truth: %s\n", err)
return true
}
logDebugMessage("%s version from source of truth: %s\n", component, componentVersionFromDependenciesYaml)
currentComponentVersion, err := semver.NewVersion(currentVersion)
if err != nil {
fmt.Printf("There has been an error parsing version from the derived current version: %s\n", err)
return true
}
logDebugMessage("\nCurrently installed %s version: %v\n", component, currentComponentVersion)
// check component version
if currentComponentVersion.LessThan(componentVersionFromDependenciesYaml) {
fmt.Printf("\n\nCurrent %s Component version: %s is less than minimum required version:%s\n", component, currentComponentVersion, componentVersionFromDependenciesYaml)
return true
}
logDebugMessage("Current %s version is greater than version from dependencies, upgrade not necessary.\n\n", component)
return false
}
func logDebugMessage(strMessage string, args ...interface{}) {
if os.Getenv("QLIKSENSE_DEBUG") == "true" {
fmt.Printf(strMessage, args...)
}
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

49
go.mod
View File

@@ -12,59 +12,62 @@ replace (
// github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
// golang.org/x/crypto v0.0.0-20190129210102-0709b304e793 => golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8
k8s.io/client-go => k8s.io/client-go v0.0.0-20191016111102-bec269661e48
k8s.io/kubectl => k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51
sigs.k8s.io/kustomize/api => github.com/qlik-oss/kustomize/api v0.3.3-0.20200129153315-09eb26c762c8
)
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
cloud.google.com/go v0.52.0 // indirect
cloud.google.com/go/storage v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.0.3
github.com/Microsoft/hcsshim v0.8.7 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/aws/aws-sdk-go v1.28.9 // indirect
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/bugsnag/bugsnag-go v1.5.3 // indirect
github.com/bugsnag/panicwrap v1.2.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cloudflare/cfssl v1.4.1 // indirect
github.com/containerd/containerd v1.3.2 // indirect
github.com/containerd/continuity v0.0.0-20191214063359-1097c8bae83b // indirect
github.com/containers/image/v5 v5.1.0
github.com/docker/cli v0.0.0-20191212191748-ebca1413117a
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7
github.com/docker/docker-credential-helpers v0.6.3 // indirect
github.com/docker/go v1.5.1-1 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d // indirect
github.com/gobuffalo/packr/v2 v2.7.1
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/gorilla/mux v1.7.3 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jinzhu/gorm v1.9.11 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/pkg/errors v0.8.1
github.com/qlik-oss/k-apis v0.0.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1
github.com/theupdateframework/notary v0.6.1 // indirect
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 // indirect
google.golang.org/genproto v0.0.0-20200128133413-58ce757ed39b // indirect
google.golang.org/grpc v1.27.0 // indirect
gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect
gopkg.in/fatih/pool.v2 v2.0.0 // indirect
gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect
gopkg.in/yaml.v2 v2.2.7
gopkg.in/yaml.v2 v2.2.8
sigs.k8s.io/kustomize/api v0.3.2
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect
)
exclude github.com/Azure/go-autorest v12.0.0+incompatible

987
go.sum

File diff suppressed because it is too large Load Diff

83
pkg/api/apis.go Normal file
View File

@@ -0,0 +1,83 @@
package api
import (
"errors"
"fmt"
"github.com/jinzhu/copier"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path/filepath"
)
// NewQConfig create QliksenseConfig object from file ~/.qliksense/config.yaml
func NewQConfig(qsHome string) *QliksenseConfig {
configFile := filepath.Join(qsHome, "config.yaml")
data, err := ioutil.ReadFile(configFile)
if err != nil {
fmt.Println("Cannot read config file from: "+configFile, err)
os.Exit(1)
}
qc := &QliksenseConfig{}
err = yaml.Unmarshal(data, qc)
if err != nil {
fmt.Println("yaml unmarshalling error ", err)
os.Exit(1)
}
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 := ""
for _, ctx := range qc.Spec.Contexts {
if ctx.Name == contextName {
crFilePath = ctx.CrFile
break
}
}
if crFilePath == "" {
return nil, errors.New("context name " + contextName + " not found")
}
return getCRObject(crFilePath)
}
// GetCurrentCR create a QliksenseCR object for current context
func (qc *QliksenseConfig) GetCurrentCR() (*QliksenseCR, error) {
return qc.GetCR(qc.Spec.CurrentContext)
}
// SetCrLocation sets the CR location for a context. Helpful during test
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
found = true
}
tempQc.Spec.Contexts = append(tempQc.Spec.Contexts, []Context{c}...)
}
if found {
return tempQc, nil
}
return nil, errors.New("cannot find the context")
}
func getCRObject(crfile string) (*QliksenseCR, error) {
data, err := ioutil.ReadFile(crfile)
if err != nil {
fmt.Println("Cannot read config file from: "+crfile, err)
return nil, err
}
cr := &QliksenseCR{}
err = yaml.Unmarshal(data, cr)
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return nil, err
}
return cr, nil
}

85
pkg/api/apis_test.go Normal file
View File

@@ -0,0 +1,85 @@
package api
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
const tempPermissionCode os.FileMode = 0777
func setup() (func(), string) {
dir, _ := ioutil.TempDir("", "testing_path")
config :=
`
apiVersion: config.qlik.com/v1
kind: QliksenseConfig
metadata:
name: whatever
spec:
contexts:
- name: contx1
crLocation: /Users/mqb/.qliksense/contexts/contx1
- name: cotx2
crLocation: /root/.qliksense/contexts/cotx2.yaml
currentContext: contx1
`
configFile := filepath.Join(dir, "config.yaml")
ioutil.WriteFile(configFile, []byte(config), tempPermissionCode)
tearDown := func() {
os.RemoveAll(dir)
}
return tearDown, dir
}
func createCRFile(homeDir string) {
cr :=
`
apiVersion: qlik.com/v1
kind: QlikSense
metadata:
name: contx1
labels:
version: v1.0.0
spec:
profile: docker-desktop
manifestsRoot: /Users/mqb/.qliksense/contexts/contx1/qlik-k8s/v0.0.1/manifests
namespace: myqliksense
storageClassName: efs
configs:
qliksense:
- name: acceptEULA
value: "yes"
`
ctx1Dir := filepath.Join(homeDir, "contexts", "contx1")
crFile := filepath.Join(ctx1Dir, "contx1.yaml")
os.MkdirAll(ctx1Dir, tempPermissionCode)
ioutil.WriteFile(crFile, []byte(cr), tempPermissionCode)
}
func TestGetCR(t *testing.T) {
td, dir := setup()
qc := NewQConfig(dir)
if qc.Spec.CurrentContext != "contx1" {
t.Fail()
}
// create CR
createCRFile(dir)
crFile := filepath.Join(dir, "contexts", "contx1", "contx1.yaml")
qct, e := qc.SetCrLocation("contx1", crFile)
if e != nil {
t.Fail()
t.Log(e)
}
qcr, err := qct.GetCurrentCR()
if err != nil {
t.Fail()
t.Log(err)
}
if qcr.Spec.Profile != "docker-desktop" {
t.Fail()
}
td()
}

29
pkg/api/kubectl.go Normal file
View File

@@ -0,0 +1,29 @@
package api
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
)
func KubectlApply(manifests string) error {
tempYaml, err := ioutil.TempFile("", "")
if err != nil {
fmt.Println("cannot create file ", err)
return err
}
tempYaml.WriteString(manifests)
cmd := exec.Command("kubectl", "apply", "-f", tempYaml.Name(), "--validate=false")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Printf("kubectl apply failed with %s\n", err)
fmt.Println("temp CRD file: " + tempYaml.Name())
return err
}
os.Remove(tempYaml.Name())
return nil
}

40
pkg/api/types.go Normal file
View File

@@ -0,0 +1,40 @@
package api
import "github.com/qlik-oss/k-apis/pkg/config"
// CommonConfig is exported
type CommonConfig struct {
ApiVersion string `json:"apiVersion" yaml:"apiVersion"`
Kind string `json:"kind" yaml:"kind"`
Metadata *Metadata `json:"metadata" yaml:"metadata"`
}
// QliksenseConfig is exported
type QliksenseConfig struct {
CommonConfig `json:",inline" yaml:",inline"`
Spec *ContextSpec `json:"spec" yaml:"spec"`
}
// QliksenseCR is exported
type QliksenseCR struct {
CommonConfig `json:",inline" yaml:",inline"`
Spec *config.CRSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
}
// ContextSpec is exported
type ContextSpec struct {
Contexts []Context `json:"contexts" yaml:"contexts"`
CurrentContext string `json:"currentContext" yaml:"currentContext"`
}
// Context is exported
type Context struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
CrFile string `json:"crFile,omitempty" yaml:"crFile,omitempty"`
}
// Metadata is exported
type Metadata struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}

238
pkg/qliksense/about.go Normal file
View File

@@ -0,0 +1,238 @@
package qliksense
import (
"bytes"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"sort"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
"gopkg.in/yaml.v2"
)
type patch struct {
Target struct {
Kind string `yaml:"kind"`
LabelSelector string `yaml:"labelSelector"`
} `yaml:"target"`
Patch string `yaml:"patch"`
}
type selectivePatch struct {
APIVersion string `yaml:"apiVersion"`
Metadata struct {
Name string `yaml:"name"`
} `yaml:"metadata"`
Enabled bool `yaml:"enabled"`
Patches []patch `yaml:"patches"`
}
type helmChart struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
} `yaml:"metadata"`
ReleaseNamespace string `yaml:"releaseNamespace"`
ChartHome string `yaml:"chartHome"`
ChartRepo string `yaml:"chartRepo"`
ChartName string `yaml:"chartName"`
ChartVersion string `yaml:"chartVersion"`
}
type VersionOutput struct {
QliksenseVersion string `yaml:"qlikSenseVersion"`
Images []string `yaml:"images"`
}
type nullWriter struct {
}
func (nw *nullWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
const (
defaultProfile = "docker-desktop"
defaultGitUrl = "https://github.com/qlik-oss/qliksense-k8s"
)
func (p *Qliksense) About(gitRef, profile string) (*VersionOutput, error) {
configDirectory, isTemporary, profile, err := getConfigDirectory(defaultGitUrl, gitRef, profile)
if err != nil {
return nil, err
}
if isTemporary {
defer os.RemoveAll(configDirectory)
}
chartVersion, err := getChartVersion(filepath.Join(configDirectory, "transformers", "qseokversion.yaml"), "qliksense")
if err != nil {
return nil, err
}
kuzManifest, err := executeKustomizeBuild(filepath.Join(configDirectory, "manifests", profile))
if err != nil {
return nil, err
}
images, err := getImageList(kuzManifest)
if err != nil {
return nil, err
}
return &VersionOutput{
QliksenseVersion: chartVersion,
Images: images,
}, nil
}
func getConfigDirectory(gitUrl, gitRef, profileEntered string) (dir string, isTemporary bool, profile string, err error) {
profile = profileEntered
if profile == "" {
profile = defaultProfile
}
if gitRef != "" {
if dir, err = downloadFromGitRepoToTmpDir(gitUrl, gitRef); err != nil {
return "", false, "", err
} else {
return dir, true, profile, nil
}
}
var exists bool
exists, dir, err = configExistsInCurrentDirectory(profile)
if err != nil {
return "", false, "", err
} else if exists {
return dir, false, profile, nil
}
var profileFromCR string
exists, dir, profileFromCR, err = configExistsInCR()
if err != nil {
return "", false, "", err
} else if exists {
if profileEntered == "" {
profile = profileFromCR
}
return dir, false, profile, nil
}
if dir, err = downloadFromGitRepoToTmpDir(gitUrl, "master"); err != nil {
return "", false, "", err
} else {
return dir, true, profile, nil
}
}
func downloadFromGitRepoToTmpDir(gitUrl, gitRef string) (string, error) {
if tmpDir, err := ioutil.TempDir("", ""); err != nil {
return "", err
} else {
downloadPath := path.Join(tmpDir, "repo")
if err := downloadFromGitRepo(gitUrl, gitRef, downloadPath); err != nil {
_ = os.RemoveAll(tmpDir)
return "", err
} else {
return downloadPath, nil
}
}
}
func downloadFromGitRepo(gitUrl, gitRef, destDir string) error {
if repo, err := kapis_git.CloneRepository(destDir, gitUrl, nil); err != nil {
return err
} else {
return kapis_git.Checkout(repo, gitRef, "", nil)
}
}
func configExistsInCurrentDirectory(profile string) (exists bool, currentDirectory string, err error) {
currentDirectory, err = os.Getwd()
if err == nil {
info, err := os.Stat(path.Join(currentDirectory, "manifests", profile))
if err == nil && info.IsDir() {
exists = true
}
}
return exists, currentDirectory, err
}
func configExistsInCR() (exists bool, directory string, profile string, err error) {
return exists, directory, profile, err
}
func getImageList(yamlContent []byte) ([]string, error) {
decoder := yaml.NewDecoder(bytes.NewReader(yamlContent))
var resource map[string]interface{}
imageMap := make(map[string]bool)
for {
err := decoder.Decode(&resource)
if err != nil {
if err != io.EOF {
return nil, err
}
break
}
traverseYamlDecodedMapRecursively(reflect.ValueOf(resource), []string{}, func(path []string, val interface{}) {
if len(path) >= 2 && path[len(path)-1] == "image" &&
(path[len(path)-2] == "containers" || path[len(path)-2] == "initContainers") {
if image, ok := val.(string); ok {
imageMap[image] = true
}
}
})
}
var sortedImageList []string
for image, _ := range imageMap {
sortedImageList = append(sortedImageList, image)
}
sort.Strings(sortedImageList)
return sortedImageList, nil
}
func traverseYamlDecodedMapRecursively(val reflect.Value, path []string, visitorFunc func(path []string, val interface{})) {
kind := val.Kind()
switch kind {
case reflect.Interface:
traverseYamlDecodedMapRecursively(val.Elem(), path, visitorFunc)
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
traverseYamlDecodedMapRecursively(val.Index(i), path, visitorFunc)
}
case reflect.Map:
for _, key := range val.MapKeys() {
traverseYamlDecodedMapRecursively(val.MapIndex(key), append(path, key.Interface().(string)), visitorFunc)
}
default:
if kind != reflect.Invalid {
visitorFunc(path, val.Interface())
}
}
}
func getChartVersion(versionFile, chartName string) (string, error) {
var patchInst patch
var selPatch selectivePatch
var chart helmChart
if bytes, err := ioutil.ReadFile(versionFile); err != nil {
return "", err
} else if err = yaml.Unmarshal(bytes, &selPatch); err != nil {
return "", err
}
for _, patchInst = range selPatch.Patches {
if err := yaml.Unmarshal([]byte(patchInst.Patch), &chart); err == nil {
if chart.ChartName == chartName {
return chart.ChartVersion, nil
}
}
}
return "", nil
}

505
pkg/qliksense/about_test.go Normal file
View File

@@ -0,0 +1,505 @@
package qliksense
import (
"fmt"
"os"
"path"
"reflect"
"testing"
)
func Test_About_getImageList(t *testing.T) {
var testCases = []struct {
name string
k8sYaml string
expectedImages []string
}{
{
name: "base",
k8sYaml: `
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: nginx-deployment-1
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
---
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: 2018-11-15T20:46:46Z
name: mysecret
namespace: default
resourceVersion: "7579"
uid: 91460ecb-e917-11e8-98f2-025000000001
type: Opaque
data:
username: YWRtaW5pc3RyYXRvcg==
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: nginx-deployment-2
spec:
selector:
matchLabels:
app: nginx
replicas: 2 # tells deployment to run 2 pods matching the template
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # has to match .spec.template.metadata.labels
serviceName: "nginx"
replicas: 3 # by default is 1
template:
metadata:
labels:
app: nginx # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "my-storage-class"
resources:
requests:
storage: 1Gi
---
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
---
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
containers:
- name: nginx
image: nginx
env:
- name: FOO
value: null
ports:
- containerPort: 80
volumeMounts:
- name: workdir
mountPath: /usr/share/nginx/html
# These containers are run during pod initialization
initContainers:
- name: install
image: busybox
command:
- wget
- "-O"
- "/work-dir/index.html"
- http://kubernetes.io
volumeMounts:
- name: workdir
mountPath: "/work-dir"
dnsPolicy: Default
volumes:
- name: workdir
emptyDir: {}
`,
expectedImages: []string{"busybox", "k8s.gcr.io/nginx-slim:0.8", "nginx", "nginx:1.7.9", "perl"},
},
{
name: "works for custom resources and CronJobs",
k8sYaml: `
apiVersion: "qixmanager.qlik.com/v1"
kind: "Engine"
metadata:
name: release-name-engine-reload
spec:
metadata:
labels:
qix-engine: qix-engine
annotations:
prometheus.io/scrape: "true"
workloadType: "reload"
podSpec:
imagePullSecrets:
- name: artifactory-docker-secret
dnsConfig:
options:
- name: timeout
value: "1"
- name: single-request-reopen
containers:
- name: engine-reload
image: another-engine
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox2
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
`,
expectedImages: []string{"another-engine", "busybox2"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
images, err := getImageList([]byte(testCase.k8sYaml))
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
if !reflect.DeepEqual(images, testCase.expectedImages) {
t.Fatalf("expected %v, but got: %v\n", testCase.expectedImages, images)
}
})
}
}
func Test_About_getConfigDirectory(t *testing.T) {
verifyAsdBranch := func(configDir string) (ok bool, reason string) {
tmpDir := os.TempDir()
if (path.Clean(path.Dir(path.Dir(configDir))) != path.Clean(tmpDir)) || (path.Base(configDir) != "repo") {
return false, fmt.Sprintf("expected config directory path: %v to be under: %v and terminate with repo", configDir, tmpDir)
}
if info, err := os.Stat(path.Join(configDir, "asdczxc")); err != nil || !info.Mode().IsRegular() {
return false, fmt.Sprintf(`expected to find file: "asdczxc" under directory: %v`, configDir)
}
return true, ""
}
verifyMasterBranch := func(configDir string) (ok bool, reason string) {
tmpDir := os.TempDir()
if (path.Clean(path.Dir(path.Dir(configDir))) != path.Clean(tmpDir)) || (path.Base(configDir) != "repo") {
return false, fmt.Sprintf("expected config directory path: %v to be under: %v and terminate with repo", configDir, tmpDir)
}
if _, err := os.Stat(path.Join(configDir, "asdczxc")); err == nil || !os.IsNotExist(err) {
return false, fmt.Sprintf(`expected to NOT find file: "asdczxc"" under directory: %v`, configDir)
}
if info, err := os.Stat(path.Join(configDir, "sad")); err != nil || !info.Mode().IsRegular() {
return false, fmt.Sprintf(`expected to find file: "sad"" under directory: %v`, configDir)
}
return true, ""
}
var testCases = []struct {
name string
setup func(t *testing.T) (gitUrl, gitRef, profileEntered string)
verify func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error)
cleanup func(configDir string) error
}{
{
name: "config in current directory and default profile",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
currentDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("error obtaining current directory: %v\n", err)
}
defaultProfilePath := path.Join(currentDirectory, "manifests", "docker-desktop")
err = os.MkdirAll(defaultProfilePath, os.ModePerm)
if err != nil {
t.Fatalf("error making path: %v, err: %v\n", defaultProfilePath, err)
}
return "no-clone-for-you", "", ""
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
currentDirectory, err := os.Getwd()
if err != nil {
return false, "", err
}
if configDir != currentDirectory {
return false, fmt.Sprintf("expected config directory: %v to equal current directory: %v", configDir, currentDirectory), nil
}
if isTemporary {
return false, "expected isTemporary to be false", nil
}
if profile != "docker-desktop" {
return false, fmt.Sprintf("expected profile to be: docker-desktop, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
if currentDirectory, err := os.Getwd(); err != nil {
return err
} else if err := os.RemoveAll(path.Join(currentDirectory, "manifests")); err != nil {
return err
}
return nil
},
},
{
name: "config in current directory and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
currentDirectory, err := os.Getwd()
if err != nil {
t.Fatalf("error obtaining current directory: %v\n", err)
}
profileEntered = "foo"
defaultProfilePath := path.Join(currentDirectory, "manifests", profileEntered)
err = os.MkdirAll(defaultProfilePath, os.ModePerm)
if err != nil {
t.Fatalf("error making path: %v, err: %v\n", defaultProfilePath, err)
}
return "no-clone-for-you", "", profileEntered
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
currentDirectory, err := os.Getwd()
if err != nil {
return false, "", err
}
if configDir != currentDirectory {
return false, fmt.Sprintf("expected config directory: %v to equal current directory: %v", configDir, currentDirectory), nil
}
if isTemporary {
return false, "expected isTemporary to be false", nil
}
if profile != "foo" {
return false, fmt.Sprintf("expected profile to be: foo, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
if currentDirectory, err := os.Getwd(); err != nil {
return err
} else if err := os.RemoveAll(path.Join(currentDirectory, "manifests")); err != nil {
return err
}
return nil
},
},
{
name: "config downloaded from git based on specific git ref and default profile used",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "asd", ""
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyAsdBranch(configDir)
if !ok {
return ok, reason, nil
}
if !isTemporary {
return false, "expected isTemporary to be true", nil
}
if profile != "docker-desktop" {
return false, fmt.Sprintf("expected profile to be: docker-desktop, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
return nil
},
},
{
name: "config downloaded from git based on specific git ref and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "asd", "foo"
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyAsdBranch(configDir)
if !ok {
return ok, reason, nil
}
if !isTemporary {
return false, "expected isTemporary to be true", nil
}
if profile != "foo" {
return false, fmt.Sprintf("expected profile to be: foo, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
return nil
},
},
{
name: "config downloaded from git from master branch and default profile used",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "", ""
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyMasterBranch(configDir)
if !ok {
return ok, reason, nil
}
if !isTemporary {
return false, "expected isTemporary to be true", nil
}
if profile != "docker-desktop" {
return false, fmt.Sprintf("expected profile to be: docker-desktop, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
return nil
},
},
{
name: "config downloaded from git from master branch and profile specified",
setup: func(t *testing.T) (gitUrl, gitRef, profileEntered string) {
return "https://github.com/test/HelloWorld", "", "foo"
},
verify: func(configDir string, isTemporary bool, profile string) (ok bool, reason string, err error) {
ok, reason = verifyMasterBranch(configDir)
if !ok {
return ok, reason, nil
}
if !isTemporary {
return false, "expected isTemporary to be true", nil
}
if profile != "foo" {
return false, fmt.Sprintf("expected profile to be: foo, but it was: %v", profile), nil
}
return true, "", nil
},
cleanup: func(configDir string) error {
tmpDir := os.TempDir()
if path.Clean(path.Dir(path.Dir(configDir))) == path.Clean(tmpDir) && path.Base(configDir) == "repo" {
tmpTmpDir := path.Dir(configDir)
if err := os.RemoveAll(tmpTmpDir); err != nil {
return err
}
}
return nil
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
configDirectory, isTemporary, profile, err := getConfigDirectory(testCase.setup(t))
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
if ok, reason, err := testCase.verify(configDirectory, isTemporary, profile); err != nil {
t.Fatalf("unexpected verification error: %v\n", err)
} else if !ok {
t.Fatalf("verification failed: %v\n", reason)
} else if err := testCase.cleanup(configDirectory); err != nil {
t.Fatalf("unexpected cleanup error: %v\n", err)
}
})
}
}

82
pkg/qliksense/config.go Normal file
View File

@@ -0,0 +1,82 @@
package qliksense
import (
"fmt"
"path/filepath"
"github.com/qlik-oss/k-apis/pkg/cr"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
"gopkg.in/yaml.v2"
)
const (
Q_INIT_CRD_PATH = "manifests/base/manifests/qliksense-init"
)
func (q *Qliksense) ConfigApplyQK8s() error {
//get the current context cr
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
return applyConfigToK8s(qcr)
}
func applyConfigToK8s(qcr *qapi.QliksenseCR) error {
// apply qliksense-init crd first
mroot := qcr.Spec.GetManifestsRoot()
qInitMsPath := filepath.Join(mroot, Q_INIT_CRD_PATH)
qInitByte, err := executeKustomizeBuild(qInitMsPath)
if err != nil {
fmt.Println("cannot generate crds for qliksense-init", err)
return err
}
if err = qapi.KubectlApply(string(qInitByte)); err != nil {
return err
}
// generate patches
cr.GeneratePatches(qcr.Spec)
// apply generated manifests
profilePath := filepath.Join(qcr.Spec.ManifestsRoot, qcr.Spec.Profile)
mByte, err := executeKustomizeBuild(profilePath)
if err != nil {
fmt.Println("cannot generate manifests for "+profilePath, err)
return err
}
if err = qapi.KubectlApply(string(mByte)); err != nil {
return err
}
return nil
}
func (q *Qliksense) ConfigViewCR() error {
//get the current context cr
r, err := q.getCurrentCRString()
if err != nil {
return err
}
fmt.Println(r)
return nil
}
func (q *Qliksense) getCurrentCRString() (string, error) {
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return "", err
}
out, err := yaml.Marshal(qcr)
if err != nil {
fmt.Println("cannot unmarshal cr ", err)
return "", err
}
return string(out), nil
}

View File

@@ -1,257 +1,411 @@
package qliksense
import (
"fmt"
"io"
"io/ioutil"
"os"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/registry"
"strings"
"golang.org/x/net/context"
yaml "gopkg.in/yaml.v2"
)
// Images ...
type Images struct {
Images []string `yaml:"images"`
}
// PullImages ...
func (p *Qliksense) PullImages(args []string) error {
var (
image string
err error
yamlVersion string
valid bool
images Images
)
println("getting images list...")
if yamlVersion, err = p.CallPorter(append([]string{"invoke", "--action", "about"}, args...),
func(x string) (out *string) {
if strings.HasPrefix(x, "qlikSenseVersion") {
valid = true
}
if strings.HasPrefix(x, "execution") {
valid = false
}
if valid {
return &x
}
return nil
}); err != nil {
return err
}
if err = yaml.Unmarshal([]byte(yamlVersion), &images); err != nil {
return err
}
for _, image = range images.Images {
if _, err = p.PullImage(image); err != nil {
fmt.Print(err)
}
println("---")
}
return nil
}
// PullImage ...
func (p *Qliksense) PullImage(imageName string) (map[string]string, error) {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pullOptions types.ImagePullOptions
ctx context.Context
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
// TODO: Create a real cli config context
ctx = context.Background()
if cli, err = command.NewDockerCli(); err != nil {
return nil, err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return nil, err
}
if ref, err = reference.ParseNormalizedNamed(imageName); err != nil {
return nil, err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return nil, err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return nil, err
}
pullOptions = types.ImagePullOptions{
RegistryAuth: encodedAuth,
}
if response, err = cli.Client().ImagePull(ctx, imageName, pullOptions); err != nil {
return nil, err
}
defer response.Close()
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
return nil, err
}
inspectData, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
if err != nil {
return nil, err
}
return inspectData.ContainerConfig.Labels, nil
}
// PullImage ...
func (p *Qliksense) TagAndPushImages(registry string) error {
var (
image string
err error
yamlVersion string
valid bool
images Images
)
if yamlVersion, err = p.CallPorter([]string{"invoke", "--action", "about"},
func(x string) (out *string) {
if strings.HasPrefix(x, "qlikSenseVersion") {
valid = true
}
if strings.HasPrefix(x, "execution") {
valid = false
}
if valid {
return &x
}
return nil
}); err != nil {
return err
}
if err = yaml.Unmarshal([]byte(yamlVersion), &images); err != nil {
return err
}
for _, image = range images.Images {
if err = p.TagAndPush(image, registry); err != nil {
fmt.Print(err)
}
println("---")
}
return nil
}
// PullImage ...
func (p *Qliksense) TagAndPush(image string, registryName string) error {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pushOptions types.ImagePushOptions
ctx context.Context
newName string
segments []string
imageList []types.ImageSummary
imageListOptions types.ImageListOptions
filterArgs filters.Args
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
// TODO: Create a real cli config context
ctx = context.Background()
if cli, err = command.NewDockerCli(); err != nil {
return err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return err
}
segments = strings.Split(image, "/")
if segments[0] == "docker.io" {
image = strings.Join(segments[1:], "/")
}
newName = registryName + "/" + segments[len(segments)-1]
filterArgs = filters.NewArgs()
filterArgs.Add("reference", image)
imageListOptions = types.ImageListOptions{
Filters: filterArgs,
}
if imageList, err = cli.Client().ImageList(ctx, imageListOptions); err != nil {
return err
}
if imageList == nil || len(imageList) <= 0 {
fmt.Printf("Use `qliksense pull`, to pull %v for an air gap push", newName)
return nil
}
if err = cli.Client().ImageTag(ctx, image, newName); err != nil {
return err
}
if ref, err = reference.ParseNormalizedNamed(image); err != nil {
return err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return err
}
pushOptions = types.ImagePushOptions{
All: true,
RegistryAuth: encodedAuth,
}
if response, err = cli.Client().ImagePush(ctx, newName, pushOptions); err != nil {
return err
}
defer response.Close()
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
return err
}
return nil
}
package qliksense
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports/alltransports"
imageTypes "github.com/containers/image/v5/types"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/registry"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"golang.org/x/net/context"
yaml "gopkg.in/yaml.v2"
)
// PullImages ...
func (p *Qliksense) PullImages(gitRef, profile string, engine bool) error {
var (
image, versionFile, imagesDir, homeDir string
err error
versionOut *VersionOutput
)
println("getting images list...")
// TODO: get getref and profile from config/cr for About function call
if versionOut, err = p.About(gitRef, profile); err != nil {
return err
}
if homeDir, err = homedir.Dir(); err != nil {
return err
}
imagesDir = filepath.Join(homeDir, ".qliksense", "images")
os.MkdirAll(imagesDir, os.ModePerm)
versionFile = filepath.Join(imagesDir, versionOut.QliksenseVersion)
if _, err = os.Stat(versionFile); err != nil {
if os.IsNotExist(err) {
if yamlVersion, err := yaml.Marshal(versionOut); err != nil {
return err
} else if err = ioutil.WriteFile(versionFile, yamlVersion, os.ModePerm); err != nil {
return err
}
} else {
return errors.Errorf("Unable to determine About file %v exists", versionFile)
}
}
for _, image = range versionOut.Images {
if _, err = p.PullImage(image, engine); err != nil {
fmt.Print(err)
}
println("---")
}
return nil
}
// PullImage ...
func (p *Qliksense) PullImage(imageName string, engine bool) (map[string]string, error) {
if engine {
return p.pullDockerImage(imageName)
}
return p.pullImage(imageName)
}
func (p *Qliksense) commandTimeoutContext(commandTimeout time.Duration) (context.Context, context.CancelFunc) {
ctx := context.Background()
var cancel context.CancelFunc = func() {}
if commandTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, commandTimeout)
}
return ctx, cancel
}
func (p *Qliksense) pullImage(imageName string) (map[string]string, error) {
var (
ctx context.Context
cancel context.CancelFunc
srcRef, destRef imageTypes.ImageReference
blobDir, targetDir, homeDir string
segments []string
nameTag []string
err error
policyContext *signature.PolicyContext
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if srcRef, err = alltransports.ParseImageName("docker://" + imageName); err != nil {
return nil, err
}
segments = strings.Split(imageName, "/")
nameTag = strings.Split(segments[len(segments)-1], ":")
if len(nameTag) < 2 {
nameTag = append(nameTag, "latest")
}
if homeDir, err = homedir.Dir(); err != nil {
return nil, err
}
targetDir = filepath.Join(homeDir, ".qliksense", "images", nameTag[0], nameTag[1])
fmt.Printf("==> Pulling image %v:%v", nameTag[0], nameTag[1])
fmt.Println()
os.MkdirAll(targetDir, os.ModePerm)
blobDir = filepath.Join(homeDir, ".qliksense", "blobs")
os.MkdirAll(blobDir, os.ModePerm)
if destRef, err = alltransports.ParseImageName("oci:" + targetDir); err != nil {
return nil, err
}
if policyContext, err = signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}); err != nil {
return nil, err
}
defer policyContext.Destroy()
_, err = copy.Image(ctx, policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: &imageTypes.SystemContext{
ArchitectureChoice: "amd64",
OSChoice: "linux",
},
DestinationCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: blobDir,
},
})
return nil, err
}
func (p *Qliksense) pullDockerImage(imageName string) (map[string]string, error) {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pullOptions types.ImagePullOptions
ctx context.Context
cancel context.CancelFunc
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if cli, err = command.NewDockerCli(); err != nil {
return nil, err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return nil, err
}
if ref, err = reference.ParseNormalizedNamed(imageName); err != nil {
return nil, err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return nil, err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return nil, err
}
pullOptions = types.ImagePullOptions{
RegistryAuth: encodedAuth,
}
if response, err = cli.Client().ImagePull(ctx, imageName, pullOptions); err != nil {
return nil, err
}
defer response.Close()
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
return nil, err
}
inspectData, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
if err != nil {
return nil, err
}
return inspectData.ContainerConfig.Labels, nil
}
//TagAndPushImages ...
func (p *Qliksense) TagAndPushImages(registry string, engine bool) error {
var (
image string
err error
yamlVersion string
images VersionOutput
)
if err = yaml.Unmarshal([]byte(yamlVersion), &images); err != nil {
return err
}
for _, image = range images.Images {
if err = p.TagAndPush(image, registry, engine); err != nil {
fmt.Print(err)
}
println("---")
}
return nil
}
func (p *Qliksense) directoryExists(path string) (exists bool, err error) {
if info, err := os.Stat(path); err != nil && os.IsNotExist(err) {
exists = false
err = nil
} else if err != nil && !os.IsNotExist(err) {
exists = false
} else if err == nil && info.IsDir() {
exists = true
} else if err == nil && !info.IsDir() {
exists = false
err = fmt.Errorf("path: %v is occupied by a file instead of a directory", path)
}
return exists, err
}
//TagAndPush ...
func (p *Qliksense) TagAndPush(image string, registryName string, engine bool) error {
if engine {
return p.tagAndDockerPush(image, registryName)
}
return p.tagAndPush(image, registryName)
}
func (p *Qliksense) tagAndPush(image string, registryName string) error {
var (
ctx context.Context
cancel context.CancelFunc
srcRef, destRef imageTypes.ImageReference
blobDir, srcDir, homeDir, newName string
segments []string
nameTag []string
err error
policyContext *signature.PolicyContext
srcExists bool
)
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
segments = strings.Split(image, "/")
nameTag = strings.Split(segments[len(segments)-1], ":")
if len(nameTag) < 2 {
nameTag = append(nameTag, "latest")
}
if homeDir, err = homedir.Dir(); err != nil {
return err
}
srcDir = filepath.Join(homeDir, ".qliksense", "images", nameTag[0], nameTag[1])
if srcExists, err = p.directoryExists(srcDir); err != nil {
return err
}
if !srcExists {
if _, err = p.PullImage(image, false); err != nil {
return err
}
}
if srcRef, err = alltransports.ParseImageName("oci:" + srcDir); err != nil {
return err
}
if segments[0] == "docker.io" {
image = strings.Join(segments[1:], "/")
}
newName = "//" + registryName + "/" + segments[len(segments)-1]
fmt.Printf("==> Tag and push image to %v", newName)
fmt.Println()
if destRef, err = alltransports.ParseImageName("docker:" + newName); err != nil {
return err
}
if policyContext, err = signature.NewPolicyContext(&signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}); err != nil {
return err
}
defer policyContext.Destroy()
blobDir = filepath.Join(homeDir, ".qliksense", "blobs")
os.MkdirAll(blobDir, os.ModePerm)
_, err = copy.Image(ctx, policyContext, destRef, srcRef, &copy.Options{
ReportWriter: os.Stdout,
SourceCtx: &imageTypes.SystemContext{
OCISharedBlobDirPath: blobDir,
},
DestinationCtx: &imageTypes.SystemContext{
DockerDaemonInsecureSkipTLSVerify: true,
},
})
return err
}
// PullImage ...
func (p *Qliksense) tagAndDockerPush(image string, registryName string) error {
var (
cli *command.DockerCli
dockerOutput io.Writer
response io.ReadCloser
pushOptions types.ImagePushOptions
ctx context.Context
cancel context.CancelFunc
newName string
segments []string
imageList []types.ImageSummary
imageListOptions types.ImageListOptions
filterArgs filters.Args
ref reference.Named
repoInfo *registry.RepositoryInfo
authConfig types.AuthConfig
encodedAuth string
termFd uintptr
err error
)
// TODO: Create a real cli config context
ctx, cancel = p.commandTimeoutContext(0)
defer cancel()
if cli, err = command.NewDockerCli(); err != nil {
return err
}
if err = cli.Initialize(cliflags.NewClientOptions()); err != nil {
return err
}
segments = strings.Split(image, "/")
if segments[0] == "docker.io" {
image = strings.Join(segments[1:], "/")
}
newName = registryName + "/" + segments[len(segments)-1]
filterArgs = filters.NewArgs()
filterArgs.Add("reference", image)
imageListOptions = types.ImageListOptions{
Filters: filterArgs,
}
if imageList, err = cli.Client().ImageList(ctx, imageListOptions); err != nil {
return err
}
if imageList == nil || len(imageList) <= 0 {
fmt.Printf("Use `qliksense pull`, to pull %v for an air gap push", newName)
return nil
}
if err = cli.Client().ImageTag(ctx, image, newName); err != nil {
return err
}
if ref, err = reference.ParseNormalizedNamed(image); err != nil {
return err
}
if repoInfo, err = registry.ParseRepositoryInfo(ref); err != nil {
return err
}
authConfig = command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
if encodedAuth, err = command.EncodeAuthToBase64(authConfig); err != nil {
return err
}
pushOptions = types.ImagePushOptions{
All: true,
RegistryAuth: encodedAuth,
}
if response, err = cli.Client().ImagePush(ctx, newName, pushOptions); err != nil {
return err
}
defer response.Close()
dockerOutput = ioutil.Discard
// if b.IsVerbose() {
// dockerOutput = b.Out
// }
dockerOutput = os.Stdout
termFd, _ = term.GetFdInfo(dockerOutput)
// Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only)
// when Term is true.
isTerm := false
if err = jsonmessage.DisplayJSONMessagesStream(response, dockerOutput, termFd, isTerm, nil); err != nil {
return err
}
return nil
}

10
pkg/qliksense/fetch.go Normal file
View File

@@ -0,0 +1,10 @@
package qliksense
import (
"fmt"
)
func (q *Qliksense) FetchQK8s(version string) {
//io.WriteString(os.Stdout, q.GetCRDString())
fmt.Println(version)
}

52
pkg/qliksense/install.go Normal file
View File

@@ -0,0 +1,52 @@
package qliksense
import (
"fmt"
qapi "github.com/qlik-oss/sense-installer/pkg/api"
)
func (q *Qliksense) InstallQK8s(version string) error {
// step1: fetch 1.0.0 # pull down qliksense-k8s@1.0.0
// step2: operator view | kubectl apply -f # operator manifest (CRD)
// step3: config apply | kubectl apply -f # generates patches (if required) in configuration directory, applies manifest
// step4: config view | kubectl apply -f # generates Custom Resource manifest (CR)
//io.WriteString(os.Stdout, q.GetCRDString())
//fmt.Println(version)
fmt.Println("Fetching " + version)
//qConfig := qapi.NewQConfig(q.QliksenseHome)
//qcr, err := qConfig.GetCurrentCR()
//TODO: may need to check if CRD already installed, but doing apply does not hurt for now
//install crd into cluster
fmt.Println("Installing operator CRD")
if err := qapi.KubectlApply(q.GetCRDString()); err != nil {
fmt.Println("cannot do kubectl apply on opeartor CRD", err)
return err
}
// install generated manifests into cluster
fmt.Println("Installing generated manifests into cluster")
qConfig := qapi.NewQConfig(q.QliksenseHome)
qcr, err := qConfig.GetCurrentCR()
if err != nil {
fmt.Println("cannot get the current-context cr", err)
return err
}
if err := applyConfigToK8s(qcr); err != nil {
fmt.Println("cannot do kubectl apply on manifests")
return err
}
// install operator cr into cluster
//get the current context cr
fmt.Println("Install operator CR into cluster")
r, err := q.getCurrentCRString()
if err != nil {
return err
}
if err := qapi.KubectlApply(r); err != nil {
fmt.Println("cannot do kubectl apply on operator CR")
}
return nil
}

32
pkg/qliksense/kuz.go Normal file
View File

@@ -0,0 +1,32 @@
package qliksense
import (
"log"
"os"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/types"
)
func executeKustomizeBuild(directory string) ([]byte, error) {
log.SetOutput(&nullWriter{})
defer func() {
log.SetOutput(os.Stderr)
}()
fSys := filesys.MakeFsOnDisk()
options := &krusty.Options{
DoLegacyResourceSort: false,
LoadRestrictions: types.LoadRestrictionsNone,
DoPrune: false,
PluginConfig: konfig.DisabledPluginConfig(),
}
k := krusty.MakeKustomizer(fSys, options)
resMap, err := k.Run(directory)
if err != nil {
return nil, err
}
return resMap.AsYaml()
}

71
pkg/qliksense/kuz_test.go Normal file
View File

@@ -0,0 +1,71 @@
package qliksense
import (
"io/ioutil"
"os"
"path"
"testing"
kapis_git "github.com/qlik-oss/k-apis/pkg/git"
)
func Test_executeKustomizeBuild(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
defer os.RemoveAll(tmpDir)
kustomizationYamlFilePath := path.Join(tmpDir, "kustomization.yaml")
kustomizationYaml := `
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: foo-config
literals:
- foo=bar
`
err = ioutil.WriteFile(kustomizationYamlFilePath, []byte(kustomizationYaml), os.ModePerm)
if err != nil {
t.Fatalf("error writing kustomization file to path: %v error: %v\n", kustomizationYamlFilePath, err)
}
result, err := executeKustomizeBuild(tmpDir)
if err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
expectedK8sYaml := `apiVersion: v1
data:
foo: bar
kind: ConfigMap
metadata:
name: foo-config
`
if string(result) != expectedK8sYaml {
t.Fatalf("expected k8s yaml: [%v] but got: [%v]\n", expectedK8sYaml, string(result))
}
}
func Test_executeKustomizeBuild_onQlikConfig_DISABLED(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
defer os.RemoveAll(tmpDir)
configPath := path.Join(tmpDir, "config")
if repo, err := kapis_git.CloneRepository(configPath, defaultGitUrl, nil); err != nil {
t.Fatalf("unexpected error: %v\n", err)
} else if err := kapis_git.Checkout(repo, "v1.21.23-edge", "", nil); err != nil {
t.Fatalf("unexpected error: %v\n", err)
}
if _, err := executeKustomizeBuild(path.Join(configPath, "manifests", "base")); err != nil {
t.Fatalf("unexpected kustomize error: %v\n", err)
}
}

40
pkg/qliksense/operator.go Normal file
View File

@@ -0,0 +1,40 @@
package qliksense
import (
"fmt"
"io"
"os"
"strings"
)
func (q *Qliksense) ViewOperatorCrd() {
io.WriteString(os.Stdout, q.GetCRDString())
}
// this will return crd,deployment,role, rolebinding,serviceaccount for operator
func (q *Qliksense) GetCRDString() string {
result := ""
for _, v := range q.getFileList("crd") {
result = q.getYamlFromPackrFile(v)
}
for _, v := range q.getFileList("crd-deploy") {
result = result + q.getYamlFromPackrFile(v)
}
return result
}
func (q *Qliksense) getYamlFromPackrFile(packrFile string) string {
s, err := q.CrdBox.FindString(packrFile)
if err != nil {
fmt.Printf("Cannot read file %s", packrFile)
}
return fmt.Sprintln("#soruce: " + packrFile + "\n\n" + s + "\n---")
}
func (q *Qliksense) getFileList(resourceType string) []string {
var resList []string
for _, v := range q.CrdBox.List() {
if strings.Contains(v, resourceType+"/") {
resList = append(resList, []string{v}...)
}
}
return resList
}

View File

@@ -1,59 +0,0 @@
package qliksense
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
)
// ProcessLine ...
type ProcessLine func(string) *string
// CallPorter ...
func (p *Qliksense) CallPorter(args []string, processor ProcessLine) (string, error) {
var (
outText string
cmd *exec.Cmd
err error
output io.ReadCloser
scanner *bufio.Scanner
done chan struct{}
)
cmd = exec.Command(p.PorterExe, args[:]...)
if output, err = cmd.StdoutPipe(); err != nil {
return "", err
}
cmd.Stderr = os.Stderr
done = make(chan struct{})
scanner = bufio.NewScanner(output)
go func() {
for scanner.Scan() {
var text string
var newText *string
text = scanner.Text()
if processor != nil {
newText = processor(text)
if newText != nil {
outText = outText + fmt.Sprintln(*newText)
}
} else {
outText = outText + fmt.Sprintln(text)
}
}
done <- struct{}{}
}()
if err = cmd.Start(); err != nil {
return "", err
}
<-done
if err = cmd.Wait(); err != nil {
return "", err
}
if err = scanner.Err(); err != nil {
return "", err
}
return outText, nil
}

View File

@@ -1,15 +1,20 @@
package qliksense
// Qliksense is the logic behind the qliksense client
type Qliksense struct {
PorterExe string
QliksenseHome string
}
// New qliksense client, initialized with useful defaults.
func New(porterExe, qliksenseHome string) *Qliksense {
return &Qliksense{
PorterExe: porterExe,
QliksenseHome: qliksenseHome,
}
}
//go:generate packr2
package qliksense
import (
"github.com/gobuffalo/packr/v2"
)
// Qliksense is the logic behind the qliksense client
type Qliksense struct {
QliksenseHome string
CrdBox *packr.Box
}
// New qliksense client, initialized with useful defaults.
func New(qliksenseHome string) *Qliksense {
return &Qliksense{
QliksenseHome: qliksenseHome,
CrdBox: packr.New("crds", "./crds"),
}
}

View File

@@ -1,8 +1,8 @@
package pkg
// These are build-time values, set during an official release
var (
Commit string
Version string
CommitDate string
)
package pkg
// These are build-time values, set during an official release
var (
Commit string
Version string
CommitDate string
)

View File

@@ -1,39 +0,0 @@
FROM quay.io/deis/lightweight-docker-go:v0.2.0
FROM debian:stretch as base
# This is a template Dockerfile for the bundle's invocation image
# You can customize it to use different base images, install tools and copy configuration files.
#
# Porter will use it as a template and append lines to it for the mixins
# and to set the CMD appropriately for the CNAB specification.
#
# Add the following line to porter.yaml to instruct Porter to use this template
# dockerfile: Dockerfile.tmpl
# You can control where the mixin's Dockerfile lines are inserted into this file by moving "# PORTER_MIXINS" line
# another location in this file. If you remove that line, the mixins generated content is appended to this file.
# PORTER_MIXINS
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ARG BUNDLE_DIR
ARG HELM_VERSION=v2.16.0
RUN apt-get update && \
apt-get install -y curl && \
curl -o helm.tgz https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz && \
tar -xzf helm.tgz && \
mv linux-amd64/helm /usr/local/bin && \
rm helm.tgz
RUN helm init --client-only
# CI job will update this version and make a PR
ARG QSEOK_VERSION=1.12.89
# This is likely not needed
ARG QLIKSENSE_INIT_VERSION=1.1.0
RUN mkdir -p /tmp/.chartcache
RUN helm repo add qlik-edge https://qlik.bintray.com/edge
RUN helm fetch qlik-edge/qliksense-init --version $QLIKSENSE_INIT_VERSION --untar -d /tmp/.chartcache/
# This is now done by qliksense mixin
#RUN helm fetch qlik-edge/qliksense --version $QSEOK_VERSION --untar -d /tmp/.chartcache/
# Use the BUNDLE_DIR build argument to copy files into the bundle
COPY . $BUNDLE_DIR

View File

@@ -1,42 +0,0 @@
name: test-version
version: 0.1.0
description: "An example Porter bundle demonstrating exec mixin outputs"
invocationImage: qlik/test-version:0.1.0
tag: qlik/test-version-bundle:0.1.0
dockerfile: Dockerfile.tmpl
mixins:
- exec
- qliksense
customActions:
about:
description: "About QLiksense"
stateless: true
modifies: true
install:
- exec:
description: "Get the qliksense version"
command: bash
arguments:
- -c
- "echo 1.2.13"
outputs:
- name: version
regex: '.*'
uninstall:
- exec:
description: "Get the qliksense version"
command: bash
arguments:
- -c
- "echo 1.2.13"
outputs:
- name: version
regex: '.*'
about:
- qliksense:
description: "Use bundled version by default"
version: "bundled"