diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e623fe1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,58 +0,0 @@ -language: go -services: -- docker - - -go: - - '1.12' - -go_import_path: github.com/IBM/multicloud-operators-subscription-release - -git: - depth: false - -branches: - only: - - master - # release tags - - /^v\d+\.\d+\.\d+.*/ - -stages: - - lint - - test - - build - - multiarch - -# install: -# - git clone https://github.com/IBM/multicloud-operators-subscription-release.git multicloud-operators-subscription-release -# # - git clone https://github.com/itdove/multicloud-operators-subscription-release.git multicloud-operators-subscription-release -# - cd multicloud-operators-subscription-release -# # - git checkout -qf $BUILD_COMMIT - -before_script: - - source travis-env.sh - # - make init - # - make docker:login - -jobs: - include: - - stage: lint - name: Perform linting before any jobs trigger - script: - - make lint fmt - - - stage: test - name: Test x86 image - os: linux - script: - - make test - -cache: - directories: - - $GOPATH/pkg - - $GOPATH/bin - - -notifications: - email: - on_success: never diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..91edf03 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Contributing guidelines](#contributing-guidelines) + - [Sign the CLA](#sign-the-cla) + - [Contributing A Patch](#contributing-a-patch) + - [Issue and Pull Request Management](#issue-and-pull-request-management) + - [Pre-check before submitting a PR](#pre-check-before-submitting-a-pr) + - [Build and push images](#build-and-push-images) + + + +# Contributing guidelines + +## Sign the CLA + +IBM cloud projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests. Please see xxx for more info + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The [repo owners](OWNERS) will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Submit a pull request. + +## Issue and Pull Request Management + +Anyone may comment on issues and submit reviews for pull requests. However, in +order to be assigned an issue or pull request, you must be a member of the +[IBM](https://github.com/ibm) GitHub organization. + +Repo maintainers can assign you an issue or pull request by leaving a +`/assign ` comment on the issue or pull request. + +## Pre-check before submitting a PR + +After your PR is ready to commit, please run following commands to check your code: + +```shell +make check +make test +``` + +## Build and push images + +Make sure your code build passed: + +```shell +export REGISTRY= +make build-push-images +``` + +Now, you can follow the [getting started guide](./README.md#getting-started) to work with the xxx. diff --git a/Configfile b/Configfile deleted file mode 100644 index 325a0fc..0000000 --- a/Configfile +++ /dev/null @@ -1,70 +0,0 @@ -############################################################################### -# Licensed Materials - Property of IBM Copyright IBM Corporation 2019. All Rights Reserved. -# U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP -# Schedule Contract with IBM Corp. -# -# Contributors: -# IBM Corporation - initial API and implementation -############################################################################### - -GIT_REMOTE_URL = $(shell git config --get remote.origin.url) -DOCKER_SERVER ?= hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com -DOCKER_USER ?= token -DOCKER_PASS ?= -IMAGE_REPO ?= hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com/ibmcom -RELEASE_TAG ?= latest -ARCH = $(shell uname -m) -DOCKER_FLAG = - -# This part of configurations are used by build-harness, DONOT remove them -DOCKER_REGISTRY ?= hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com -DOCKER_NAMESPACE ?= ibmcom -DOCKER_TAG ?= latest -# Variables for Red Hat required labels -IMAGE_NAME ?= multicloud-operators-subscription-release -IMAGE_DESCRIPTION = This_image_contains_the_multicloud_operators_subscription_release_for_processing_new_CRs_based_on_the_CRD -IMAGE_MAINTAINER = dominique.vernier@us.ibm.com -IMAGE_VENDOR = IBM -IMAGE_SUMMARY = $(IMAGE_DESCRIPTION) -IMAGE_OPENSHIFT_TAGS = multicloud-manager -IMAGE_VERSION ?= $(RELEASE_TAG) -IMAGE_RELEASE ?= $(VCS_REF) - -DOCKER_BUILD_OPTS = --build-arg VCS_REF=$(VCS_REF) \ - --build-arg VCS_URL=$(GIT_REMOTE_URL) \ - --build-arg IMAGE_NAME=$(IMAGE_NAME) \ - --build-arg IMAGE_MAINTAINER=$(IMAGE_MAINTAINER) \ - --build-arg IMAGE_VENDOR=$(IMAGE_VENDOR) \ - --build-arg IMAGE_VERSION=$(IMAGE_VERSION) \ - --build-arg IMAGE_RELEASE=$(IMAGE_RELEASE) \ - --build-arg IMAGE_SUMMARY=$(IMAGE_SUMMARY) \ - --build-arg IMAGE_OPENSHIFT_TAGS=$(IMAGE_OPENSHIFT_TAGS) \ - --build-arg IMAGE_NAME_ARCH=$(IMAGE_NAME_ARCH) \ - --build-arg IMAGE_DESCRIPTION=$(IMAGE_DESCRIPTION) - -GIT_COMMIT := $(shell git rev-parse --short HEAD) -VCS_REF := $(if $(WORKING_CHANGES),$(GIT_COMMIT)-$(BUILD_DATE),$(GIT_COMMIT)) -APP_VERSION ?= $(if $(shell cat VERSION 2> /dev/null),$(shell cat VERSION 2> /dev/null),0.0.1) -IMAGE_VERSION ?= $(APP_VERSION)-$(GIT_COMMIT) -DOCKER_IMAGE =$(IMAGE_NAME) -DOCKER_BUILD_TAG = $(IMAGE_VERSION) -DOCKER_URI = $(IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(RELEASE_TAG) -# End of the build-harness configurations - -# Push image to integration repo, integration is release repo when install run cicd build -RELEASE_IMAGE_REPO ?= hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com/ibmcom -IMAGE_NAME_ARCH = $(IMAGE_NAME)-$(ARCH) -GITHUB_USER ?= -GITHUB_TOKEN ?= -ARTIFACTORY_USER ?= $(DOCKER_USER) -ARTIFACTORY_TOKEN ?= $(DOCKER_PASS) - -ifeq ($(ARCH), x86_64) - IMAGE_NAME_ARCH = $(IMAGE_NAME)-amd64 -endif - -DEFAULT_S390X_IMAGE ?= ibmcom/pause-s390x:3.0 -IMAGE_NAME_S390X ?= ${IMAGE_REPO}/${IMAGE_NAME}-s390x:${RELEASE_TAG} - -DEFAULT_PPC64LE_IMAGE ?= ibmcom/pause-ppc64le:3.0 -IMAGE_NAME_PPC64LE ?= ${IMAGE_REPO}/${IMAGE_NAME}-ppc64le:${RELEASE_TAG} \ No newline at end of file diff --git a/Makefile b/Makefile index 96173ec..77f25c7 100644 --- a/Makefile +++ b/Makefile @@ -1,137 +1,144 @@ -############################################################################### -# Licensed Materials - Property of IBM. -# Copyright IBM Corporation 2019. All Rights Reserved. -# U.S. Government Users Restricted Rights - Use, duplication or disclosure -# restricted by GSA ADP Schedule Contract with IBM Corp. +# Copyright 2019 The Kubernetes Authors. # -# Contributors: -# IBM Corporation - initial API and implementation -############################################################################### +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This repo is build locally for dev/test by default; +# Override this variable in CI env. +BUILD_LOCALLY ?= 1 + +# Image URL to use all building/pushing image targets; +# Use your own docker registry and image name for dev/test by overridding the IMG and REGISTRY environment variable. +IMG ?= multicloud-operators-subscription-release +REGISTRY ?= quay.io/multicloudlab + +# Github host to use for checking the source tree; +# Override this variable ue with your own value if you're working on forked repo. +GIT_HOST ?= github.com/IBM + +PWD := $(shell pwd) +BASE_DIR := $(shell basename $(PWD)) + +# Keep an existing GOPATH, make a private one if it is undefined +GOPATH_DEFAULT := $(PWD)/.go +export GOPATH ?= $(GOPATH_DEFAULT) +GOBIN_DEFAULT := $(GOPATH)/bin +export GOBIN ?= $(GOBIN_DEFAULT) +TESTARGS_DEFAULT := "-v" +export TESTARGS ?= $(TESTARGS_DEFAULT) +DEST := $(GOPATH)/src/$(GIT_HOST)/$(BASE_DIR) +VERSION ?= $(shell git describe --exact-match 2> /dev/null || \ + git describe --match=$(git rev-parse --short=8 HEAD) --always --dirty --abbrev=8) + +LOCAL_OS := $(shell uname) +ifeq ($(LOCAL_OS),Linux) + TARGET_OS ?= linux + XARGS_FLAGS="-r" +else ifeq ($(LOCAL_OS),Darwin) + TARGET_OS ?= darwin + XARGS_FLAGS= +else + $(error "This system's OS $(LOCAL_OS) isn't recognized/supported") +endif -include Configfile +.PHONY: all work fmt check coverage lint test build images build-push-images -PROJECT_NAME := $(shell basename $(CURDIR)) +all: fmt check test coverage build images -.PHONY: init\: -init:: - @mkdir -p variables -ifndef GITHUB_USER - $(info GITHUB_USER not defined) - exit -1 -endif - $(info Using GITHUB_USER=$(GITHUB_USER)) -ifndef GITHUB_TOKEN - $(info GITHUB_TOKEN not defined) - exit -1 -endif +# ifneq ("$(realpath $(DEST))", "$(realpath $(PWD))") +# $(error Please run 'make' from $(DEST). Current directory is $(PWD)) +# endif + +include common/Makefile.common.mk +# include Makefile.local + +############################################################ +# work section +############################################################ +$(GOBIN): + @echo "create gobin" + @mkdir -p $(GOBIN) + +work: $(GOBIN) + +############################################################ +# format section +############################################################ + +# All available format: format-go format-protos format-python +# Default value will run all formats, override these make target with your requirements: +# eg: fmt: format-go format-protos +fmt: format-go format-protos format-python + +############################################################ +# check section +############################################################ -ifdef BUILD_WITH_HARNESS --include $(shell curl -fso .build-harness -H "Authorization: token ${GITHUB_TOKEN}" -H "Accept: application/vnd.github.v3.raw" "https://raw.github.ibm.com/ICP-DevOps/build-harness/master/templates/Makefile.build-harness"; echo .build-harness) -endif - -BINDIR ?= bin -BUILD_DIR ?= build -SC_PKG = github.com/IBM/multicloud-operators-subscription-release/subscription -TYPES_FILES = $(shell find pkg/apis -name types.go) -GOOS = $(shell go env GOOS) -GOARCH = $(shell go env GOARCH) -OPERATOR_SDK_RELEASE=v0.10.0 - -.PHONY: lint -lint: - GO111MODULE=off go get golang.org/x/lint/golint - # go get -u github.com/alecthomas/gometalinter - # gometalinter --install - golint -set_exit_status=true pkg/controller/... - golint -set_exit_status=true cmd/... - -.PHONY: deps -deps: - go mod tidy - -.PHONY: copyright-check -copyright-check: - $(BUILD_DIR)/copyright-check.sh - -all: deps test image - -local: - operator-sdk up local --verbose - -ossc: - # @if [ -z $(dest) ]; then \ - # echo "Usage: make dest=destination_dir wicked"; \ - # exit 1; \ - # fi - # rm -rf /tmp/awsom-tool; mkdir -p /tmp/awsom-tool; cd /tmp; git clone https://github.ibm.com/IBMPrivateCloud/awsom-tool --depth 1; cd awsom-tool; make local; cd $(CURDIR) - rm -rf $(dest)/$(PROJECT_NAME)_scan-results && \ - mkdir -p $(dest)/$(PROJECT_NAME)_scan-results && \ - /tmp/awsom-tool/_build/awsomtool golang scan -o $(dest)/$(PROJECT_NAME)_scan-results/Scan-Report.csv && \ - /tmp/awsom-tool/_build/awsomtool enrichCopyright -w $(dest)/$(PROJECT_NAME)_scan-results/Scan-Report.csv -o $(dest)/$(PROJECT_NAME)_scan-results/Scan-Report-url-copyright.csv && \ - rm -rf wicked_cli.log - -check-licenses: - @rm -rf /tmp/awsom-tool; mkdir -p /tmp/awsom-tool; cd /tmp; git clone https://github.ibm.com/IBMPrivateCloud/awsom-tool --depth 1; cd awsom-tool; make local; cd $(CURDIR) - @$(eval RESULT = $(shell /tmp/awsom-tool/_build/awsomtool golang licenses -p .*GPL.* --format '{{.Path}} {{.LicenseType}}')) - @if [ "$(RESULT)" != "" ]; then \ - echo "A License file contains the GPL word"; \ - echo -e $(RESULT); \ - exit 1; \ - fi - -# Install operator-sdk -operator-sdk-install: - @operator-sdk version ; \ - if [ $$? -ne 0 ]; then \ - ./build/install-operator-sdk.sh; \ - fi - -image: operator-sdk-install generate - $(info Building operator) - $(info --IMAGE: $(DOCKER_IMAGE)) - $(info --TAG: $(DOCKER_BUILD_TAG)) - operator-sdk build $(IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(IMAGE_VERSION) --image-build-args "$(DOCKER_BUILD_OPTS)" - uname -a | grep "Darwin"; \ - if [ $$? -eq 0 ]; then \ - sed -i "" 's|REPLACE_IMAGE|$(IMAGE_REPO)/$(IMAGE_NAME):${RELEASE_TAG}|g' deploy/operator.yaml; \ - else \ - sed -i 's|REPLACE_IMAGE|$(IMAGE_REPO)/$(IMAGE_NAME):${RELEASE_TAG}|g' deploy/operator.yaml; \ - fi +check: lint + +# All available linters: lint-dockerfiles lint-scripts lint-yaml lint-copyright-banner lint-go lint-python lint-helm lint-markdown lint-sass lint-typescript lint-protos +# Default value will run all linters, override these make target with your requirements: +# eg: lint: lint-go lint-yaml +lint: lint-all + +############################################################ +# test section +############################################################ + +test: + @go test ${TESTARGS} ./... + +############################################################ +# coverage section +############################################################ + +coverage: + @common/scripts/codecov.sh ${BUILD_LOCALLY} + +############################################################ +# generate code section +############################################################ generate: operator-sdk-install operator-sdk generate k8s operator-sdk generate openapi -release: image - @echo -e "$(TARGET) $(OS) $(ARCH)" - @$(SELF) -s docker:tag DOCKER_IMAGE=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH) DOCKER_BUILD_TAG=$(IMAGE_VERSION) DOCKER_URI=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(RELEASE_TAG) - @$(SELF) -s docker:push DOCKER_URI=$(RELEASE_IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(RELEASE_TAG) - @$(SELF) -s docker:tag DOCKER_IMAGE=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH) DOCKER_BUILD_TAG=$(IMAGE_VERSION) DOCKER_URI=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH):latest - @$(SELF) -s docker:push DOCKER_URI=$(RELEASE_IMAGE_REPO)/$(IMAGE_NAME_ARCH):latest - -ifeq ($(ARCH), x86_64) -ifneq ($(RELEASE_TAG),) - @$(SELF) -s docker:tag DOCKER_IMAGE=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH) DOCKER_BUILD_TAG=$(IMAGE_VERSION) DOCKER_URI=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(RELEASE_TAG)-rhel - @$(SELF) -s docker:push DOCKER_URI=$(RELEASE_IMAGE_REPO)/$(IMAGE_NAME_ARCH):$(RELEASE_TAG)-rhel -endif - @$(SELF) -s docker:tag DOCKER_IMAGE=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH) DOCKER_BUILD_TAG=$(IMAGE_VERSION) DOCKER_URI=$(IMAGE_REPO)/$(IMAGE_NAME_ARCH):latest-rhel - @$(SELF) -s docker:push DOCKER_URI=$(RELEASE_IMAGE_REPO)/$(IMAGE_NAME_ARCH):latest-rhel -endif +############################################################ +# build section +############################################################ -# Run tests -test: generate fmt vet -# This skip the controller test as they are no working - go test `go list ./pkg/... ./cmd/... | grep -v pkg/controller` -coverprofile=cover.out +build: + # @common/scripts/gobuild.sh go-repo-template ./cmd/manager -# Run tests with debug output -testdebug: generate fmt vet - go test ./pkg/... ./cmd/... -coverprofile=cover.out -v +############################################################ +# images section +############################################################ -# Run go fmt against code -fmt: - go fmt ./pkg/... ./cmd/... +images: build build-push-images -# Run go vet against code -vet: - go vet ./pkg/... ./cmd/... +ifeq ($(BUILD_LOCALLY),0) + export CONFIG_DOCKER_TARGET = config-docker +endif + +build-push-images: $(CONFIG_DOCKER_TARGET) + @operator-sdk build $(REGISTRY)/$(IMG):$(VERSION) + @docker tag $(REGISTRY)/$(IMG):$(VERSION) $(REGISTRY)/$(IMG):latest +ifeq ($(BUILD_LOCALLY),0) + @docker push $(REGISTRY)/$(IMG):$(VERSION) + @docker push $(REGISTRY)/$(IMG):latest +endif +############################################################ +# clean section +############################################################ +clean: + rm -f go-repo-template diff --git a/README.md b/README.md index f2ed182..2486a56 100644 --- a/README.md +++ b/README.md @@ -1,189 +1,7 @@ -# multicloud-operators-subscription-release +

+ +Build Status + Go Report Card Code Coverage

-The multicloud-operators-subscription-release is composed of 2 controllers. The helmchartsubscription controller which is in charge of managing the helmchartsubscription CR. That CR defines the location of the charts (helmrepo or github) and filters to select a subset of charts to deploy. the helmchartsubscription controller will then create a number of helmreleases and these are managed by the helmrelease controller. The helmrelease controller will manage the helmrelease CR, download the chart from the helmrepo or github and then call the operator-sdk helm-operator methods to start the deployment of each chart. - -The helmrelease controller can be use independently without the helmchartsubscription controller. The flag `--helmchart-subscription-controller-disabled` can be used to disable the helmchartsubscription controller. - -## Environment variable - -The environment variable `CHARTS_DIR` must be set when developing, it specifies the directory where the charts will be downloaded and expanded. - -## Launch Dev mode -``` -operator-sdk up local --verbose [--operator-flags "--helmchart-subscription-controller-disabled"] -``` - -## Build - -operator-sdk build ibm/multicloud-operators-subscription-release:latest -docker tag ibm/multicloud-operators-subscription-release:latest mycluster.icp:8500/kube-system/ibm/multicloud-operators-subscription-release:latest -docker push mycluster.icp:8500/kube-system/ibm/multicloud-operators-subscription-release:latest - -## Environment deployment - -TODO to improve for ClusterRole - -1) Do `kubectl apply -f` on all files in deploy/crds.*-crd.yaml -2) `kubectl apply -f service_account.yaml` -3) `kubectl apply -f role.yaml` -4) `kubectl apply -f role_binding.yaml` -5) `kubectl apply -f operator.yaml` - -## General process -The operator generates `HelmRelease` CR for each chart to deploy in the same namespace and named `-[-]`. The channel_name is added only if the channel attribute is set in the subscription. - -To do so, the following steps are taken: - -1) Read the index.yaml at the source address. -2) Filter the index.yaml with the spec.Name and spec.packageFilter. -3) Take the last version of a chart if multiple version are still present for the same chart after filtering. -4) Create a HelmRelease for each entries in the filtered index.yaml - -## HelmChartSubscriptions - -The subscription operator watches `HelmChartSubscription` and `HelmRelease` CRs. - -The User creates a HelmChartSubscription CR. if installPlanApproval is set to `Automatic` then the helmrepo will be monitored and new chart version will be deployed, if set to `Manual` then no automatic deployment. - - -```yaml -apiVersion: app.ibm.com/v1alpha1 -kind: HelmChartSubscription -metadata: - name: razee - namespace: default -spec: - channel: default/ope - installPlanApproval: Automatic - secretRef: - name: mysecret - configRef: - name: mycluster-config - name: ibm-razee-api - packageFilter: - keywords: - - ICP - annotations: - tillerVersion: 2.4.0 - version: '>0.2.2' - packageOverrides: - - packageName: ibm-razee-api - packageOverrides: - - path: spec.values - value: "RazeeAPI: \n Endpoint: http://9.30.166.165:31311\n ObjectstoreSecretName: - minio\n Region: us-east-1\n" - chartsSource: - helmrepo: - urls: - - https://mycluster.icp:8443/helm-repo/charts - ``` - - Source can have the following format for github (not yet fully implemented): - - ``` yaml - chartsSource: - type: github - github: - urls: - - https://github.ibm.com/IBMPrivateCloud/hybrid-cluster-manager-v2-chart.git - chartsPath: 3.2.1-examples/guestbook-kube-subscription - branch: master - ``` - -branch master is the default. - -## Helm-charts filtering - -The optional spec.name defines the name of the helm-chart, it can be also a regex if multiple helm-charts must be deployed. - -The optional spec.packageFilter allows to filter the helm-charts. -Filtering is done on: - -- the version of the helm-chart (semver expression), -- the tiller version of the helm-chart (Should may be removed as the operator has its own tiller) -- the digest must match -- the keywords, if the helm-chart has a least 1 listed keywords then it eligible for deployment. - -## Authentication - -A secretRef can be provided in the subscriptionRelease spec. It references a secret where the authentication parameter to access the helm-repo are set. -The attributes are either `user` and `password` or `authHeader`. All values must be base64 encoded. -The `authHeader` format is ` ` and so for example: -`Bearer xxxxxx`. - -## Helm-repo client configuration - -The configRef is a reference to a configMap which holds the parameters to the helm-repo. - -```yaml -apiVersion: v1 -data: - insecureSkipVerify: "true" -kind: ConfigMap -metadata: - name: mycluster-config - namespace: default -``` - - -The HelmReleases are owned by the HelmChartSubscription and so if the subscription is deleted the release is deleted too. - -```yaml -apiVersion: app.ibm.com/v1alpha1 -kind: HelmRelease -metadata: - annotations: - app.ibm.com/hosting-deployable: default/ope - app.ibm.com/hosting-subscription: default/razee - creationTimestamp: 2019-08-12T09:01:52Z - generation: 1 - name: razee-ibm-razee-api-ope - namespace: default - ownerReferences: - - apiVersion: app.ibm.com/v1alpha1 - blockOwnerDeletion: true - controller: true - kind: HelmChartSubscription - name: razee - uid: ec3c8f28-bcde-11e9-b55f-fa163e0cb658 - resourceVersion: "3852059" - selfLink: /apis/app.ibm.com/v1alpha1/namespaces/default/subscriptionreleases/razee-ibm-razee-api-ope - uid: d35adca8-bcdf-11e9-b55f-fa163e0cb658 -spec: - source: - type: helmrepo - helmRepo: - URLs: - - https://mycluster.icp:8443/helm-repo/requiredAssets/ibm-razee-api-0.2.3-015-20190725140717.tgz - chartName: ibm-razee-api - secretRef: - name: mysecret - configRef: - name: mycluster-config - values: "RazeeAPI: \n Endpoint: http://9.30.166.165:31311\n ObjectstoreSecretName: - minio\n Region: us-east-1\n" - version: 0.2.3-015-20190725140717 -``` - -Source can have the following format for github: - -```yaml - source: - github: - urls: - - https://github.ibm.com/IBMPrivateCloud/icp-cert-manager-chart - chartPath: stable/ibm-cert-manager - branch: master - type: github -``` - -Branch master is the default. - -Once the HelmRelease is created or modified, the operator will deploy each charts specified in each HelmRelease. - -To do so, the following steps are taken: - -1) Download the chart tgz in the `$CHARTS_DIR`. -2) Unzip the tgz in `$CHARTS_DIR///` -3) Create a manager with the values provided in the HelmRelease -4) Launch the deployment. + + diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..907a223 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,17 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Release Process](#release-process) + + + +# Release Process + +The XXX is released on an as-needed basis. The process is as follows: + +1. An issue is proposing a new release with a changelog since the last release +1. All [OWNERS](OWNERS) must LGTM this release +1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` +1. The release issue is closed +1. An announcement email is sent to `xxx` with the subject `[ANNOUNCE] xxx $VERSION is released` diff --git a/build/Dockerfile b/build/Dockerfile index 1de352f..5a041b7 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi7/ubi-minimal:latest +FROM registry.access.redhat.com/ubi7/ubi-minimal:7.7-98 ENV OPERATOR=/usr/local/bin/multicloud-operators-subscription-release \ USER_UID=1001 \ diff --git a/build/install-operator-sdk.sh b/build/install-operator-sdk.sh deleted file mode 100755 index 6b0fe9c..0000000 --- a/build/install-operator-sdk.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -echo ">>> Installing Operator SDK" -echo ">>> >>> Downloading source code" -GO111MODULE=off go get -d -v github.com/operator-framework/operator-sdk - -cd $GOPATH/src/github.com/operator-framework/operator-sdk - -echo ">>> >>> Checking out version 0.10.0" -git checkout v0.10.0 - -echo ">>> >>> Running make tidy" -make tidy - -echo ">>> >>> Running make install" -make install \ No newline at end of file diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 4b7e17f..e55efb9 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -23,6 +23,9 @@ import ( "os" "runtime" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc. + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc. + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc. // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc. // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc. // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -73,7 +76,10 @@ func main() { pflag.CommandLine.AddGoFlagSet(flag.CommandLine) // Add subscription operator flag set to the CLI. - pflag.CommandLine.BoolVar(&helmchartsubscription.Options.Disabled, "helmchart-subscription-controller-disabled", false, "Disable the helmchart subscription controller") + pflag.CommandLine.BoolVar(&helmchartsubscription.Options.Disabled, + "helmchart-subscription-controller-disabled", + false, + "Disable the helmchart subscription controller") pflag.Parse() @@ -143,8 +149,16 @@ func main() { // Add to the below struct any other metrics ports you want to expose. servicePorts := []v1.ServicePort{ - {Port: metricsPort, Name: metrics.OperatorPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}}, - {Port: operatorMetricsPort, Name: metrics.CRPortName, Protocol: v1.ProtocolTCP, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}}, + {Port: metricsPort, + Name: metrics.OperatorPortName, + Protocol: v1.ProtocolTCP, + TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: metricsPort}, + }, + {Port: operatorMetricsPort, + Name: metrics.CRPortName, + Protocol: v1.ProtocolTCP, + TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: operatorMetricsPort}, + }, } // Create Service object to expose the metrics port(s). service, err := metrics.CreateMetricsService(ctx, cfg, servicePorts) @@ -155,6 +169,7 @@ func main() { // CreateServiceMonitors will automatically create the prometheus-operator ServiceMonitor resources // necessary to configure Prometheus to scrape metrics from this operator. services := []*v1.Service{service} + _, err = metrics.CreateServiceMonitors(cfg, namespace, services) if err != nil { log.Info("Could not create ServiceMonitor object", "error", err.Error()) @@ -195,5 +210,6 @@ func serveCRMetrics(cfg *rest.Config) error { if err != nil { return err } + return nil } diff --git a/common/Makefile.common.mk b/common/Makefile.common.mk new file mode 100644 index 0000000..5fade0d --- /dev/null +++ b/common/Makefile.common.mk @@ -0,0 +1,84 @@ +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +############################################################ +# GKE section +############################################################ +PROJECT ?= oceanic-guard-191815 +ZONE ?= us-west1-a +CLUSTER ?= prow + +activate-serviceaccount: +ifdef GOOGLE_APPLICATION_CREDENTIALS + gcloud auth activate-service-account --key-file="$(GOOGLE_APPLICATION_CREDENTIALS)" +endif + +get-cluster-credentials: activate-serviceaccount + gcloud container clusters get-credentials "$(CLUSTER)" --project="$(PROJECT)" --zone="$(ZONE)" + +config-docker: get-cluster-credentials + @common/scripts/config_docker.sh + +FINDFILES=find . \( -path ./.git -o -path ./.github \) -prune -o -type f +XARGS = xargs -0 ${XARGS_FLAGS} +CLEANXARGS = xargs ${XARGS_FLAGS} + + +lint-dockerfiles: + @${FINDFILES} -name 'Dockerfile*' -print0 | ${XARGS} hadolint -c ./common/config/.hadolint.yml + +lint-scripts: + @${FINDFILES} -name '*.sh' -print0 | ${XARGS} shellcheck + +lint-yaml: + @${FINDFILES} \( -name '*.yml' -o -name '*.yaml' \) -print0 | ${XARGS} grep -L -e "{{" | ${CLEANXARGS} yamllint -c ./common/config/.yamllint.yml + +lint-helm: + @${FINDFILES} -name 'Chart.yaml' -print0 | ${XARGS} -L 1 dirname | ${CLEANXARGS} helm lint --strict + +lint-copyright-banner: + @${FINDFILES} \( -name '*.go' -o -name '*.cc' -o -name '*.h' -o -name '*.proto' -o -name '*.py' -o -name '*.sh' \) \( ! \( -name '*.gen.go' -o -name '*.pb.go' -o -name '*_pb2.py' \) \) -print0 |\ + ${XARGS} common/scripts/lint_copyright_banner.sh + +lint-go: + @${FINDFILES} -name '*.go' \( ! \( -name '*.gen.go' -o -name '*.pb.go' \) \) -print0 | ${XARGS} common/scripts/lint_go.sh + +lint-python: + @${FINDFILES} -name '*.py' \( ! \( -name '*_pb2.py' \) \) -print0 | ${XARGS} autopep8 --max-line-length 160 --exit-code -d + +lint-markdown: + @${FINDFILES} -name '*.md' -print0 | ${XARGS} mdl --ignore-front-matter --style common/config/mdl.rb + @${FINDFILES} -name '*.md' -print0 | ${XARGS} awesome_bot --skip-save-results --allow_ssl --allow-timeout --allow-dupe --allow-redirect --white-list ${MARKDOWN_LINT_WHITELIST} + +lint-sass: + @${FINDFILES} -name '*.scss' -print0 | ${XARGS} sass-lint -c common/config/sass-lint.yml --verbose + +lint-typescript: + @${FINDFILES} -name '*.ts' -print0 | ${XARGS} tslint -c common/config/tslint.json + +lint-protos: + @$(FINDFILES) -name '*.proto' -print0 | $(XARGS) -L 1 prototool lint --protoc-bin-path=/usr/bin/protoc + +lint-all: lint-dockerfiles lint-scripts lint-yaml lint-helm lint-copyright-banner lint-go lint-python lint-markdown lint-sass lint-typescript lint-protos + +format-go: + @${FINDFILES} -name '*.go' \( ! \( -name '*.gen.go' -o -name '*.pb.go' \) \) -print0 | ${XARGS} goimports -w -local "github.com/IBM" + +format-python: + @${FINDFILES} -name '*.py' -print0 | ${XARGS} autopep8 --max-line-length 160 --aggressive --aggressive -i + +format-protos: + @$(FINDFILES) -name '*.proto' -print0 | $(XARGS) -L 1 prototool format -w + +.PHONY: lint-dockerfiles lint-scripts lint-yaml lint-copyright-banner lint-go lint-python lint-helm lint-markdown lint-sass lint-typescript lint-protos lint-all format-go format-python format-protos config-docker diff --git a/common/config/.golangci.yml b/common/config/.golangci.yml new file mode 100644 index 0000000..54f84ba --- /dev/null +++ b/common/config/.golangci.yml @@ -0,0 +1,187 @@ +service: + # When updating this, also update the version stored in docker/build-tools/Dockerfile in the multicloudlab/tools repo. + golangci-lint-version: 1.18.x # use the fixed version to not introduce new linters unexpectedly +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + deadline: 20m + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs: + - genfiles$ + - vendor$ + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + skip-files: + - ".*\\.pb\\.go" + - ".*\\.gen\\.go" + +linters: + enable-all: true + disable: + - depguard + - dupl + - gochecknoglobals + - gochecknoinits + - goconst + - gocyclo + - gosec + - nakedret + - prealloc + - scopelint + - funlen + - bodyclose + fast: false + +linters-settings: + errcheck: + # report about not checking of errors in type assetions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + govet: + # report about shadowed variables + check-shadowing: false + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.0 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/IBM/ + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - cancelled + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 160 + # tab width in spaces. Default to 1. + tab-width: 1 + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unparam: + # call graph construction algorithm (cha, rta). In general, use cha for libraries, + # and rta for programs with main packages. Default is cha. + algo: cha + + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + gocritic: + enabled-checks: + - appendCombine + - argOrder + - assignOp + - badCond + - boolExprSimplify + - builtinShadow + - captLocal + - caseOrder + - codegenComment + - commentedOutCode + - commentedOutImport + - defaultCaseOrder + - deprecatedComment + - docStub + - dupArg + - dupBranchBody + - dupCase + - dupSubExpr + - elseif + - emptyFallthrough + - equalFold + - flagDeref + - flagName + - hexLiteral + - indexAlloc + - initClause + - methodExprCall + - nilValReturn + - octalLiteral + - offBy1 + - rangeExprCopy + - regexpMust + - sloppyLen + - stringXbytes + - switchTrue + - typeAssertChain + - typeSwitchVar + - typeUnparen + - underef + - unlambda + - unnecessaryBlock + - unslice + - valSwap + - weakCond + + # Unused + # - yodaStyleExpr + # - appendAssign + # - commentFormatting + # - emptyStringTest + # - exitAfterDefer + # - ifElseChain + # - hugeParam + # - importShadow + # - nestingReduce + # - paramTypeCombine + # - ptrToRefParam + # - rangeValCopy + # - singleCaseSwitch + # - sloppyReassign + # - unlabelStmt + # - unnamedResult + # - wrapperFunc + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - composite literal uses unkeyed fields + + exclude-rules: + # Exclude some linters from running on test files. + - path: _test\.go$|^tests/|^samples/ + linters: + - errcheck + - maligned + + # Independently from option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: true + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 \ No newline at end of file diff --git a/common/config/.hadolint.yml b/common/config/.hadolint.yml new file mode 100644 index 0000000..03d0162 --- /dev/null +++ b/common/config/.hadolint.yml @@ -0,0 +1,7 @@ +# ignored: + +trustedRegistries: + - gcr.io + - docker.io + - quay.io + - registry.access.redhat.com diff --git a/common/config/.yamllint.yml b/common/config/.yamllint.yml new file mode 100644 index 0000000..fe331a6 --- /dev/null +++ b/common/config/.yamllint.yml @@ -0,0 +1,22 @@ +rules: + braces: disable + brackets: disable + colons: enable + commas: disable + comments: disable + comments-indentation: disable + document-end: disable + document-start: disable + empty-lines: disable + empty-values: enable + hyphens: enable + indentation: disable + key-duplicates: enable + key-ordering: disable + line-length: disable + new-line-at-end-of-file: disable + new-lines: enable + octal-values: enable + quoted-strings: disable + trailing-spaces: disable + truthy: disable \ No newline at end of file diff --git a/common/config/mdl.rb b/common/config/mdl.rb new file mode 100644 index 0000000..0e4edce --- /dev/null +++ b/common/config/mdl.rb @@ -0,0 +1,12 @@ +all +rule 'MD002', :level => 1 +rule 'MD007', :indent => 4 +rule 'MD013', :line_length => 160, :code_blocks => false, :tables => false +rule 'MD026', :punctuation => ".,;:!" +exclude_rule 'MD013' +exclude_rule 'MD014' +exclude_rule 'MD030' +exclude_rule 'MD032' +exclude_rule 'MD033' +exclude_rule 'MD041' +exclude_rule 'MD046' \ No newline at end of file diff --git a/common/config/sass-lint.yml b/common/config/sass-lint.yml new file mode 100644 index 0000000..250e4b8 --- /dev/null +++ b/common/config/sass-lint.yml @@ -0,0 +1,98 @@ +######################### +## Config for sass-lint +######################### +# Linter Options +options: + # Don't merge default rules + merge-default-rules: false + # Raise an error if more than 50 warnings are generated + max-warnings: 500 +# Rule Configuration +rules: + attribute-quotes: + - 2 + - + include: false + bem-depth: 2 + border-zero: 2 + brace-style: 2 + class-name-format: 2 + clean-import-paths: 2 + declarations-before-nesting: 2 + empty-args: 2 + empty-line-between-blocks: 2 + extends-before-declarations: 2 + extends-before-mixins: 2 + final-newline: 2 + force-attribute-nesting: 0 + force-element-nesting: 0 + force-pseudo-nesting: 0 + function-name-format: 2 + hex-length: 0 + hex-notation: 2 + id-name-format: 2 + indentation: + - 2 + - + size: 4 + leading-zero: + - 2 + - + include: false + max-file-line-count: 0 + max-file-length: 0 + mixins-before-declarations: 2 + no-attribute-selectors: 0 + no-color-hex: 0 + no-color-keywords: 0 + no-color-literals: 0 + no-combinators: 0 + no-css-comments: 2 + no-debug: 2 + no-disallowed-properties: 2 + no-duplicate-properties: 2 + no-empty-rulesets: 2 + no-extends: 2 + no-ids: 0 + no-invalid-hex: 2 + no-important: 0 + no-mergeable-selectors: 2 + no-misspelled-properties: 2 + no-qualifying-elements: 0 + no-trailing-whitespace: 2 + no-trailing-zero: 2 + no-transition-all: 0 + no-url-domains: 2 + no-url-protocols: 2 + no-warn: 2 + one-declaration-per-line: 2 + placeholder-in-extend: 2 + placeholder-name-format: 2 + property-sort-order: 0 + property-units: 2 + pseudo-element: 2 + quotes: + - 2 + - + style: double + shorthand-values: 2 + single-line-per-selector: 0 + space-after-bang: 2 + space-after-colon: 2 + space-after-comma: 2 + space-around-operator: 2 + space-before-bang: 2 + space-before-brace: 2 + space-before-colon: 2 + space-between-parens: 2 + trailing-semicolon: 2 + url-quotes: 2 + variable-for-property: + - 0 + - + properties: + - color + - background-color + - fill + variable-name-format: 0 + zero-unit: 2 \ No newline at end of file diff --git a/common/config/tslint.json b/common/config/tslint.json new file mode 100644 index 0000000..6db4bb1 --- /dev/null +++ b/common/config/tslint.json @@ -0,0 +1,25 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "rules": { + "max-line-length": { + "options": [160] + }, + "arrow-parens": false, + "new-parens": true, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": true, + "no-console": { + "severity": "warning", + "options": ["debug", "info", "log", "time", "timeEnd", "trace"] + }, + "no-shadowed-variable": false, + "eofline": false + }, + "jsRules": {}, + "rulesDirectory": [] +} \ No newline at end of file diff --git a/common/scripts/codecov.sh b/common/scripts/codecov.sh new file mode 100755 index 0000000..6831f97 --- /dev/null +++ b/common/scripts/codecov.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -u +set -o pipefail + +# turn off GOSUMDB check +export GOSUMDB=off + +# add $GOPATH/bin to $PATH +# TODO: add $GOPATH/bin to $PATH in build-tools image +export PATH=${PATH}:${GOPATH}/bin + +ROOTDIR="$(cd "$(dirname "$0")"/../.. ; pwd -P)" +REPORT_PATH=${REPORT_PATH:-"${GOPATH}/out/codecov"} +#CODECOV_SKIP=${GOPATH}/out/codecov/codecov.skip +MAXPROCS="${MAXPROCS:-}" +BUILD_LOCALLY=${1:?0} +shift + +mkdir -p "${GOPATH}"/out/codecov + +DIR="./..." +#SKIPPED_TESTS_GREP_ARGS= +TEST_RETRY_COUNT=3 + +if [ "${1:-}" != "" ]; then + DIR="./$1/..." +fi + +COVERAGEDIR="$(mktemp -d /tmp/test_coverage.XXXXXXXXXX)" +mkdir -p "$COVERAGEDIR" + +# half the number of cpus seem to saturate +if [[ -z ${MAXPROCS:-} ]]; then + MAXPROCS=$(($(getconf _NPROCESSORS_ONLN)/2)) +fi + +function code_coverage() { + local filename + local count=${2:-0} + filename="$(echo "${1}" | tr '/' '-')" + go test \ + -coverprofile="${COVERAGEDIR}/${filename}.cov" \ + -covermode=atomic "${1}" \ + | tee "${COVERAGEDIR}/${filename}.report" \ + | tee >(go-junit-report > "${COVERAGEDIR}/${filename}-junit.xml") \ + && RC=$? || RC=$? + + if [[ ${RC} != 0 ]]; then + if (( count < TEST_RETRY_COUNT )); then + code_coverage "${1}" $((count+1)) + else + echo "${1}" | tee "${COVERAGEDIR}/${filename}.err" + fi + fi + + #remove skipped tests from .cov file +# remove_skipped_tests_from_cov "${COVERAGEDIR}/${filename}.cov" +} + +function wait_for_proc() { + local num + num=$(jobs -p | wc -l) + while [ "${num}" -gt ${MAXPROCS} ]; do + sleep 2 + num=$(jobs -p|wc -l) + done +} + +# function parse_skipped_tests() { +# while read -r entry; do +# if [[ "${SKIPPED_TESTS_GREP_ARGS}" != '' ]]; then +# SKIPPED_TESTS_GREP_ARGS+='\|' +# fi +# if [[ "${entry}" != "#"* ]]; then +# SKIPPED_TESTS_GREP_ARGS+="\\(${entry}\\)" +# fi +# done < "${CODECOV_SKIP}" +# } + +# function remove_skipped_tests_from_cov() { +# while read -r entry; do +# entry="$(echo "${entry}" | sed 's/\//\\\//g')" +# sed -i "/${entry}/d" "$1" +# done < "${CODECOV_SKIP}" +# } + +cd "${ROOTDIR}" + +# parse_skipped_tests + +# For generating junit.xml files +go get github.com/jstemmer/go-junit-report + +echo "Code coverage test (concurrency ${MAXPROCS})" +for P in $(go list "${DIR}" | grep -v vendor); do +# if echo "${P}" | grep -q "${SKIPPED_TESTS_GREP_ARGS}"; then +# echo "Skipped ${P}" +# continue +# fi + code_coverage "${P}" & + wait_for_proc +done + +wait + +###################################################### +# start generating report +###################################################### +touch "${COVERAGEDIR}/empty" +mkdir -p "${REPORT_PATH}" +pushd "${REPORT_PATH}" + +# Build the combined coverage files +go get github.com/wadey/gocovmerge +gocovmerge "${COVERAGEDIR}"/*.cov > coverage.cov +cat "${COVERAGEDIR}"/*.report > report.out + +# Build the combined junit.xml +go get github.com/imsky/junit-merger/src/junit-merger +junit-merger "${COVERAGEDIR}"/*-junit.xml > junit.xml + +popd + +echo "Intermediate files were written to ${COVERAGEDIR}" +echo "Final reports are stored in ${REPORT_PATH}" + +if ls "${COVERAGEDIR}"/*.err 1> /dev/null 2>&1; then + echo "The following tests had failed:" + cat "${COVERAGEDIR}"/*.err + exit 1 +fi +###################################################### +# end generating report +###################################################### + +# Upload to codecov.io in post submit only for visualization +if [ "$BUILD_LOCALLY" == 0 ]; then + bash <(curl -s https://codecov.io/bash) -t "${CODECOV_TOKEN}" -f "${REPORT_PATH}/coverage.cov" +fi \ No newline at end of file diff --git a/common/scripts/config_docker.sh b/common/scripts/config_docker.sh new file mode 100755 index 0000000..81fb9a6 --- /dev/null +++ b/common/scripts/config_docker.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +KUBECTL=$(command -v kubectl) +DOCKER_REGISTRY="quay.io" +DOCKER_USERNAME="multicloudlab" +DOCKER_PASSWORD=$(${KUBECTL} -n default get secret quay-cred -o jsonpath='{.data.password}' | base64 --decode) + +# support other container tools, e.g. podman +CONTAINER_CLI=${CONTAINER_CLI:-docker} + +# login the docker registry +${CONTAINER_CLI} login "${DOCKER_REGISTRY}" -u "${DOCKER_USERNAME}" -p "${DOCKER_PASSWORD}" diff --git a/common/scripts/gobuild.sh b/common/scripts/gobuild.sh new file mode 100755 index 0000000..495039f --- /dev/null +++ b/common/scripts/gobuild.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script builds and version stamps the output + +VERBOSE=${VERBOSE:-"0"} +V="" +if [[ "${VERBOSE}" == "1" ]];then + V="-x" + set -x +fi + +OUT=${1:?"output path"} +shift + +set -e + +BUILD_GOOS=${GOOS:-linux} +BUILD_GOARCH=${GOARCH:-amd64} +GOBINARY=${GOBINARY:-go} +BUILDINFO=${BUILDINFO:-""} +STATIC=${STATIC:-1} +LDFLAGS="-extldflags -static" +GOBUILDFLAGS=${GOBUILDFLAGS:-""} +# Split GOBUILDFLAGS by spaces into an array called GOBUILDFLAGS_ARRAY. +IFS=' ' read -r -a GOBUILDFLAGS_ARRAY <<< "$GOBUILDFLAGS" + +GCFLAGS=${GCFLAGS:-} +export CGO_ENABLED=0 + +if [[ "${STATIC}" != "1" ]];then + LDFLAGS="" +fi + +time GOOS=${BUILD_GOOS} GOARCH=${BUILD_GOARCH} ${GOBINARY} build \ + ${V} "${GOBUILDFLAGS_ARRAY[@]}" ${GCFLAGS:+-gcflags "${GCFLAGS}"} \ + -o "${OUT}" \ + -ldflags "${LDFLAGS}" "${@}" diff --git a/common/scripts/lint_copyright_banner.sh b/common/scripts/lint_copyright_banner.sh new file mode 100755 index 0000000..061b8ed --- /dev/null +++ b/common/scripts/lint_copyright_banner.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +ec=0 +for fn in "$@"; do + if ! grep -L -q -e "Apache License, Version 2" "${fn}"; then + echo "Missing license: ${fn}" + ec=1 + fi + + if ! grep -L -q -e "Copyright" "${fn}"; then + echo "Missing copyright: ${fn}" + ec=1 + fi +done + +exit $ec diff --git a/common/scripts/lint_go.sh b/common/scripts/lint_go.sh new file mode 100755 index 0000000..b2fefa7 --- /dev/null +++ b/common/scripts/lint_go.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# +# Copyright 2019 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +GOGC=25 golangci-lint run -c ./common/config/.golangci.yml diff --git a/deploy/crds/app_v1alpha1_helmchartsubscription_crd.yaml b/deploy/crds/app_v1alpha1_helmchartsubscription_crd.yaml index 2cb8786..5c972ac 100644 --- a/deploy/crds/app_v1alpha1_helmchartsubscription_crd.yaml +++ b/deploy/crds/app_v1alpha1_helmchartsubscription_crd.yaml @@ -29,6 +29,8 @@ spec: type: object spec: properties: + channel: + type: string chartsSource: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "operator-sdk generate k8s" to regenerate code after @@ -62,6 +64,11 @@ spec: description: Configuration parameters to access the helm-repo defined in the CatalogSource type: object + installPlanApproval: + type: string + name: + description: To specify 1 package in channel + type: string packageFilter: description: To specify more than 1 package in channel properties: @@ -135,6 +142,8 @@ spec: required: - lastUpdateTime type: object + required: + - channel type: object status: properties: diff --git a/deploy/crds/app_v1alpha1_helmrelease_cr.yaml b/deploy/crds/app_v1alpha1_helmrelease_cr.yaml index 412199a..2d952e2 100644 --- a/deploy/crds/app_v1alpha1_helmrelease_cr.yaml +++ b/deploy/crds/app_v1alpha1_helmrelease_cr.yaml @@ -1,7 +1,7 @@ apiVersion: app.ibm.com/v1alpha1 -kind: SubscriptionRelease +kind: HelmRelease metadata: - name: example-subscriptionrelease + name: example-helmrelease spec: # Add fields here size: 3 diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 6aee8f8..e8cbffe 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -1,3 +1,46 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: multicloud-operators-subscription-release +rules: +- apiGroups: + - app.ibm.com + resources: + - helmchartsubscription + - helmreleases + verbs: + - get + - list + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: multicloud-operators-subscription-release-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: multicloud-operators-subscription-release + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: multicloud-operators-subscription-release-cluster-role-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: multicloud-operators-subscription-release +subjects: +- kind: ServiceAccount + name: default + namespace: default +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -16,14 +59,10 @@ spec: volumes: - name: charts emptyDir: {} - hostAliases: - - ip: 9.20.204.231 - hostnames: - - mycluster.icp containers: - name: multicloud-operators-subscription-release # Replace this with the built image name - image: mycluster.icp:8500/kube-system/ibm/multicloud-operators-subscription-release:latest + image: hyc-cloud-private-integration-docker-local.artifactory.swg-devops.com/ibmcom/multicloud-operators-subscription-release-amd64:latest command: - multicloud-operators-subscription-release imagePullPolicy: Always @@ -44,7 +83,7 @@ spec: - name: charts mountPath: "/charts" securityContext: - procMount: Default + # procMount: Default readOnlyRootFilesystem: true resources: requests: diff --git a/deploy/role.yaml b/deploy/role.yaml index 53b2bd9..5543e0c 100644 --- a/deploy/role.yaml +++ b/deploy/role.yaml @@ -57,7 +57,7 @@ rules: - app.ibm.com resources: - '*' - - subscriptionreleases + - helmreleases - helmchartsubscriptions verbs: - '*' diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..c0838a8 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,3 @@ +# Development Guide + +This document explains how to develop xxx. diff --git a/go.mod b/go.mod index 3d24fcf..753c863 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/ghodss/yaml v1.0.0 github.com/go-openapi/spec v0.19.0 + github.com/jstemmer/go-junit-report v0.9.1 // indirect github.com/martinlindhe/base36 v0.0.0-20180729042928-5cda0030da17 github.com/onsi/gomega v1.4.3 github.com/operator-framework/operator-lifecycle-manager v0.0.0-20190128024246-5eb7ae5bdb7a diff --git a/go.sum b/go.sum index 866975b..db3f5de 100644 --- a/go.sum +++ b/go.sum @@ -254,6 +254,8 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..648435d --- /dev/null +++ b/hack/boilerplate.go.txt @@ -0,0 +1,13 @@ +// Copyright 2019 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/pkg/apis/app/v1alpha1/helmchartsubscription_types.go b/pkg/apis/app/v1alpha1/helmchartsubscription_types.go index c32636d..930668c 100644 --- a/pkg/apis/app/v1alpha1/helmchartsubscription_types.go +++ b/pkg/apis/app/v1alpha1/helmchartsubscription_types.go @@ -20,12 +20,14 @@ import ( "fmt" "strings" - operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +//ChartsDir env variable name which contains the directory where the charts are installed +const ChartsDir = "CHARTS_DIR" + // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. @@ -50,13 +52,14 @@ type Overrides struct { PackageOverrides []PackageOverride `json:"packageOverrides"` // To be added } +//GitHubSubscription provides information to retrieve the helm-chart from github type GitHubSubscription struct { Urls []string `json:"urls,omitempty"` ChartsPath string `json:"chartsPath,omitempty"` Branch string `json:"branch,omitempty"` } -//HelmRepoSubscription provides the urls to retreive the helm-chart +//HelmRepoSubscription provides the urls to retrieve the helm-chart type HelmRepoSubscription struct { Urls []string `json:"urls,omitempty"` } @@ -89,15 +92,11 @@ type HelmChartSubscriptionSpec struct { // Source holds the url toward the helm-chart Source *SourceSubscription `json:"chartsSource,omitempty"` - // leverage and enhance subscription spec from operator lifecycle framework - // mapping of the fields: - // CatalogSourceNamespace - N/A - // CatalogSource - if specified, ignore Source and will be a helm-repo - // Package - Optional, to filter package by names - // Channel - Channel NamespacedName (in hub) - // StartingCSV - N/A - // InstallPlanApproval - N/A - operatorsv1alpha1.SubscriptionSpec + Channel string `json:"channel"` + // To specify 1 package in channel + Package string `json:"name,omitempty"` + + InstallPlanApproval Approval `json:"installPlanApproval,omitempty"` // To specify more than 1 package in channel PackageFilter *PackageFilter `json:"packageFilter,omitempty"` @@ -113,6 +112,16 @@ type HelmChartSubscriptionSpec struct { Status HelmChartSubscriptionStatus `json:"status,omitempty"` } +//Approval approval types +type Approval string + +const ( + //ApprovalManual when set to this value, the helmRelease will not be automatically updated if a new version of the helm-chart is available. + ApprovalManual Approval = "Manual" + //ApprovalAutomatic when set to this value, the helmRelease will be automatically updated when a new version of the helm-chart is available. + ApprovalAutomatic Approval = "Automatic" +) + // HelmChartSubscriptionStatusEnum defines the status of a HelmChartSubscription type HelmChartSubscriptionStatusEnum string diff --git a/pkg/apis/app/v1alpha1/helmrelease_types.go b/pkg/apis/app/v1alpha1/helmrelease_types.go index 02c7cec..1fd0815 100644 --- a/pkg/apis/app/v1alpha1/helmrelease_types.go +++ b/pkg/apis/app/v1alpha1/helmrelease_types.go @@ -37,17 +37,17 @@ const ( HelmReleaseSuccess HelmReleaseStatusEnum = "Success" ) -//SourceTypeEnum ... +//SourceTypeEnum types of sources type SourceTypeEnum string const ( - // HelmRepoSourceType ... + // HelmRepoSourceType helmrepo source type HelmRepoSourceType SourceTypeEnum = "helmrepo" - // GitHubSourceType ... + // GitHubSourceType github source type GitHubSourceType SourceTypeEnum = "github" ) -//HelmReleaseStatus ... +//HelmReleaseStatus struct containing the status type HelmReleaseStatus struct { Status HelmReleaseStatusEnum `json:"phase,omitempty"` Message string `json:"message,omitempty"` @@ -62,7 +62,7 @@ type GitHub struct { Branch string `json:"branch,omitempty"` } -//HelmRepo provides the urls to retreive the helm-chart +//HelmRepo provides the urls to retrieve the helm-chart type HelmRepo struct { Urls []string `json:"urls,omitempty"` } diff --git a/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go index c840b22..f636259 100644 --- a/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,19 @@ // +build !ignore_autogenerated +// Copyright 2019 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + // Code generated by operator-sdk. DO NOT EDIT. package v1alpha1 @@ -121,7 +135,6 @@ func (in *HelmChartSubscriptionSpec) DeepCopyInto(out *HelmChartSubscriptionSpec *out = new(SourceSubscription) (*in).DeepCopyInto(*out) } - out.SubscriptionSpec = in.SubscriptionSpec if in.PackageFilter != nil { in, out := &in.PackageFilter, &out.PackageFilter *out = new(PackageFilter) diff --git a/pkg/apis/app/v1alpha1/zz_generated.openapi.go b/pkg/apis/app/v1alpha1/zz_generated.openapi.go index 59e0ae3..a8b901e 100644 --- a/pkg/apis/app/v1alpha1/zz_generated.openapi.go +++ b/pkg/apis/app/v1alpha1/zz_generated.openapi.go @@ -1,5 +1,19 @@ // +build !ignore_autogenerated +// Copyright 2019 The Kubernetes Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + // This file was autogenerated by openapi-gen. Do not edit it manually! package v1alpha1 diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index a472e35..47fcf81 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -30,5 +30,6 @@ func AddToManager(m manager.Manager) error { return err } } + return nil } diff --git a/pkg/controller/helmchartsubscription/helmchartsubscription_controller.go b/pkg/controller/helmchartsubscription/helmchartsubscription_controller.go index 946c736..aa8d366 100644 --- a/pkg/controller/helmchartsubscription/helmchartsubscription_controller.go +++ b/pkg/controller/helmchartsubscription/helmchartsubscription_controller.go @@ -22,8 +22,6 @@ import ( "reflect" "time" - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - "github.com/IBM/multicloud-operators-subscription-release/pkg/helmreposubscriber" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,6 +34,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/source" + + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" + "github.com/IBM/multicloud-operators-subscription-release/pkg/helmreposubscriber" ) //ControllerCMDOptions possible command line options @@ -59,6 +60,7 @@ func Add(mgr manager.Manager) error { if !Options.Disabled { return add(mgr, newReconciler(mgr)) } + return nil } @@ -84,18 +86,20 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { if !reflect.DeepEqual(subRelOld.Spec, subRelNew.Spec) { return true } + if subRelNew.Status.Status == subRelOld.Status.Status { return false } + return true }, } + err = c.Watch(&source.Kind{Type: &appv1alpha1.HelmChartSubscription{}}, &handler.EnqueueRequestForObject{}, p) if err != nil { return err } - // TODO(user): Modify this to be the types you create that are owned by the primary resource // Watch for changes to secondary resource Pods and requeue the owner Subscription err = c.Watch(&source.Kind{Type: &appv1alpha1.HelmRelease{}}, &handler.EnqueueRequestForOwner{ IsController: true, @@ -122,8 +126,6 @@ type ReconcileSubscription struct { // Reconcile reads that state of the cluster for a Subscription object and makes changes based on the state read // and what is in the Subscription.Spec -// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates -// a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. @@ -134,6 +136,7 @@ func (r *ReconcileSubscription) Reconcile(request reconcile.Request) (reconcile. // Fetch the Subscription instance instance := &appv1alpha1.HelmChartSubscription{} subkey := request.NamespacedName.String() + err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -141,23 +144,26 @@ func (r *ReconcileSubscription) Reconcile(request reconcile.Request) (reconcile. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue reqLogger.Info("Subscription deleted but request already created, cleaning subscriber") - r.cleanSubscriber(subkey) - return reconcile.Result{}, nil + return reconcile.Result{}, r.cleanSubscriber(subkey) } // Error reading the object - requeue the request. reqLogger.Error(err, "Error reading the object - requeue the request") + return reconcile.Result{}, err } subscriber := r.subscriberMap[subkey] if subscriber == nil { reqLogger.Info("subscriber does not exist") + subscriber = &helmreposubscriber.HelmRepoSubscriber{ Client: r.client, Scheme: r.scheme, HelmChartSubscription: instance, } - reqLogger.Info("Subscription", "subscription.Name", instance.Name, "configMapRef", instance.Spec.ConfigMapRef) + + reqLogger.Info("Subscription", "subscription.Name", instance.Name) + r.subscriberMap[subkey] = subscriber err = subscriber.Restart() } else { @@ -167,61 +173,57 @@ func (r *ReconcileSubscription) Reconcile(request reconcile.Request) (reconcile. //If the subscriber didn't start then clean if !subscriber.IsStarted() { reqLogger.Info("Subscription didn't start") - r.cleanSubscriber(subkey) + + err = r.cleanSubscriber(subkey) + if err != nil { + return r.SetStatus(instance, err) + } } + return r.SetStatus(instance, err) } -// Helper functions to check and remove string from a slice of strings. -// func containsString(slice []string, s string) bool { -// for _, item := range slice { -// if item == s { -// return true -// } -// } -// return false -// } - -// func removeString(slice []string, s string) (result []string) { -// for _, item := range slice { -// if item == s { -// continue -// } -// result = append(result, item) -// } -// return -// } - -func (r *ReconcileSubscription) cleanSubscriber(subkey string) { +func (r *ReconcileSubscription) cleanSubscriber(subkey string) error { reqLogger := log.WithValues("subkey", subkey) + subscriber := r.subscriberMap[subkey] if subscriber != nil { reqLogger.Info("Cleaning subscriber map and stopping subscriber") - subscriber.Stop() + + err := subscriber.Stop() + delete(r.subscriberMap, subkey) + + return err } + + return nil } //SetStatus set the subscription status func (r *ReconcileSubscription) SetStatus(s *appv1alpha1.HelmChartSubscription, issue error) (reconcile.Result, error) { - srLogger := log.WithValues("HelmRelease.Namespace", s.GetNamespace(), "HelmRelease.Name", s.GetName()) + srLogger := log.WithValues("HelmChartSubscription.Namespace", s.GetNamespace(), "HelmChartSubscription.Name", s.GetName()) //Success if issue == nil { s.Status.Message = "" s.Status.Status = appv1alpha1.HelmChartSubscriptionSuccess s.Status.Reason = "" s.Status.LastUpdateTime = metav1.Now() + err := r.client.Status().Update(context.Background(), s) if err != nil { srLogger.Error(err, "unable to update status") + return reconcile.Result{ RequeueAfter: time.Second, }, nil } + return reconcile.Result{}, nil } + var retryInterval time.Duration - //r.Recorder.Event(s, "Warning", "ProcessingError", issue.Error()) + lastUpdate := s.Status.LastUpdateTime.Time lastPhase := s.Status.Status s.Status.Message = "Error, retrying later" @@ -232,18 +234,21 @@ func (r *ReconcileSubscription) SetStatus(s *appv1alpha1.HelmChartSubscription, err := r.client.Status().Update(context.Background(), s) if err != nil { srLogger.Error(err, "unable to update status") + return reconcile.Result{ RequeueAfter: time.Second, }, nil } + if lastUpdate.IsZero() || lastPhase != appv1alpha1.HelmChartSubscriptionFailed { retryInterval = time.Second } else { - //retryInterval = time.Duration(math.Max(float64(time.Second.Nanoseconds()*2), float64(metav1.Now().Sub(lastUpdate).Round(time.Second).Nanoseconds()))) - retryInterval = s.Status.LastUpdateTime.Sub(lastUpdate).Round(time.Second) + retryInterval = time.Duration(math.Max(float64(time.Second.Nanoseconds()*2), float64(metav1.Now().Sub(lastUpdate).Round(time.Second).Nanoseconds()))) } + requeueAfter := time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))) srLogger.Info("requeueAfter", "->requeueAfter", requeueAfter) + return reconcile.Result{ RequeueAfter: requeueAfter, }, nil diff --git a/pkg/controller/helmrelease/helmrelease_controller.go b/pkg/controller/helmrelease/helmrelease_controller.go index a5d43a5..1fbbd15 100644 --- a/pkg/controller/helmrelease/helmrelease_controller.go +++ b/pkg/controller/helmrelease/helmrelease_controller.go @@ -23,9 +23,6 @@ import ( "reflect" "time" - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - "github.com/IBM/multicloud-operators-subscription-release/pkg/helmreleasemgr" - "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -38,6 +35,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/source" + + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" + "github.com/IBM/multicloud-operators-subscription-release/pkg/helmreleasemgr" + "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" ) var log = logf.Log.WithName("controller_helmrelease") @@ -80,6 +81,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return true }, } + err = c.Watch(&source.Kind{Type: &appv1alpha1.HelmRelease{}}, &handler.EnqueueRequestForObject{}, p) if err != nil { return err @@ -101,8 +103,6 @@ type ReconcileHelmRelease struct { // Reconcile reads that state of the cluster for a HelmRelease object and makes changes based on the state read // and what is in the HelmRelease.Spec -// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates -// a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. @@ -112,6 +112,7 @@ func (r *ReconcileHelmRelease) Reconcile(request reconcile.Request) (reconcile.R // Fetch the HelmRelease instance instance := &appv1alpha1.HelmRelease{} + err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -125,36 +126,45 @@ func (r *ReconcileHelmRelease) Reconcile(request reconcile.Request) (reconcile.R } // Define a new Pod object - err = r.manageSubcriptionRelease(instance) + err = r.manageHelmRelease(instance) + return r.SetStatus(instance, err) } -func (r *ReconcileHelmRelease) manageSubcriptionRelease(sr *appv1alpha1.HelmRelease) error { +func (r *ReconcileHelmRelease) manageHelmRelease(sr *appv1alpha1.HelmRelease) error { srLogger := log.WithValues("HelmRelease.Namespace", sr.Namespace, "SubscrptionRelease.Name", sr.Name) srLogger.Info("chart: ", "sr.Spec.ChartName", sr.Spec.ChartName, "sr.Spec.Version", sr.Spec.Version) + configMap, err := utils.GetConfigMap(r.client, sr.Namespace, sr.Spec.ConfigMapRef) if err != nil { return err } + secret, err := utils.GetSecret(r.client, sr.Namespace, sr.Spec.SecretRef) if err != nil { srLogger.Error(err, "Failed to retrieve secret ", "sr.Spec.SecretRef.Name", sr.Spec.SecretRef.Name) return err } + srLogger.Info("Create Manager") + mgr, err := helmreleasemgr.NewManager(configMap, secret, sr) if err != nil { srLogger.Error(err, "Failed to create NewManager ", "sr.Spec.ChartName", sr.Spec.ChartName) return err } + srLogger.Info("Sync repo") + err = mgr.Sync(context.TODO()) if err != nil { srLogger.Error(err, "Failed to while sync ", "sr.Spec.ChartName", sr.Spec.ChartName) return err } + if mgr.IsInstalled() { srLogger.Info("Update chart", "sr.Spec.ChartName", sr.Spec.ChartName) + _, _, err = mgr.UpdateRelease(context.TODO()) if err != nil { srLogger.Error(err, "Failed to while sync ", "sr.Spec.ChartName", sr.Spec.ChartName) @@ -162,12 +172,14 @@ func (r *ReconcileHelmRelease) manageSubcriptionRelease(sr *appv1alpha1.HelmRele } } else { srLogger.Info("Install chart", "sr.Spec.ChartName", sr.Spec.ChartName) + _, err = mgr.InstallRelease(context.TODO()) if err != nil { srLogger.Error(err, "Failed to while sync ", "sr.Spec.ChartName", sr.Spec.ChartName) return err } } + return nil } @@ -180,17 +192,21 @@ func (r *ReconcileHelmRelease) SetStatus(s *appv1alpha1.HelmRelease, issue error s.Status.Status = appv1alpha1.HelmReleaseSuccess s.Status.Reason = "" s.Status.LastUpdateTime = metav1.Now() + err := r.client.Status().Update(context.Background(), s) if err != nil { srLogger.Error(err, "unable to update status") + return reconcile.Result{ RequeueAfter: time.Second, }, nil } + return reconcile.Result{}, nil } + var retryInterval time.Duration - //r.Recorder.Event(s, "Warning", "ProcessingError", issue.Error()) + lastUpdate := s.Status.LastUpdateTime.Time lastStatus := s.Status.Status s.Status.Message = "Error, retrying later" @@ -201,18 +217,21 @@ func (r *ReconcileHelmRelease) SetStatus(s *appv1alpha1.HelmRelease, issue error err := r.client.Status().Update(context.Background(), s) if err != nil { srLogger.Error(err, "unable to update status") + return reconcile.Result{ RequeueAfter: time.Second, }, nil } + if lastUpdate.IsZero() || lastStatus != appv1alpha1.HelmReleaseFailed { retryInterval = time.Second } else { - //retryInterval = time.Duration(math.Max(float64(time.Second.Nanoseconds()*2), float64(metav1.Now().Sub(lastUpdate).Round(time.Second).Nanoseconds()))) - retryInterval = s.Status.LastUpdateTime.Sub(lastUpdate).Round(time.Second) + retryInterval = time.Duration(math.Max(float64(time.Second.Nanoseconds()*2), float64(metav1.Now().Sub(lastUpdate).Round(time.Second).Nanoseconds()))) } + requeueAfter := time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))) srLogger.Info("requeueAfter", "->requeueAfter", requeueAfter) + return reconcile.Result{ RequeueAfter: requeueAfter, }, nil diff --git a/pkg/controller/helmrelease/helmrelease_controller_suite_test.go b/pkg/controller/helmrelease/helmrelease_controller_suite_test.go index 73840f9..5e4c6c1 100644 --- a/pkg/controller/helmrelease/helmrelease_controller_suite_test.go +++ b/pkg/controller/helmrelease/helmrelease_controller_suite_test.go @@ -20,16 +20,13 @@ import ( stdlog "log" "os" "path/filepath" - "sync" "testing" - "github.com/IBM/multicloud-operators-subscription-release/pkg/apis" - "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/IBM/multicloud-operators-subscription-release/pkg/apis" ) var cfg *rest.Config @@ -41,6 +38,7 @@ func TestMain(m *testing.M) { filepath.Join("..", "..", "..", "vendor", "github.ibm.com", "IBMMulticloudPlatform", "subscriptionrelease", "config", "crds"), }, } + apis.AddToScheme(scheme.Scheme) var err error @@ -49,30 +47,32 @@ func TestMain(m *testing.M) { } code := m.Run() + t.Stop() + os.Exit(code) } // SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and // writes the request to requests after Reconcile is finished. -func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) { - requests := make(chan reconcile.Request) - fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { - result, err := inner.Reconcile(req) - requests <- req - return result, err - }) - return fn, requests -} +// func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) { +// requests := make(chan reconcile.Request) +// fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { +// result, err := inner.Reconcile(req) +// requests <- req +// return result, err +// }) +// return fn, requests +// } // StartTestManager adds recFn -func StartTestManager(mgr manager.Manager, g *gomega.GomegaWithT) (chan struct{}, *sync.WaitGroup) { - stop := make(chan struct{}) - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - g.Expect(mgr.Start(stop)).NotTo(gomega.HaveOccurred()) - }() - return stop, wg -} +// func StartTestManager(mgr manager.Manager, g *gomega.GomegaWithT) (chan struct{}, *sync.WaitGroup) { +// stop := make(chan struct{}) +// wg := &sync.WaitGroup{} +// wg.Add(1) +// go func() { +// defer wg.Done() +// g.Expect(mgr.Start(stop)).NotTo(gomega.HaveOccurred()) +// }() +// return stop, wg +// } diff --git a/pkg/controller/helmrelease/helmrelease_controller_test.go b/pkg/controller/helmrelease/helmrelease_controller_test.go deleted file mode 100644 index 8029427..0000000 --- a/pkg/controller/helmrelease/helmrelease_controller_test.go +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package helmrelease - -import ( - "github.com/IBM/multicloud-operators-subscription-release/pkg/apis" - // "strconv" - "context" - "strings" - - // "archive/tar" - // "bytes" - // "compress/gzip" - "io/ioutil" - "os" - "path/filepath" - "testing" - - // "net/http" - - "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - "github.com/pborman/uuid" - - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - //"sigs.k8s.io/controller-runtime/pkg/client" - //"sigs.k8s.io/controller-runtime/pkg/client/config" - - "github.com/martinlindhe/base36" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - // "k8s.io/apimachinery/pkg/types" - logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" -) - -//This test is not working because the helm-operator needs a config.Config and not a client. -//So the fake client is not passed along. -func TestHelmReleaseReconcileCreate(t *testing.T) { - logf.SetLogger(logf.ZapLogger(true)) - tempDir, _ := ioutil.TempDir("/tmp", "charts") - var ( - name = "multicloud-operators-subscription-release" - namespace = "default" - releaseName = "nginx-ingress" - chartName = "nginx-ingress" - chartsDir = filepath.Join(tempDir, "test") - ) - sr := &v1alpha1.HelmRelease{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "app.ibm.com/v1alpha1", - Kind: "HelmRelease", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - UID: types.UID("89e6052a-d566-11e9-b55f-fa163e0cb658"), - }, - Spec: v1alpha1.HelmReleaseSpec{ - Source: &v1alpha1.Source{ - SourceType: v1alpha1.HelmRepoSourceType, - HelmRepo: &v1alpha1.HelmRepo{ - Urls: []string{"https://helm.nginx.com/stable/nginx-ingress-0.3.5.tgz"}, - }, - }, - ChartName: chartName, - ReleaseName: releaseName, - // Version: "", - }, - } - - // Check if deployment has been created and has the correct size. - dep := &appsv1.Deployment{} - t.Log("sr.GetUID() " + sr.GetUID()) - shorthenUID := shortenUID(sr.GetUID()) - t.Log("shorthenUID " + shorthenUID) - deploymentName := releaseName[0:4] + "-" + shorthenUID + "-" + chartName - namespacedName := types.NamespacedName{ - Namespace: namespace, - Name: deploymentName, - } - - os.Setenv("CHARTS_DIR", chartsDir) - // Objects to track in the fake client. - objs := []runtime.Object{sr} - - // Register operator types with the runtime scheme. - s := scheme.Scheme - s.AddKnownTypes(v1alpha1.SchemeGroupVersion, sr) - apis.AddToScheme(scheme.Scheme) - - // Create a fake client to mock API calls. - cl := fake.NewFakeClient(objs...) - // cfg, err := config.GetConfig() - // if err != nil { - // t.Error(err.Error()) - // } - // cl, err := client.New(cfg, client.Options{ - // Scheme: s, - // }) - // if err != nil { - // t.Error(err.Error()) - // } - // Create a ReconcileMemcached object with the scheme and fake client. - r := &ReconcileHelmRelease{client: cl, scheme: s} - - // Mock request to simulate Reconcile() being called on an event for a - // watched resource . - req := reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: name, - Namespace: namespace, - }, - } - res, err := r.Reconcile(req) - if err != nil { - t.Fatalf("reconcile: (%v)", err) - } - // Check the result of reconciliation to make sure it has the desired state. - if res.Requeue { - t.Error("reconcile did not requeue request as expected") - } - err = r.client.Get(context.TODO(), namespacedName, dep) - if err != nil { - t.Fatalf("get deployment: (%v)", err) - } -} - -// func getReleaseName(cr *unstructured.Unstructured) string { -// return fmt.Sprintf("%s-%s", cr.GetName(), shortenUID(cr.GetUID())) -// } - -func shortenUID(uid types.UID) string { - u := uuid.Parse(string(uid)) - uidBytes, err := u.MarshalBinary() - if err != nil { - return strings.Replace(string(uid), "-", "", -1) - } - return strings.ToLower(base36.EncodeBytes(uidBytes)) -} diff --git a/pkg/helmreleasemgr/subscriptionreleasemgr.go b/pkg/helmreleasemgr/helmreleasemgr.go similarity index 94% rename from pkg/helmreleasemgr/subscriptionreleasemgr.go rename to pkg/helmreleasemgr/helmreleasemgr.go index 5db7b07..b200d3c 100644 --- a/pkg/helmreleasemgr/subscriptionreleasemgr.go +++ b/pkg/helmreleasemgr/helmreleasemgr.go @@ -18,11 +18,8 @@ package helmreleasemgr import ( "io/ioutil" - "os" - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" "github.com/ghodss/yaml" helmrelease "github.com/operator-framework/operator-sdk/pkg/helm/release" corev1 "k8s.io/api/core/v1" @@ -30,16 +27,17 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" -) -//ChartsDir env variable name which contains the directory where the charts are installed -const ChartsDir = "CHARTS_DIR" + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" + "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" +) var log = logf.Log.WithName("helmreleasemgr") //NewManager create a new manager func NewManager(configMap *corev1.ConfigMap, secret *corev1.Secret, s *appv1alpha1.HelmRelease) (helmrelease.Manager, error) { srLogger := log.WithValues("HelmRelease.Namespace", s.Namespace, "HelmRelease.Name", s.Name) + cfg, err := config.GetConfig() if err != nil { return nil, err @@ -63,7 +61,7 @@ func NewManager(configMap *corev1.ConfigMap, secret *corev1.Secret, s *appv1alph return nil, err } - chartsDir := os.Getenv(ChartsDir) + chartsDir := os.Getenv(appv1alpha1.ChartsDir) if chartsDir == "" { chartsDir, err = ioutil.TempDir("/tmp", "charts") if err != nil { @@ -71,22 +69,30 @@ func NewManager(configMap *corev1.ConfigMap, secret *corev1.Secret, s *appv1alph return nil, err } } + chartDir, err := utils.DownloadChart(configMap, secret, chartsDir, s) srLogger.Info("ChartDir", "ChartDir", chartDir) + if err != nil { srLogger.Error(err, "Failed to download the chart") return nil, err } + f := helmrelease.NewManagerFactory(mgr, chartDir) + if s.Spec.Values != "" { var spec interface{} + err = yaml.Unmarshal([]byte(s.Spec.Values), &spec) if err != nil { srLogger.Error(err, "Failed to Unmarshal the values", "values", s.Spec.Values) return nil, err } + o.Object["spec"] = spec } + helmManager, err := f.NewManager(o) + return helmManager, err } diff --git a/pkg/helmreleasemgr/subscriptionrelease_test.go b/pkg/helmreleasemgr/subscriptionrelease_test.go deleted file mode 100644 index 233c707..0000000 --- a/pkg/helmreleasemgr/subscriptionrelease_test.go +++ /dev/null @@ -1,266 +0,0 @@ -/* -Copyright 2019 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helmreleasemgr - -import ( - "testing" -) - -const index = ` -apiVersion: v1 -entries: - ibm-razee-api: - - created: 2019-07-25T14:59:42.541350233Z - description: Razee API - digest: e5a8e6c80c4885af0804f4097a09db7a73d5c153415b5d8d58716e4c661a7799 - icon: https://www.ibm.com/cloud-computing/images/new-cloud/img/cloud.png - keywords: - - amd64 - - DevOps - - Development - - ICP - - Tech - name: ibm-razee-api - tillerVersion: '>=2.4.0' - urls: - - https://mycluster.icp:8443/helm-repo/requiredAssets/ibm-razee-api-0.2.3-015-20190725140717.tgz - version: 0.2.3-015-20190725140717 - - created: 2019-07-25T14:59:42.447720534Z - description: Razee API - digest: ffa1c247827aa06c58999de6df6d36746dfaa9eefae033f565e9aa488829560c - icon: https://www.ibm.com/cloud-computing/images/new-cloud/img/cloud.png - keywords: - - amd64 - - DevOps - - Development - - ICP - - Tech - name: ibm-razee-api - tillerVersion: '>=2.4.0' - urls: - - https://mycluster.icp:8443/helm-repo/requiredAssets/ibm-razee-api-0.2.3-014-20190725131437.tgz - version: 0.2.3-014-20190725131437 - - created: 2019-07-25T14:59:42.446045539Z - description: Razee API - digest: 1510427022f6b2a45e25a1dd5106bf425f038fb828474f64cece545abe23f10a - icon: https://www.ibm.com/cloud-computing/images/new-cloud/img/cloud.png - keywords: - - amd64 - - DevOps - - Development - - ICP - - Tech - name: ibm-razee-api - tillerVersion: '>=2.4.0' - urls: - - https://mycluster.icp:8443/helm-repo/requiredAssets/ibm-razee-api-0.2.3-013-20190725012204.tgz - version: 0.2.3-013-20190725012204 - - created: 2019-07-25T14:59:42.444642983Z - description: Razee API - digest: 8cc226c9f1d1ec472c3f1b58142cb8c3d98b33e4cc5f8fa55b46d3a69a9953cd - icon: https://www.ibm.com/cloud-computing/images/new-cloud/img/cloud.png - keywords: - - amd64 - - DevOps - - Development - - ICP - - Tech - name: ibm-razee-api - tillerVersion: '>=2.4.0' - urls: - - https://mycluster.icp:8443/helm-repo/requiredAssets/ibm-razee-api-0.2.2-013-20190717154729.tgz - version: 0.2.2-013-20190717154729 -generated: 2019-07-25T14:59:42.443201016Z -` - -const sub = `apiVersion: app.ibm.com/v1alpha1 -kind: Subscription -metadata: - annotations: - tillerVersion: 2.4.0 - name: dev-sub-razee - namespace: default - resourceVersion: "1798769" - selfLink: /apis/app.ibm.com/v1alpha1/namespaces/default/subscriptions/dev-sub-razee - uid: 1475377b-aeed-11e9-b55f-fa163e0cb658 -spec: - channel: default/test - name: ibm-razee-api - packageFilter: - annotations: - tillerVersion: 2.4.0 - version: ">0.2.2" - packageOverrides: - - packageName: ibm-razee-api - packageOverrides: - - path: spec.values - value: "RazeeAPI: \n Endpoint: http://9.30.166.165:31311\n ObjectstoreSecretName: - minio\n Region: us-east-1\n" -` - -const sr = ` -apiVersion: app.ibm.com/v1alpha1 -kind: SubscriptionRelease -metadata: - creationTimestamp: 2019-08-06T11:33:55Z - generation: 1 - labels: - app: dev-sub-razee-ope - subscriptionName: dev-sub-razee-ope - subscriptionNamespace: default - name: dev-sub-razee-ope-sr - namespace: default - ownerReferences: - - apiVersion: app.ibm.com/v1alpha1 - blockOwnerDeletion: true - controller: true - kind: Subscription - name: dev-sub-razee-ope - uid: 293b92e4-b763-11e9-b55f-fa163e0cb658 - resourceVersion: "2746558" - selfLink: /apis/app.ibm.com/v1alpha1/namespaces/default/subscriptionreleases/dev-sub-razee-ope-sr - uid: 130a26f2-b83e-11e9-b55f-fa163e0cb658 -spec: - chartName: ibm-razee-api - releaseName: ibm-razee-api - repoUrl: https://mycluster.icp:8443/helm-repo/charts - version: 0.2.3-015-20190725140717 -` - -// const index = `apiVersion: v1 -// entries: -// nginx-ingress: -// - appVersion: 1.5.2 -// created: "2019-07-31T14:29:16.561859185Z" -// description: NGINX Ingress Controller -// digest: 02089cbfc65e684c4943f29c971e4affbffe05fea88328e5c10011c6e2a46da4 -// icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.5.2/deployments/helm-chart/chart-icon.png -// keywords: -// - ingress -// - nginx -// maintainers: -// - email: kubernetes@nginx.com -// name: NGINX Kubernetes Team -// name: nginx-ingress -// sources: -// - https://github.com/nginxinc/kubernetes-ingress/tree/v1.5.2/deployments/helm-chart -// urls: -// - https://helm.nginx.com/stable/nginx-ingress-0.3.2.tgz -// version: 0.3.2 -// - appVersion: 1.5.1 -// created: "2019-07-31T14:29:16.561335791Z" -// description: NGINX Ingress Controller -// digest: 9c59c9ca99c0894a9db24ee6f842bd99304a29ca64b5c57356c1332c701a8e64 -// icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.5.1/deployments/helm-chart/chart-icon.png -// keywords: -// - ingress -// - nginx -// maintainers: -// - email: kubernetes@nginx.com -// name: NGINX Kubernetes Team -// name: nginx-ingress -// sources: -// - https://github.com/nginxinc/kubernetes-ingress/tree/v1.5.1/deployments/helm-chart -// urls: -// - https://helm.nginx.com/stable/nginx-ingress-0.3.1.tgz -// version: 0.3.1 -// - appVersion: 1.5.0 -// created: "2019-07-31T14:29:16.560786527Z" -// description: NGINX Ingress Controller -// digest: c205aaa25a641353f3c255c99b18bafe150267b8dc4a9ac276c1e3dab1cc83ee -// icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.5.0/deployments/helm-chart/chart-icon.png -// keywords: -// - ingress -// - nginx -// maintainers: -// - email: kubernetes@nginx.com -// name: NGINX Kubernetes Team -// name: nginx-ingress -// sources: -// - https://github.com/nginxinc/kubernetes-ingress/tree/v1.5.0/deployments/helm-chart -// urls: -// - https://helm.nginx.com/stable/nginx-ingress-0.3.0.tgz -// version: 0.3.0 -// - appVersion: 1.4.6 -// created: "2019-07-31T14:29:16.560279903Z" -// description: NGINX Ingress Controller -// digest: 1c40fb925dcc19fb24b6af864400642360e188f2ee2b63c029b5441c0a906160 -// icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.4.6/deployments/helm-chart/chart-icon.png -// keywords: -// - ingress -// - nginx -// maintainers: -// - email: kubernetes@nginx.com -// name: NGINX Kubernetes Team -// name: nginx-ingress -// sources: -// - https://github.com/nginxinc/kubernetes-ingress/tree/v1.4.6/deployments/helm-chart -// urls: -// - https://helm.nginx.com/stable/nginx-ingress-0.2.1.tgz -// version: 0.2.1 -// - appVersion: 1.4.5 -// created: "2019-07-31T14:29:16.559837451Z" -// description: NGINX Ingress Controller -// digest: 5c7f7badb8cf5bc7f36f0b770dfd4d232109623e6fbe7fd5907fb82243245c0d -// icon: https://raw.githubusercontent.com/nginxinc/kubernetes-ingress/v1.4.5/deployments/helm-chart/chart-icon.png -// keywords: -// - ingress -// - nginx -// maintainers: -// - email: kubernetes@nginx.com -// name: NGINX Kubernetes Team -// name: nginx-ingress -// sources: -// - https://github.com/nginxinc/kubernetes-ingress/tree/v1.4.5/deployments/helm-chart -// urls: -// - https://helm.nginx.com/stable/nginx-ingress-0.2.0.tgz -// version: 0.2.0 -// generated: "2019-07-31T14:29:16.559225141Z" -// ` -// const sub = `apiVersion: app.ibm.com/v1alpha1 -// kind: Subscription -// metadata: -// name: ngnix -// namespace: default -// spec: -// channel: default/ngnix -// name: nginx-ingress -// packageFilter: -// annotations: -// tillerVersion: 2.4.0 -// version: ">=0.3.1" -// packageOverrides: -// - packageName: nginx-ingress -// packageOverrides: -// - path: spec.values -// value: | -// replicaCount: 2 -// ` - -func TestRelease(t *testing.T) { - // var s appv1alpha1.SubscriptionRelease - - // err := yaml.Unmarshal([]byte(sr), &s) - // assert.NoError(t, err) - // mgr, err := NewManager(nil, s) - // assert.NoError(t, err) - // err = mgr.Sync(context.TODO()) - // assert.NoError(t, err) - // _, err = mgr.InstallRelease(context.TODO()) - // assert.NoError(t, err) - -} diff --git a/pkg/helmreposubscriber/hrsubscriber.go b/pkg/helmreposubscriber/hrsubscriber.go index 36956e3..181f791 100644 --- a/pkg/helmreposubscriber/hrsubscriber.go +++ b/pkg/helmreposubscriber/hrsubscriber.go @@ -20,30 +20,32 @@ import ( "context" "crypto/sha1" "encoding/json" - // gerrors "errors" "fmt" "io/ioutil" "net/http" "net/url" - // "regexp" + "os" + "path/filepath" + "reflect" + "sort" "strings" "time" "github.com/blang/semver" "github.com/ghodss/yaml" - operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" - - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/repo" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" + "github.com/IBM/multicloud-operators-subscription-release/pkg/utils" ) //HelmRepoSubscriber the object thar represent a subscriber of a helmRepo @@ -68,27 +70,39 @@ const DeploymentProcessHelmOperator = "helm-operator" //DeploymentProcessBitnami value to use bitnami as deployment tool const DeploymentProcessBitnami = "bitnami" -//DeploymentProcess -//var DeploymentProcess = DeploymentProcessHelmOperator - // Restart a helm repo subscriber func (s *HelmRepoSubscriber) Restart() error { - subLogger := log.WithValues("method", "Restart", "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger := log.WithValues("method", "Restart", + "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, + "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger.Info("begin") + if s.started { - s.Stop() + err := s.Stop() + if err != nil { + return err + } } + s.stopCh = make(chan struct{}) s.HelmRepoHash = "" approval := strings.ToLower(string(s.HelmChartSubscription.Spec.InstallPlanApproval)) - subLogger.Info("Check start helm-repo monitoring", "s.HelmChartSubscription.Spec.InstallPlanApproval", s.HelmChartSubscription.Spec.InstallPlanApproval) - if approval != "" && approval == strings.ToLower(string(operatorsv1alpha1.ApprovalAutomatic)) { + subLogger.Info("Check start helm-repo monitoring", + "s.HelmChartSubscription.Spec.InstallPlanApproval", s.HelmChartSubscription.Spec.InstallPlanApproval) + + if approval != "" && strings.EqualFold(approval, string(appv1alpha1.ApprovalAutomatic)) { subLogger.Info("Start helm-repo monitoring") + go wait.Until(func() { - s.doHelmChartSubscription() + err := s.doHelmChartSubscription() + if err != nil { + subLogger.Error(err, "Error while managing the helmChartSubscription") + } }, subscriptionPeriod, s.stopCh) + s.started = true } else { err := s.doHelmChartSubscription() @@ -102,24 +116,34 @@ func (s *HelmRepoSubscriber) Restart() error { // Stop a helm repo subscriber func (s *HelmRepoSubscriber) Stop() error { - subLogger := log.WithValues("method", "Stop", "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger := log.WithValues("method", "Stop", + "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, + "Subscrption.Name", s.HelmChartSubscription.Name) subLogger.Info("begin") close(s.stopCh) s.started = false + return nil } // Update a namespace subscriber func (s *HelmRepoSubscriber) Update(sub *appv1alpha1.HelmChartSubscription) error { - subLogger := log.WithValues("method", "Update", "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger := log.WithValues("method", "Update", + "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, + "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger.Info("begin") + s.HelmChartSubscription = sub approval := strings.ToLower(string(s.HelmChartSubscription.Spec.InstallPlanApproval)) + subLogger.Info("InstallPlanApproval", "InstallPlanApproval", approval) - subLogger.Info("ApprovalManual", "ApprovalManual", strings.ToLower(string(operatorsv1alpha1.ApprovalManual))) - if approval == "" || strings.ToLower(string(s.HelmChartSubscription.Spec.InstallPlanApproval)) == strings.ToLower(string(operatorsv1alpha1.ApprovalManual)) { + subLogger.Info("ApprovalManual", "ApprovalManual", strings.ToLower(string(appv1alpha1.ApprovalManual))) + + if approval == "" || strings.EqualFold(string(s.HelmChartSubscription.Spec.InstallPlanApproval), string(appv1alpha1.ApprovalManual)) { return s.Stop() } + return s.Restart() } @@ -128,52 +152,54 @@ func (s *HelmRepoSubscriber) IsStarted() bool { return s.started } -//TODO func (s *HelmRepoSubscriber) doHelmChartSubscription() error { - subLogger := log.WithValues("method", "doHelmChartSubscription", "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + subLogger := log.WithValues("method", "doHelmChartSubscription", + "HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, + "Subscrption.Name", s.HelmChartSubscription.Name) subLogger.Info("start") //Retrieve the helm repo - if s.HelmChartSubscription.Spec.CatalogSource != "" { - s.HelmChartSubscription.Spec.Source = &appv1alpha1.SourceSubscription{ - SourceType: appv1alpha1.HelmRepoSourceType, - HelmRepo: &appv1alpha1.HelmRepoSubscription{ - Urls: []string{s.HelmChartSubscription.Spec.CatalogSource}, - }, - } - } repoURL := s.HelmChartSubscription.Spec.Source.String() subLogger.Info("Source: " + repoURL) subLogger.Info("name: " + s.HelmChartSubscription.GetName()) var indexFile *repo.IndexFile + var hash, url string + var err error switch strings.ToLower(string(s.HelmChartSubscription.Spec.Source.SourceType)) { case string(appv1alpha1.HelmRepoSourceType): - indexFile, hash, err = s.GetHelmRepoIndex() + indexFile, hash, err = s.getHelmRepoIndex() url = fmt.Sprintf("%v", s.HelmChartSubscription.Spec.Source.HelmRepo.Urls) case string(appv1alpha1.GitHubSourceType): - err = fmt.Errorf("Get IndexFile for sourceType '%s' not implemented", appv1alpha1.GitHubSourceType) + indexFile, hash, err = s.generateIndexYAML() + url = fmt.Sprintf("%v", s.HelmChartSubscription.Spec.Source.GitHub.Urls) default: - err = fmt.Errorf("SourceType '%s' unsupported", s.HelmChartSubscription.Spec.Source.SourceType) + err = fmt.Errorf("sourceType '%s' unsupported", s.HelmChartSubscription.Spec.Source.SourceType) } + if err != nil { subLogger.Error(err, "Unable to retrieve the helm repo index ", "url", url) return err } + subLogger.Info("Hashes", "hash", hash, "s.HelmRepoHash", s.HelmRepoHash) + if hash != s.HelmRepoHash { subLogger.Info("HelmRepo changed or subscription changed", "URL", repoURL) + + s.HelmRepoHash = hash + err = s.processHelmChartSubscription(indexFile) if err != nil { subLogger.Error(err, "Error processing subscription") return err } - s.HelmRepoHash = hash } else { subLogger.Info("HelmRepo didn't change", "URL", repoURL) } + return nil } @@ -186,93 +212,227 @@ func (s *HelmRepoSubscriber) processHelmChartSubscription(indexFile *repo.IndexF subLogger.Error(err, "Unable to filter ") return err } + return s.manageHelmChartSubscription(indexFile) } -//GetHelmRepoIndex retreives the index.yaml, loads it into a repo.IndexFile and filters it -func (s *HelmRepoSubscriber) GetHelmRepoIndex() (indexFile *repo.IndexFile, hash string, err error) { +//getHelmRepoIndex retrieves the index.yaml, loads it into a repo.IndexFile and filters it +func (s *HelmRepoSubscriber) getHelmRepoIndex() (indexFile *repo.IndexFile, hash string, err error) { subLogger := log.WithValues("HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) subLogger.Info("begin") + configMap, err := utils.GetConfigMap(s.Client, s.HelmChartSubscription.Namespace, s.HelmChartSubscription.Spec.ConfigMapRef) if err != nil { subLogger.Error(err, "Failed to retrieve configMap ", "s.Spec.ConfigMapRef.Name", s.HelmChartSubscription.Spec.ConfigMapRef.Name) } + httpClient, err := utils.GetHelmRepoClient(s.HelmChartSubscription.Namespace, configMap) if err != nil { - subLogger.Error(err, "Unable to create client for helm repo", "s.HelmChartSubscription.Spec.Source.HelmRepo.Urls", s.HelmChartSubscription.Spec.Source.HelmRepo.Urls) + subLogger.Error(err, "Unable to create client for helm repo", + "s.HelmChartSubscription.Spec.Source.HelmRepo.Urls", s.HelmChartSubscription.Spec.Source.HelmRepo.Urls) } + secret, err := utils.GetSecret(s.Client, s.HelmChartSubscription.Namespace, s.HelmChartSubscription.Spec.SecretRef) if err != nil { subLogger.Error(err, "Failed to retrieve secret ", "s.Spec.SecretRef.Name", s.HelmChartSubscription.Spec.SecretRef.Name) } + for _, url := range s.HelmChartSubscription.Spec.Source.HelmRepo.Urls { cleanRepoURL := strings.TrimSuffix(url, "/") + var req *http.Request + req, err = http.NewRequest(http.MethodGet, cleanRepoURL+"/index.yaml", nil) if err != nil { subLogger.Error(err, "Can not build request: ", "cleanRepoURL", cleanRepoURL) continue } + if secret != nil && secret.Data != nil { if authHeader, ok := secret.Data["authHeader"]; ok { req.Header.Set("Authorization", string(authHeader)) - } else { - if user, ok := secret.Data["user"]; ok { - if password, ok := secret.Data["password"]; ok { - req.SetBasicAuth(string(user), string(password)) - } else { - err = fmt.Errorf("Password not found in secret for basic authentication") - continue - } + } else if user, ok := secret.Data["user"]; ok { + if password, ok := secret.Data["password"]; ok { + req.SetBasicAuth(string(user), string(password)) + } else { + err = fmt.Errorf("password not found in secret for basic authentication") + continue } } } + var resp *http.Response + resp, err = httpClient.Do(req) if err != nil { subLogger.Error(err, "Http request failed: ", "cleanRepoURL", cleanRepoURL) continue } - subLogger.Info("Get suceeded", "cleanRepoURL", cleanRepoURL) + + subLogger.Info("Get succeeded", "cleanRepoURL", cleanRepoURL) + defer resp.Body.Close() + var body []byte + body, err = ioutil.ReadAll(resp.Body) if err != nil { subLogger.Error(err, "Unable to read body: ", "cleanRepoURL", cleanRepoURL) continue } - hash = hashKey(body) + + hash, err = hashKey(body) + if err != nil { + subLogger.Error(err, "Unable to generate hashkey") + continue + } + indexFile, err = LoadIndex(body) if err != nil { subLogger.Error(err, "Unable to parse the indexfile of ", "cleanRepoURL", cleanRepoURL) continue } } + if err != nil { subLogger.Error(err, "All repo URL tested and all failed") return nil, "", err } + return indexFile, hash, err } +func (s *HelmRepoSubscriber) generateIndexYAML() (*repo.IndexFile, string, error) { + subLogger := log.WithValues("HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + + configMap, err := utils.GetConfigMap(s.Client, s.HelmChartSubscription.Namespace, s.HelmChartSubscription.Spec.ConfigMapRef) + if err != nil { + return nil, "", err + } + + secret, err := utils.GetSecret(s.Client, s.HelmChartSubscription.Namespace, s.HelmChartSubscription.Spec.SecretRef) + if err != nil { + subLogger.Error(err, "Failed to retrieve secret ", "sr.HelmChartSubscription.Spec.SecretRef.Name", s.HelmChartSubscription.Spec.SecretRef.Name) + return nil, "", err + } + + chartsDir := os.Getenv(appv1alpha1.ChartsDir) + if chartsDir == "" { + chartsDir, err = ioutil.TempDir("/tmp", "charts") + if err != nil { + subLogger.Error(err, "Can not create tempdir") + return nil, "", err + } + } + + repoRoot, hash, err := utils.DownloadGitHubRepo(configMap, secret, chartsDir, s.HelmChartSubscription) + if err != nil { + subLogger.Error(err, "Failed to download the repo") + return nil, "", err + } + + chartsPath := filepath.Join(repoRoot, s.HelmChartSubscription.Spec.Source.GitHub.ChartsPath) + subLogger.Info("chartsPath", "chartsPath", chartsPath) + + /////////////////////////////////////////////// + // Get chart directories first + /////////////////////////////////////////////// + chartDirs := make(map[string]string) + resourceDirs := make(map[string]string) + + currentChartDir := "NONE" + + err = filepath.Walk(chartsPath, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + subLogger.Info("Ignoring subfolders", "folder", currentChartDir) + if _, err := os.Stat(path + "/Chart.yaml"); err == nil { + subLogger.Info("Found Chart.yaml in ", "dir", path) + if !strings.HasPrefix(path, currentChartDir) { + subLogger.Info("This is a helm chart folder.") + chartDirs[path+"/"] = path + "/" + currentChartDir = path + "/" + } + } else if !strings.HasPrefix(path, currentChartDir) && !strings.HasPrefix(path, repoRoot+"/.git") { + subLogger.Info("This is not a helm chart directory. ", "dir", path) + resourceDirs[path+"/"] = path + "/" + } + } + return nil + }) + if err != nil { + return nil, "", err + } + + ////// + // Generate index.yaml + ///// + + keys := make([]string, 0) + for k := range chartDirs { + keys = append(keys, k) + } + + sort.Strings(keys) + + indexFile := repo.NewIndexFile() + + for _, k := range keys { + chartDir := chartDirs[k] + // for chartDir := range chartDirs { + chartFolderName := filepath.Base(chartDir) + chartParentDir := strings.Split(chartDir, chartFolderName)[0] + // Get the relative parent directory from the git repo root + chartBaseDir := strings.SplitAfter(chartParentDir, chartsPath+"/")[1] + + chartMetadata, err := chartutil.LoadChartfile(chartDir + "Chart.yaml") + if err != nil { + subLogger.Error(err, "There was a problem in generating helm charts index file: ") + return nil, "", err + } + + if !indexFile.Has(chartMetadata.Name, chartMetadata.Version) { + indexFile.Add(chartMetadata, chartFolderName, chartBaseDir, "generated-by-multicloud-operators-subscription") + } + } + + indexFile.SortEntries() + + b, _ := yaml.Marshal(indexFile) + subLogger.Info("New index file ", "content:", string(b), "hash:", hash) + + return indexFile, hash, nil +} + //LoadIndex loads data into a repo.IndexFile func LoadIndex(data []byte) (*repo.IndexFile, error) { i := &repo.IndexFile{} if err := yaml.Unmarshal(data, i); err != nil { return i, err } + i.SortEntries() + if i.APIVersion == "" { return i, repo.ErrNoAPIVersion } + return i, nil } //hashKey Calculate a hash key -func hashKey(b []byte) string { +func hashKey(b []byte) (string, error) { h := sha1.New() - h.Write(b) - return string(h.Sum(nil)) + + _, err := h.Write(b) + if err != nil { + return "", err + } + + return string(h.Sum(nil)), nil } //filterCharts filters the indexFile by name, tillerVersion, version, digest @@ -292,6 +452,7 @@ func (s *HelmRepoSubscriber) filterCharts(indexFile *repo.IndexFile) (err error) subLogger.Error(err, "Failed to takeLatestVersion") return err } + return nil } @@ -299,22 +460,19 @@ func (s *HelmRepoSubscriber) filterCharts(indexFile *repo.IndexFile) (err error) func (s *HelmRepoSubscriber) removeNoMatchingName(indexFile *repo.IndexFile) error { if s.HelmChartSubscription != nil { if s.HelmChartSubscription.Spec.Package != "" { - // r, err := regexp.Compile(s.HelmChartSubscription.Spec.Package) - // if err != nil { - // return err - // } keys := make([]string, 0) for k := range indexFile.Entries { keys = append(keys, k) } + for _, k := range keys { if k != s.HelmChartSubscription.Spec.Package { - // if !r.MatchString(k) { delete(indexFile.Entries, k) } } } } + return nil } @@ -326,14 +484,17 @@ func (s *HelmRepoSubscriber) filterIndexFile(indexFile *repo.IndexFile) { for k := range indexFile.Entries { keys = append(keys, k) } + for _, k := range keys { chartVersions := indexFile.Entries[k] newChartVersions := make([]*repo.ChartVersion, 0) + for index, chartVersion := range chartVersions { if s.checkDigest(chartVersion) && s.checkKeywords(chartVersion) && s.checkTillerVersion(chartVersion) && s.checkVersion(chartVersion) { newChartVersions = append(newChartVersions, chartVersions[index]) } } + if len(newChartVersions) > 0 { indexFile.Entries[k] = newChartVersions } else { @@ -349,6 +510,7 @@ func (s *HelmRepoSubscriber) checkKeywords(chartVersion *repo.ChartVersion) bool if s.HelmChartSubscription.Spec.PackageFilter.Keywords == nil { return true } + for _, filterKeyword := range s.HelmChartSubscription.Spec.PackageFilter.Keywords { for _, chartKeyword := range chartVersion.Keywords { if filterKeyword == chartKeyword { @@ -356,9 +518,11 @@ func (s *HelmRepoSubscriber) checkKeywords(chartVersion *repo.ChartVersion) bool } } } + return false } } + return true } @@ -373,13 +537,14 @@ func (s *HelmRepoSubscriber) checkDigest(chartVersion *repo.ChartVersion) bool { } } } - return true + return true } //checkTillerVersion Checks if the TillerVersion matches func (s *HelmRepoSubscriber) checkTillerVersion(chartVersion *repo.ChartVersion) bool { subLogger := log.WithValues("HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + if s.HelmChartSubscription != nil { if s.HelmChartSubscription.Spec.PackageFilter != nil { if s.HelmChartSubscription.Spec.PackageFilter.Annotations != nil { @@ -391,41 +556,49 @@ func (s *HelmRepoSubscriber) checkTillerVersion(chartVersion *repo.ChartVersion) subLogger.Error(err, "Error while parsing", "tillerVersion: ", tillerVersion, " of ", chartVersion.GetName()) return false } + filterTillerVersion, err := semver.Parse(filterTillerVersion) if err != nil { subLogger.Error(err, "Failed to Parse ", filterTillerVersion) return false } + return tillerVersionVersion(filterTillerVersion) } } } } } + return true } //checkVersion checks if the version matches func (s *HelmRepoSubscriber) checkVersion(chartVersion *repo.ChartVersion) bool { subLogger := log.WithValues("HelmChartSubscription.Namespace", s.HelmChartSubscription.Namespace, "Subscrption.Name", s.HelmChartSubscription.Name) + if s.HelmChartSubscription != nil { if s.HelmChartSubscription.Spec.PackageFilter != nil { if s.HelmChartSubscription.Spec.PackageFilter.Version != "" { version := chartVersion.GetVersion() + versionVersion, err := semver.Parse(version) if err != nil { subLogger.Error(err, "Failed to parse ", version) return false } + filterVersion, err := semver.ParseRange(s.HelmChartSubscription.Spec.PackageFilter.Version) if err != nil { subLogger.Error(err, "Failed to parse range ", "s.HelmChartSubscription.Spec.PackageFilter.Version", s.HelmChartSubscription.Spec.PackageFilter.Version) return false } + return filterVersion(versionVersion) } } } + return true } @@ -433,6 +606,7 @@ func (s *HelmRepoSubscriber) checkVersion(chartVersion *repo.ChartVersion) bool //only the latest is kept. func (s *HelmRepoSubscriber) takeLatestVersion(indexFile *repo.IndexFile) (err error) { indexFile.SortEntries() + for k := range indexFile.Entries { //Get return the latest version when version is empty but //there is a bug in the masterminds semver used by helm @@ -443,8 +617,10 @@ func (s *HelmRepoSubscriber) takeLatestVersion(indexFile *repo.IndexFile) (err e log.Error(err, "Failed to get the latest version") return err } + indexFile.Entries[k] = []*repo.ChartVersion{chartVersion} } + return nil } @@ -463,28 +639,37 @@ func (s *HelmRepoSubscriber) manageHelmChartSubscription(indexFile *repo.IndexFi } // Check if this Pod already exists found := &appv1alpha1.HelmRelease{} + err = s.Client.Get(context.TODO(), types.NamespacedName{Name: sr.Name, Namespace: sr.Namespace}, found) if err != nil { if errors.IsNotFound(err) { - subLogger.Info("Creating a new SubcriptionRelease", "SubcriptionRelease.Namespace", sr.Namespace, "SubcriptionRelease.Name", sr.Name) + subLogger.Info("Creating a new HelmRelease", "HelmRelease.Namespace", sr.Namespace, "HelmRelease.Name", sr.Name) + err = s.Client.Create(context.TODO(), sr) if err != nil { return err } - } else { return err } } else { - subLogger.Info("Update a the SubcriptionRelease", "SubcriptionRelease.Namespace", sr.Namespace, "SubcriptionRelease.Name", sr.Name) - sr.ObjectMeta = found.ObjectMeta - err = s.Client.Update(context.TODO(), sr) - if err != nil { - return err + // sr.ObjectMeta = found.ObjectMeta + // err = s.Client.Update(context.TODO(), sr) + if !reflect.DeepEqual(found.Spec, sr.Spec) { + subLogger.Info("Update a the HelmRelease", "HelmRelease.Namespace", sr.Namespace, "HelmRelease.Name", sr.Name) + subLogger.Info("found", "Spec", found.Spec) + subLogger.Info("sr", "Spec", sr.Spec) + found.Spec = sr.Spec + + err = s.Client.Update(context.TODO(), found) + if err != nil { + return err + } } } } } + return nil } @@ -494,6 +679,7 @@ func (s *HelmRepoSubscriber) newHelmChartHelmReleaseForCR(chartVersion *repo.Cha "app.ibm.com/hosting-deployable": s.HelmChartSubscription.Spec.Channel, "app.ibm.com/hosting-subscription": s.HelmChartSubscription.Namespace + "/" + s.HelmChartSubscription.Name, } + values, err := s.getValues(chartVersion) if err != nil { return nil, err @@ -506,6 +692,7 @@ func (s *HelmRepoSubscriber) newHelmChartHelmReleaseForCR(chartVersion *repo.Cha if err != nil { return nil, err } + if parsedURL.Scheme == "local" { //make sure there is one and only one slash repoURL := strings.TrimSuffix(s.HelmChartSubscription.Spec.Source.HelmRepo.Urls[0], "/") + "/" @@ -520,9 +707,7 @@ func (s *HelmRepoSubscriber) newHelmChartHelmReleaseForCR(chartVersion *repo.Cha Annotations: annotations, }, Spec: appv1alpha1.HelmReleaseSpec{ - Source: &appv1alpha1.Source{ - SourceType: appv1alpha1.HelmRepoSourceType, - }, + Source: &appv1alpha1.Source{}, ConfigMapRef: s.HelmChartSubscription.Spec.ConfigMapRef, SecretRef: s.HelmChartSubscription.Spec.SecretRef, ChartName: chartVersion.Name, @@ -531,18 +716,22 @@ func (s *HelmRepoSubscriber) newHelmChartHelmReleaseForCR(chartVersion *repo.Cha Values: values, }, } + switch strings.ToLower(string(s.HelmChartSubscription.Spec.Source.SourceType)) { case string(appv1alpha1.HelmRepoSourceType): + sr.Spec.Source.SourceType = appv1alpha1.HelmRepoSourceType sr.Spec.Source.HelmRepo = &appv1alpha1.HelmRepo{Urls: chartVersion.URLs} case string(appv1alpha1.GitHubSourceType): + sr.Spec.Source.SourceType = appv1alpha1.GitHubSourceType sr.Spec.Source.GitHub = &appv1alpha1.GitHub{ Urls: s.HelmChartSubscription.Spec.Source.GitHub.Urls, Branch: s.HelmChartSubscription.Spec.Source.GitHub.Branch, - ChartPath: chartVersion.URLs[0], + ChartPath: filepath.Join(s.HelmChartSubscription.Spec.Source.GitHub.ChartsPath, chartVersion.URLs[0]), } default: - return nil, fmt.Errorf("SourceType '%s' unsupported", s.HelmChartSubscription.Spec.Source.SourceType) + return nil, fmt.Errorf("sourceType '%s' unsupported", s.HelmChartSubscription.Spec.Source.SourceType) } + return sr, nil } @@ -554,16 +743,20 @@ func (s *HelmRepoSubscriber) getValues(chartVersion *repo.ChartVersion) (string, if err != nil { return "", err } + var m map[string]interface{} + err = json.Unmarshal(data, &m) if err != nil { return "", err } + if v, ok := m["path"]; ok && v == "spec.values" { return m["value"].(string), nil } } } } + return "", nil } diff --git a/pkg/helmreposubscriber/hrsubscriber_test.go b/pkg/helmreposubscriber/hrsubscriber_test.go index c4fcb1b..e399b10 100644 --- a/pkg/helmreposubscriber/hrsubscriber_test.go +++ b/pkg/helmreposubscriber/hrsubscriber_test.go @@ -19,9 +19,9 @@ package helmreposubscriber import ( "testing" - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" - operatorv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" "github.com/stretchr/testify/assert" + + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" ) const index = `apiVersion: v1 @@ -95,8 +95,10 @@ entries: func TestLoadIndex(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 2, len(chartVersions)) + name := chartVersions[1].GetName() assert.Equal(t, "ibm-cfee-installer", name) } @@ -104,17 +106,17 @@ func TestLoadIndex(t *testing.T) { func Test_MatchingNameCharts(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ - SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{ - Package: "ibm-cfee-installer", - }, + Package: "ibm-cfee-installer", }, }, } s.filterCharts(indexFile) assert.Equal(t, 1, len(indexFile.Entries)) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(chartVersions)) } @@ -122,15 +124,15 @@ func Test_MatchingNameCharts(t *testing.T) { func Test_MatchingWithoutPackageName(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ - Spec: appv1alpha1.HelmChartSubscriptionSpec{ - SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{}, - }, + Spec: appv1alpha1.HelmChartSubscriptionSpec{}, }, } s.filterCharts(indexFile) assert.Equal(t, 3, len(indexFile.Entries)) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(chartVersions)) } @@ -138,6 +140,7 @@ func Test_MatchingWithoutPackageName(t *testing.T) { func Test_MatchingWithoutOPSubscriptionSpec(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{}, @@ -145,6 +148,7 @@ func Test_MatchingWithoutOPSubscriptionSpec(t *testing.T) { } s.filterCharts(indexFile) assert.Equal(t, 3, len(indexFile.Entries)) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(chartVersions)) } @@ -152,11 +156,13 @@ func Test_MatchingWithoutOPSubscriptionSpec(t *testing.T) { func Test_MatchingWithoutSubscriptionSpec(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{}, } s.filterCharts(indexFile) assert.Equal(t, 3, len(indexFile.Entries)) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(chartVersions)) } @@ -164,6 +170,7 @@ func Test_MatchingWithoutSubscriptionSpec(t *testing.T) { func Test_MatchingDigest(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ @@ -177,6 +184,7 @@ func Test_MatchingDigest(t *testing.T) { } s.filterCharts(indexFile) assert.Equal(t, 1, len(indexFile.Entries)) + chartVersions := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(chartVersions)) } @@ -184,6 +192,7 @@ func Test_MatchingDigest(t *testing.T) { func Test_MatchingTillerVersion(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ @@ -197,6 +206,7 @@ func Test_MatchingTillerVersion(t *testing.T) { } s.filterCharts(indexFile) assert.Equal(t, 1, len(indexFile.Entries)) + versionedCharts := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(versionedCharts)) } @@ -204,6 +214,7 @@ func Test_MatchingTillerVersion(t *testing.T) { func Test_MatchingTillerVersionNotFound(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ @@ -215,6 +226,7 @@ func Test_MatchingTillerVersionNotFound(t *testing.T) { }, }, } + s.filterCharts(indexFile) assert.Equal(t, 0, len(indexFile.Entries)) } @@ -222,6 +234,7 @@ func Test_MatchingTillerVersionNotFound(t *testing.T) { func Test_MatchingVersion(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ @@ -233,11 +246,14 @@ func Test_MatchingVersion(t *testing.T) { } s.filterCharts(indexFile) assert.Equal(t, 2, len(indexFile.Entries)) + versionedCharts := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(versionedCharts)) assert.Equal(t, "3.2.0-alpha", versionedCharts[0].GetVersion()) + versionedChartsNil := indexFile.Entries["ibm-mcm-prod"] assert.Nil(t, versionedChartsNil) + versionedCharts = indexFile.Entries["ibm-mcmk-prod"] assert.Equal(t, 1, len(versionedCharts)) } @@ -245,20 +261,28 @@ func Test_MatchingVersion(t *testing.T) { func Test_takeLatestVersion(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{} + err = s.takeLatestVersion(indexFile) assert.NoError(t, err) assert.Equal(t, 3, len(indexFile.Entries)) + versionedCharts := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(versionedCharts)) + versrionedChart := versionedCharts[0] assert.Equal(t, "3.2.0-beta", versrionedChart.GetVersion()) + versionedCharts = indexFile.Entries["ibm-mcm-prod"] assert.Equal(t, 1, len(versionedCharts)) + versrionedChart = versionedCharts[0] assert.Equal(t, "3.1.2", versrionedChart.GetVersion()) + versionedCharts = indexFile.Entries["ibm-mcmk-prod"] assert.Equal(t, 1, len(versionedCharts)) + versrionedChart = versionedCharts[0] assert.Equal(t, "3.1.3", versrionedChart.GetVersion()) } @@ -266,12 +290,11 @@ func Test_takeLatestVersion(t *testing.T) { func Test_filterCharts(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ - SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{ - Package: "ibm-mcm-prod", - }, + Package: "ibm-mcm-prod", PackageFilter: &appv1alpha1.PackageFilter{ Version: ">=3.1.2 <3.2.0", Annotations: map[string]string{ @@ -281,6 +304,7 @@ func Test_filterCharts(t *testing.T) { }, }, } + err = s.filterCharts(indexFile) assert.NoError(t, err) //Zero because no version for tiller >=2.7.3 @@ -290,48 +314,22 @@ func Test_filterCharts(t *testing.T) { func Test_filterChartsLatest(t *testing.T) { indexFile, err := LoadIndex([]byte(index)) assert.NoError(t, err) + s := &HelmRepoSubscriber{ HelmChartSubscription: &appv1alpha1.HelmChartSubscription{ Spec: appv1alpha1.HelmChartSubscriptionSpec{ - SubscriptionSpec: operatorv1alpha1.SubscriptionSpec{ - Package: "ibm-cfee-installer", - }, + Package: "ibm-cfee-installer", }, }, } + err = s.filterCharts(indexFile) assert.NoError(t, err) assert.Equal(t, 1, len(indexFile.Entries)) + versionedCharts := indexFile.Entries["ibm-cfee-installer"] assert.Equal(t, 1, len(versionedCharts)) + versrionedChart := versionedCharts[0] assert.Equal(t, "3.2.0-beta", versrionedChart.GetVersion()) } - -const subWithOverrides = `apiVersion: app.ibm.com/v1alpha1 -kind: Subscription -metadata: - name: dev-sub-with-overrides - namespace: default -spec: - channel: default/test - name: ibm-cfee-installer - packageOverrides: - - packageName: ibm-cfee-installer - packageOverrides: - - path: spec.values - value: | - TestValue: - att1: val1 - att2: val2 -` - -const subWithOutOverrides = `apiVersion: app.ibm.com/v1alpha1 -kind: Subscription -metadata: - name: dev-sub-with-overrides - namespace: default -spec: - channel: default/test - name: ibm-cfee-installer -` diff --git a/pkg/utils/helmrepoutils.go b/pkg/utils/helmrepoutils.go index 7d04561..bbacec5 100644 --- a/pkg/utils/helmrepoutils.go +++ b/pkg/utils/helmrepoutils.go @@ -32,7 +32,6 @@ import ( "strings" "time" - appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" @@ -41,6 +40,8 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + + appv1alpha1 "github.com/IBM/multicloud-operators-subscription-release/pkg/apis/app/v1alpha1" ) var log = logf.Log.WithName("utils") @@ -49,7 +50,6 @@ var log = logf.Log.WithName("utils") func GetHelmRepoClient(parentNamespace string, configMap *corev1.ConfigMap) (*http.Client, error) { srLogger := log.WithValues("package", "utils", "method", "GetHelmRepoClient") - httpClient := http.DefaultClient transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ @@ -65,18 +65,24 @@ func GetHelmRepoClient(parentNamespace string, configMap *corev1.ConfigMap) (*ht InsecureSkipVerify: false, }, } + if configMap != nil { configData := configMap.Data srLogger.Info("ConfigRef retrieved", "configMap.Data", configData) - if configData["insecureSkipVerify"] != "" { - b, err := strconv.ParseBool(configData["insecureSkipVerify"]) + insecureSkipVerify := configData["insecureSkipVerify"] + + if insecureSkipVerify != "" { + b, err := strconv.ParseBool(insecureSkipVerify) if err != nil { if errors.IsNotFound(err) { return nil, nil } - srLogger.Error(err, "Unable to parse", "insecureSkipVerify", configData["insecureSkipVerify"]) + + srLogger.Error(err, "Unable to parse", "insecureSkipVerify", insecureSkipVerify) + return nil, err } + srLogger.Info("Set InsecureSkipVerify", "insecureSkipVerify", b) transport.TLSClientConfig.InsecureSkipVerify = b } else { @@ -85,56 +91,73 @@ func GetHelmRepoClient(parentNamespace string, configMap *corev1.ConfigMap) (*ht } else { srLogger.Info("configMap is nil") } + + httpClient := http.DefaultClient httpClient.Transport = transport srLogger.Info("InsecureSkipVerify equal", "InsecureSkipVerify", transport.TLSClientConfig.InsecureSkipVerify) + return httpClient, nil } //GetConfigMap search the config map containing the helm repo client configuration. func GetConfigMap(client client.Client, parentNamespace string, configMapRef *corev1.ObjectReference) (configMap *corev1.ConfigMap, err error) { srLogger := log.WithValues("package", "utils", "method", "getConfigMap") + if configMapRef != nil { srLogger.Info("Retrieve configMap ", "parentNamespace", parentNamespace, "configMapRef.Name", configMapRef.Name) ns := configMapRef.Namespace + if ns == "" { ns = parentNamespace } + configMap = &corev1.ConfigMap{} + err = client.Get(context.TODO(), types.NamespacedName{Namespace: ns, Name: configMapRef.Name}, configMap) if err != nil { if errors.IsNotFound(err) { srLogger.Error(err, "ConfigMap not found ", "Name:", configMapRef.Name, " on namespace: ", ns) return nil, nil } + srLogger.Error(err, "Failed to get configMap ", "Name:", configMapRef.Name, " on namespace: ", ns) + return nil, err } + srLogger.Info("ConfigMap found ", "Name:", configMapRef.Name, " on namespace: ", ns) } else { srLogger.Info("no configMapRef defined ", "parentNamespace", parentNamespace) } + return configMap, err } //GetSecret returns the secret to access the helm-repo func GetSecret(client client.Client, parentNamespace string, secretRef *corev1.ObjectReference) (secret *corev1.Secret, err error) { srLogger := log.WithValues("package", "utils", "method", "getSecret") + if secretRef != nil { - srLogger.Info("Retreive secret", "parentNamespace", parentNamespace, "secretRef", secretRef) + srLogger.Info("retrieve secret", "parentNamespace", parentNamespace, "secretRef", secretRef) + ns := secretRef.Namespace if ns == "" { ns = parentNamespace } + secret = &corev1.Secret{} + err = client.Get(context.TODO(), types.NamespacedName{Namespace: ns, Name: secretRef.Name}, secret) if err != nil { srLogger.Error(err, "Failed to get secret ", "Name:", secretRef.Name, " on namespace: ", secretRef.Namespace) return nil, err } + srLogger.Info("Secret found ", "Name:", secretRef.Name, " on namespace: ", secretRef.Namespace) } else { srLogger.Info("No secret defined", "parentNamespace", parentNamespace) } + return secret, err } @@ -145,17 +168,19 @@ func DownloadChart(configMap *corev1.ConfigMap, secret *corev1.Secret, chartsDir case string(appv1alpha1.GitHubSourceType): return DownloadChartFromGitHub(configMap, secret, chartsDir, s) default: - return "", fmt.Errorf("SourceType '%s' unsupported", s.Spec.Source.SourceType) + return "", fmt.Errorf("sourceType '%s' unsupported", s.Spec.Source.SourceType) } } //DownloadChartFromGitHub downloads a chart into the charsDir func DownloadChartFromGitHub(configMap *corev1.ConfigMap, secret *corev1.Secret, chartsDir string, s *appv1alpha1.HelmRelease) (chartDir string, err error) { srLogger := log.WithValues("HelmRelease.Namespace", s.Namespace, "SubscrptionRelease.Name", s.Name) + if s.Spec.Source.GitHub == nil { - err := fmt.Errorf("GitHub type but Spec.GitHub is not defined") + err := fmt.Errorf("github type but Spec.GitHub is not defined") return "", err } + if _, err := os.Stat(chartsDir); os.IsNotExist(err) { err := os.MkdirAll(chartsDir, 0755) if err != nil { @@ -163,7 +188,9 @@ func DownloadChartFromGitHub(configMap *corev1.ConfigMap, secret *corev1.Secret, return "", err } } + destRepo := filepath.Join(chartsDir, s.Spec.ReleaseName, s.Namespace, s.Spec.ChartName) + for _, url := range s.Spec.Source.GitHub.Urls { options := &git.CloneOptions{ URL: url, @@ -171,40 +198,54 @@ func DownloadChartFromGitHub(configMap *corev1.ConfigMap, secret *corev1.Secret, SingleBranch: true, RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, } + if secret != nil && secret.Data != nil { srLogger.Info("Add credentials") + options.Auth = &githttp.BasicAuth{ Username: string(secret.Data["user"]), Password: string(secret.Data["password"]), } } + if s.Spec.Source.GitHub.Branch == "" { options.ReferenceName = plumbing.Master } else { options.ReferenceName = plumbing.ReferenceName(s.Spec.Source.GitHub.Branch) } + os.RemoveAll(chartDir) + _, err = git.PlainClone(destRepo, false, options) if err != nil { os.RemoveAll(destRepo) srLogger.Error(err, "Clone failed", "url", url) + continue } } + if err != nil { srLogger.Error(err, "All urls failed") } + chartDir = filepath.Join(destRepo, s.Spec.Source.GitHub.ChartPath) + return chartDir, err } //DownloadChartFromHelmRepo downloads a chart into the charsDir -func DownloadChartFromHelmRepo(configMap *corev1.ConfigMap, secret *corev1.Secret, chartsDir string, s *appv1alpha1.HelmRelease) (chartDir string, err error) { +func DownloadChartFromHelmRepo(configMap *corev1.ConfigMap, + secret *corev1.Secret, + chartsDir string, + s *appv1alpha1.HelmRelease) (chartDir string, err error) { srLogger := log.WithValues("HelmRelease.Namespace", s.Namespace, "SubscrptionRelease.Name", s.Name) + if s.Spec.Source.HelmRepo == nil { - err := fmt.Errorf("HelmRepo type but Spec.HelmRepo is not defined") + err := fmt.Errorf("helmrepo type but Spec.HelmRepo is not defined") return "", err } + if _, err := os.Stat(chartsDir); os.IsNotExist(err) { err := os.MkdirAll(chartsDir, 0755) if err != nil { @@ -212,46 +253,60 @@ func DownloadChartFromHelmRepo(configMap *corev1.ConfigMap, secret *corev1.Secre return "", err } } + httpClient, err := GetHelmRepoClient(s.Namespace, configMap) if err != nil { srLogger.Error(err, "Failed to create httpClient ", "sr.Spec.SecretRef.Name", s.Spec.SecretRef.Name) return "", err } + var downloadErr error + for _, urlelem := range s.Spec.Source.HelmRepo.Urls { var URLP *url.URL + URLP, downloadErr = url.Parse(urlelem) - if err != nil { + if downloadErr != nil { srLogger.Error(downloadErr, "url", urlelem) continue } + fileName := filepath.Base(URLP.Path) // Create the file chartZip := filepath.Join(chartsDir, fileName) if _, err := os.Stat(chartZip); os.IsNotExist(err) { var req *http.Request + req, downloadErr = http.NewRequest(http.MethodGet, urlelem, nil) if downloadErr != nil { srLogger.Error(downloadErr, "Can not build request: ", "urlelem", urlelem) continue } + if secret != nil && secret.Data != nil { req.SetBasicAuth(string(secret.Data["user"]), string(secret.Data["password"])) } + var resp *http.Response + resp, downloadErr = httpClient.Do(req) if downloadErr != nil { srLogger.Error(downloadErr, "Http request failed: ", "urlelem", urlelem) continue } - srLogger.Info("Get suceeded: ", "urlelem", urlelem) + + srLogger.Info("Get succeeded: ", "urlelem", urlelem) + defer resp.Body.Close() + var out *os.File + out, downloadErr = os.Create(chartZip) if downloadErr != nil { srLogger.Error(downloadErr, "Failed to create: ", "chartZip", chartZip) continue } + defer out.Close() // Write the body to file @@ -261,35 +316,121 @@ func DownloadChartFromHelmRepo(configMap *corev1.ConfigMap, secret *corev1.Secre continue } } + var r *os.File + r, downloadErr = os.Open(chartZip) if downloadErr != nil { srLogger.Error(downloadErr, "Failed to open: ", "chartZip", chartZip) continue } + chartDirUnzip := filepath.Join(chartsDir, s.Spec.ReleaseName, s.Namespace) chartDir = filepath.Join(chartDirUnzip, s.Spec.ChartName) //Clean before untar os.RemoveAll(chartDirUnzip) + downloadErr = Untar(chartDirUnzip, r) if downloadErr != nil { //Remove zip because failed to untar and so probably corrupted os.RemoveAll(chartZip) srLogger.Error(downloadErr, "Failed to unzip: ", "chartZip", chartZip) + continue } } + return chartDir, downloadErr } +//DownloadGitHubRepo downloads a github repo into the charsDir +func DownloadGitHubRepo(configMap *corev1.ConfigMap, + secret *corev1.Secret, + chartsDir string, + s *appv1alpha1.HelmChartSubscription) (destRepo string, commitID string, err error) { + srLogger := log.WithValues("HelmRelease.Namespace", s.Namespace, "SubscrptionRelease.Name", s.Name) + + if s.Spec.Source.GitHub == nil { + err := fmt.Errorf("github type but Spec.GitHub is not defined") + return "", "", err + } + + if _, err := os.Stat(chartsDir); os.IsNotExist(err) { + err := os.MkdirAll(chartsDir, 0755) + if err != nil { + srLogger.Error(err, "Unable to create chartDir: ", "chartsDir", chartsDir) + return "", "", err + } + } + + destRepo = filepath.Join(chartsDir, s.Name, s.Namespace) + + for _, url := range s.Spec.Source.GitHub.Urls { + options := &git.CloneOptions{ + URL: url, + Depth: 1, + SingleBranch: true, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, + } + + if secret != nil && secret.Data != nil { + srLogger.Info("Add credentials") + + options.Auth = &githttp.BasicAuth{ + Username: string(secret.Data["user"]), + Password: string(secret.Data["password"]), + } + } + + if s.Spec.Source.GitHub.Branch == "" { + options.ReferenceName = plumbing.Master + } else { + options.ReferenceName = plumbing.ReferenceName(s.Spec.Source.GitHub.Branch) + } + + os.RemoveAll(destRepo) + + r, errClone := git.PlainClone(destRepo, false, options) + + if errClone != nil { + os.RemoveAll(destRepo) + srLogger.Error(errClone, "Clone failed", "url", url) + err = errClone + + continue + } + + h, errHead := r.Head() + + if errHead != nil { + os.RemoveAll(destRepo) + srLogger.Error(errHead, "Get Head failed", "url", url) + err = errHead + + continue + } + + commitID = h.Hash().String() + srLogger.Info("commitID", "commitID", commitID) + } + + if err != nil { + srLogger.Error(err, "All urls failed") + } + + return destRepo, commitID, err +} + //Untar untars the reader into the dst directory func Untar(dst string, r io.Reader) error { srLogger := log.WithValues("destination", dst) + gzr, err := gzip.NewReader(r) if err != nil { srLogger.Error(err, "") return err } + defer gzr.Close() tr := tar.NewReader(gzr) @@ -298,18 +439,12 @@ func Untar(dst string, r io.Reader) error { header, err := tr.Next() switch { - - // if no more files are found return - case err == io.EOF: + case err == io.EOF: // if no more files are found return return nil - - // return any other error - case err != nil: + case err != nil: // return any other error srLogger.Error(err, "") return err - - // if the header is nil, just skip it (not sure how this happens) - case header == nil: + case header == nil: // if the header is nil, just skip it (not sure how this happens) continue } @@ -322,18 +457,14 @@ func Untar(dst string, r io.Reader) error { // check the file type switch header.Typeflag { - - // if its a dir and it doesn't exist create it - case tar.TypeDir: + case tar.TypeDir: // if its a dir and it doesn't exist create it if _, err := os.Stat(target); err != nil { if err := os.MkdirAll(target, 0755); err != nil { srLogger.Error(err, "") return err } } - - // if it's a file create it - case tar.TypeReg: + case tar.TypeReg: // if it's a file create it dir := filepath.Dir(target) if _, err := os.Stat(dir); err != nil { if err := os.MkdirAll(dir, 0755); err != nil { @@ -341,6 +472,7 @@ func Untar(dst string, r io.Reader) error { return err } } + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { srLogger.Error(err, "") diff --git a/tools.go b/tools.go deleted file mode 100644 index 648413f..0000000 --- a/tools.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build tools - -package tools - -import ( - _ "sigs.k8s.io/controller-tools/pkg/crd/generator" -) diff --git a/travis-env.sh b/travis-env.sh deleted file mode 100644 index 255ba80..0000000 --- a/travis-env.sh +++ /dev/null @@ -1,24 +0,0 @@ -# licensed Materials - Property of IBM -# 5737-E67 -# (C) Copyright IBM Corporation 2016, 2019 All Rights Reserved -# US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. - -# Release Tag -if [ "$TRAVIS_BRANCH" = "master" ]; then - RELEASE_TAG=latest -else - RELEASE_TAG="${TRAVIS_BRANCH#release-}-latest" -fi -if [ "$TRAVIS_TAG" != "" ]; then - RELEASE_TAG="${TRAVIS_TAG#v}" -fi -export RELEASE_TAG="$RELEASE_TAG" - -# Release Tag -echo TRAVIS_EVENT_TYPE=$TRAVIS_EVENT_TYPE -echo TRAVIS_BRANCH=$TRAVIS_BRANCH -echo TRAVIS_TAG=$TRAVIS_TAG -echo RELEASE_TAG="$RELEASE_TAG" - -# Set go module on -export GO111MODULE=on