diff --git a/.travis.yml b/.travis.yml index 071b7226cb0..f0d06e7abfb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -149,6 +149,11 @@ jobs: name: Helm on Kubernetes script: make test-e2e-helm + # Build and test helm + - <<: *test + name: Helm on Kubernetes (new) + script: make test-e2e-helm-new + ## Image deploy/push stage jobs ## # Build and deploy arm64 ansible-operator docker image diff --git a/Makefile b/Makefile index 3e9be6e2d5c..9d7ebd56bb9 100644 --- a/Makefile +++ b/Makefile @@ -281,11 +281,14 @@ test-subcommand-olm-install: # E2E tests. .PHONY: test-e2e test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm -test-e2e: test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm ## Run the e2e tests +test-e2e: test-e2e-go test-e2e-ansible test-e2e-ansible-molecule test-e2e-helm test-e2e-helm-new ## Run the e2e tests test-e2e-go: ./hack/tests/e2e-go.sh +test-e2e-helm-new: image-build-helm + ./hack/tests/e2e-helm-new.sh + test-e2e-ansible: image-build-ansible ./hack/tests/e2e-ansible.sh diff --git a/cmd/operator-sdk/cli/cli.go b/cmd/operator-sdk/cli/cli.go index 24d1f251f17..f1bca30451d 100644 --- a/cmd/operator-sdk/cli/cli.go +++ b/cmd/operator-sdk/cli/cli.go @@ -27,6 +27,7 @@ import ( "github.com/operator-framework/operator-sdk/cmd/operator-sdk/version" "github.com/operator-framework/operator-sdk/internal/flags" golangv2 "github.com/operator-framework/operator-sdk/internal/plugins/golang/v2" + helmv1 "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1" "github.com/operator-framework/operator-sdk/internal/util/projutil" log "github.com/sirupsen/logrus" @@ -64,6 +65,7 @@ func GetPluginsCLIAndRoot() (cli.CLI, *cobra.Command) { cli.WithCommandName("operator-sdk"), cli.WithPlugins( &golangv2.Plugin{}, + &helmv1.Plugin{}, ), cli.WithDefaultPlugins( &golangv2.Plugin{}, diff --git a/go.mod b/go.mod index 78529af89a3..f5676209644 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,8 @@ require ( github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 + github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 + github.com/kr/text v0.1.0 github.com/markbates/inflect v1.0.4 github.com/mattn/go-isatty v0.0.12 github.com/mitchellh/go-homedir v1.1.0 @@ -28,6 +29,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.5.1 + github.com/xenolf/lego v2.7.2+incompatible go.uber.org/zap v1.14.1 golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b @@ -55,4 +57,5 @@ replace ( github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.10.0 k8s.io/client-go => k8s.io/client-go v0.18.2 + ) diff --git a/go.sum b/go.sum index ec0127b5b58..f81975882b1 100644 --- a/go.sum +++ b/go.sum @@ -537,8 +537,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww= -github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -927,6 +927,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xenolf/lego v2.7.2+incompatible h1:aGxxYqhnQLQ71HsvEAjJVw6ao14APwPpRk0mpFroPXk= +github.com/xenolf/lego v2.7.2+incompatible/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= diff --git a/hack/tests/e2e-helm-new.sh b/hack/tests/e2e-helm-new.sh new file mode 100755 index 00000000000..fb2d61cd129 --- /dev/null +++ b/hack/tests/e2e-helm-new.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +set -eux + +source hack/lib/test_lib.sh +source hack/lib/image_lib.sh + +DEST_IMAGE="quay.io/example/nginx-operator:v0.0.2" +TMPDIR="$(mktemp -d)" +trap_add 'rm -rf $TMPDIR' EXIT + +test_namespace="nginx-operator-system" + +remove_operator() { + make undeploy + kubectl delete --ignore-not-found namespace ${test_namespace} +} + +test_operator() { + # kind has an issue with certain image registries (ex. redhat's), so use a + # different test pod image. + local metrics_test_image="fedora:latest" + + # wait for operator pod to run + if ! timeout 1m kubectl rollout status deployment.apps/nginx-operator-controller-manager -n ${test_namespace} ; + then + kubectl get events --namespace=${test_namespace} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + # verify that metrics service was created + if ! timeout 60s bash -c -- "until kubectl get service/nginx-operator-metrics --namespace=${test_namespace} > /dev/null 2>&1; do sleep 1; done"; + then + echo "Failed to get metrics service" + kubectl get events --namespace=${test_namespace} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + # give permissions to reach the metrics endpoint + kubectl create clusterrolebinding nginx-operator-system-metrics-reader --clusterrole=nginx-operator-metrics-reader --serviceaccount=nginx-operator-system:default + + # verify that the metrics endpoint exists + if ! timeout 1m bash -c -- "until kubectl run --attach --rm --restart=Never --namespace=${test_namespace} test-metrics --image=${metrics_test_image} -- curl -sfo /dev/null -v -s -k -H Authorization: Bearer `cat /var/run/secrets/kubernetes.io/serviceaccount/token` https://nginx-operator-controller-manager-metrics-service:8443/metrics; do sleep 1; done"; + then + echo "Failed to verify that metrics endpoint exists" + kubectl get events --namespace=${test_namespace} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + # create CR + kubectl create --namespace=${test_namespace} -f config/samples/example_v1alpha1_nginx.yaml + trap_add "kubectl delete --namespace=${test_namespace} --ignore-not-found -f ${OPERATORDIR}/config/samples/example_v1alpha1_nginx.yaml" EXIT + if ! timeout 1m bash -c -- "until kubectl get --namespace=${test_namespace} Nginx nginx-sample -o jsonpath='{..status.deployedRelease.name}' | grep 'nginx-sample'; do sleep 1; done"; + then + kubectl get events --namespace=${test_namespace} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + # verify that the custom resource metrics endpoint exists + if ! timeout 1m bash -c -- "until kubectl run --attach --rm --restart=Never test-cr-metrics --image=${metrics_test_image} -- curl -sfo /dev/null http://nginx-operator-metrics:8686/metrics; do sleep 1; done"; + then + echo "Failed to verify that custom resource metrics endpoint exists" + kubectl logs deployment/nginx-operator + exit 1 + fi + + header_text "verify that the servicemonitor is created" + if ! timeout 1m bash -c -- "until kubectl get servicemonitors/nginx-operator-metrics --namespace=${test_namespace} > /dev/null 2>&1; do sleep 1; done"; + then + error_text "FAIL: Failed to get service monitor" + kubectl get events --namespace=${test_namespace} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + release_name=$(kubectl get --namespace=${test_namespace} Nginx nginx-sample -o jsonpath="{..status.deployedRelease.name}") + nginx_deployment=$(kubectl get --namespace=${test_namespace} deployments -l "app.kubernetes.io/instance=${release_name}" -o jsonpath="{..metadata.name}") + + if ! timeout 1m kubectl rollout --namespace=${test_namespace} status deployment.apps/${nginx_deployment}; + then + kubectl get events --namespace=${test_namespace} + kubectl describe --namespace=${test_namespace} pods -l "app.kubernetes.io/instance=${release_name}" + kubectl describe --namespace=${test_namespace} deployments ${nginx_deployment} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager + exit 1 + fi + + nginx_service=$(kubectl get --namespace=${test_namespace} service -l "app.kubernetes.io/instance=${release_name}" -o jsonpath="{..metadata.name}") + kubectl get --namespace=${test_namespace} service ${nginx_service} + + # scale deployment replicas to 2 and verify the + # deployment automatically scales back down to 1. + kubectl scale --namespace=${test_namespace} deployment.apps/${nginx_deployment} --replicas=2 + if ! timeout 1m bash -c -- "until test \$(kubectl get --namespace=${test_namespace} deployment/${nginx_deployment} -o jsonpath='{..spec.replicas}') -eq 1; do sleep 1; done"; + then + kubectl get events --namespace=${test_namespace} + kubectl describe --namespace=${test_namespace} pods -l "app.kubernetes.io/instance=${release_name}" + kubectl describe --namespace=${test_namespace} deployments ${nginx_deployment} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + # update CR to replicaCount=2 and verify the deployment + # automatically scales up to 2 replicas. + kubectl patch --namespace=${test_namespace} Nginx nginx-sample -p '[{"op":"replace","path":"/spec/replicaCount","value":2}]' --type=json + if ! timeout 1m bash -c -- "until test \$(kubectl get --namespace=${test_namespace} deployment/${nginx_deployment} -o jsonpath='{..spec.replicas}') -eq 2; do sleep 1; done"; + then + kubectl get events --namespace=${test_namespace} + kubectl describe --namespace=${test_namespace} pods -l "app.kubernetes.io/instance=${release_name}" + kubectl describe --namespace=${test_namespace} deployments ${nginx_deployment} + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} + exit 1 + fi + + kubectl delete --namespace=${test_namespace} -f config/samples/example_v1alpha1_nginx.yaml --wait=true + kubectl logs deployment.apps/nginx-operator-controller-manager -c manager --namespace=${test_namespace} | grep "Uninstalled release" | grep "${release_name}" +} + +# create and build the operator + +mkdir nginx-operator +cd nginx-operator +log=$(operator-sdk init --plugins=helm.operator-sdk.io/v1 \ + --domain=com --group=example --version=v1alpha1 --kind=Nginx \ + 2>&1) +echo $log +if echo $log | grep -q "failed to generate RBAC rules"; then + echo FAIL expected successful generation of RBAC rules + exit 1 +fi + +install_service_monitor_crd + +sed -i".bak" -E -e 's/(FROM quay.io\/operator-framework\/helm-operator)(:.*)?/\1:dev/g' Dockerfile; rm -f Dockerfile.bak +make docker-build IMG="$DEST_IMAGE" + +# If using a kind cluster, load the image into all nodes. +load_image_if_kind "$DEST_IMAGE" + +make install +make deploy IMG="$DEST_IMAGE" +OPERATORDIR="$(pwd)" + +# kind has an issue with certain image registries (ex. redhat's), so use a +# different test pod image. +METRICS_TEST_IMAGE="fedora:latest" +docker pull "$METRICS_TEST_IMAGE" +# If using a kind cluster, load the metrics test image into all nodes. +load_image_if_kind "$METRICS_TEST_IMAGE" + +trap_add 'remove_operator' EXIT +test_operator diff --git a/internal/plugins/helm/api.go b/internal/plugins/helm/api.go deleted file mode 100644 index 37ef1b63581..00000000000 --- a/internal/plugins/helm/api.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2020 The Operator-SDK 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 helm - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/util/validation" - "sigs.k8s.io/kubebuilder/pkg/model/config" - "sigs.k8s.io/kubebuilder/pkg/plugin" - - "github.com/operator-framework/operator-sdk/internal/scaffold/helm" - "github.com/operator-framework/operator-sdk/internal/scaffold/input" - "github.com/operator-framework/operator-sdk/internal/util/projutil" -) - -type createAPIPlugin struct { - config *config.Config - - // For help text. - commandName string - - // Helm APIFlags - apiFlags APIFlags -} - -var ( - _ plugin.CreateAPI = &createAPIPlugin{} -) - -func (p *createAPIPlugin) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Create a Kubernetes API by creating a CR and CRD with the Helm Chart package directories.` - p.commandName = ctx.CommandName -} - -func (p *createAPIPlugin) BindFlags(fs *pflag.FlagSet) { - p.apiFlags.AddTo(fs) -} - -func (p *createAPIPlugin) InjectConfig(c *config.Config) { - p.config = c -} - -func (p *createAPIPlugin) Run() error { - if err := p.Validate(); err != nil { - return err - } - if err := p.Scaffold(); err != nil { - return err - } - return nil -} - -func (p *createAPIPlugin) Validate() error { - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error to get the current path: %v", err) - } - projectName := filepath.Base(dir) - - // Check if the project name is a valid k8s object name. - if err := validation.IsDNS1123Label(strings.ToLower(projectName)); err != nil { - return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) - } - - if err := p.apiFlags.Validate(); err != nil { - return err - } - return nil -} - -func (p *createAPIPlugin) Scaffold() error { - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error to get the current path: %v", err) - } - - cfg := input.Config{ - AbsProjectPath: filepath.Join(projutil.MustGetwd()), - ProjectName: filepath.Base(dir), - } - - createOpts := helm.CreateChartOptions{ - ResourceAPIVersion: p.apiFlags.APIVersion, - ResourceKind: p.apiFlags.Kind, - Chart: p.apiFlags.HelmChartRef, - Version: p.apiFlags.HelmChartVersion, - Repo: p.apiFlags.HelmChartRepo, - CRDVersion: p.apiFlags.CRDVersion, - } - - if err := helm.API(cfg, createOpts); err != nil { - return err - } - return nil -} diff --git a/internal/plugins/helm/flags.go b/internal/plugins/helm/flags.go deleted file mode 100644 index d51f69b7ea4..00000000000 --- a/internal/plugins/helm/flags.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2020 The Operator-SDK 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 helm - -import ( - "fmt" - "strings" - - "github.com/spf13/pflag" - - gencrd "github.com/operator-framework/operator-sdk/internal/generate/crd" - sdkscaffold "github.com/operator-framework/operator-sdk/internal/scaffold" -) - -const ( - apiVersion = "api-version" - kind = "kind" - crdVersion = "crd-version" - helmChart = "helm-chart" - helmChartVersion = "helm-chart-version" - helmChartRepo = "helm-chart-repo" -) - -type APIFlags struct { - APIVersion string - Kind string - CRDVersion string - HelmChartRef string - HelmChartVersion string - HelmChartRepo string -} - -// AddTo will add the API flags -func (f *APIFlags) AddTo(fs *pflag.FlagSet) { - fs.StringVar(&f.APIVersion, apiVersion, "", - "Kubernetes apiVersion and has a format of $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)") - fs.StringVar(&f.Kind, kind, "", "Kubernetes resource Kind name. (e.g AppService)") - fs.StringVar(&f.CRDVersion, crdVersion, gencrd.DefaultCRDVersion, - "CRD version to generate") - fs.StringVar(&f.HelmChartRef, helmChart, "", - "Initialize helm operator with existing helm chart (, /, or local path).") - fs.StringVar(&f.HelmChartVersion, helmChartVersion, "", - "Specific version of the helm chart (default is latest version)") - fs.StringVar(&f.HelmChartRepo, helmChartRepo, "", - "Chart repository URL for the requested helm chart") -} - -// Validate will verify the helm API flags -func (f *APIFlags) Validate() error { - if len(strings.TrimSpace(f.HelmChartRef)) == 0 { - if len(strings.TrimSpace(f.HelmChartRepo)) != 0 { - return fmt.Errorf("value of --%s can only be used with --%s", helmChartRepo, helmChart) - } else if len(f.HelmChartVersion) != 0 { - return fmt.Errorf("value of --%s can only be used with --%s", helmChartVersion, helmChart) - } - } - - if len(strings.TrimSpace(f.HelmChartRef)) == 0 { - if len(strings.TrimSpace(f.APIVersion)) == 0 { - return fmt.Errorf("value of --%s must not have empty value", apiVersion) - } - if len(strings.TrimSpace(f.Kind)) == 0 { - return fmt.Errorf("value of --%s must not have empty value", kind) - } - // Validate the resource. - _, err := sdkscaffold.NewResource(f.APIVersion, f.Kind) - if err != nil { - return err - } - } - return nil -} diff --git a/internal/plugins/helm/init.go b/internal/plugins/helm/init.go deleted file mode 100644 index e1c223a1abb..00000000000 --- a/internal/plugins/helm/init.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2020 The Operator-SDK 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 helm - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/util/validation" - "sigs.k8s.io/kubebuilder/pkg/model/config" - "sigs.k8s.io/kubebuilder/pkg/plugin" - - "github.com/operator-framework/operator-sdk/internal/scaffold/helm" - "github.com/operator-framework/operator-sdk/internal/scaffold/input" - "github.com/operator-framework/operator-sdk/internal/util/projutil" -) - -type initPlugin struct { - config *config.Config - - // For help text. - commandName string - - // Helm APIFlags - apiFlags APIFlags -} - -var ( - _ plugin.Init = &initPlugin{} -) - -func (p *initPlugin) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a Helm project with the Helm Chart package directories.` - p.commandName = ctx.CommandName -} - -func (p *initPlugin) BindFlags(fs *pflag.FlagSet) { - p.apiFlags.AddTo(fs) - -} - -func (p *initPlugin) InjectConfig(c *config.Config) { - c.Layout = plugin.KeyFor(Plugin{}) - p.config = c -} - -func (p *initPlugin) Run() error { - if err := p.Validate(); err != nil { - return err - } - if err := p.Scaffold(); err != nil { - return err - } - return nil -} - -func (p *initPlugin) Validate() error { - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error to get the current path: %v", err) - } - projectName := filepath.Base(dir) - - // Check if the project name is a valid k8s object name. - if err := validation.IsDNS1123Label(strings.ToLower(projectName)); err != nil { - return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) - } - - if err := p.apiFlags.Validate(); err != nil { - return err - } - return nil -} - -func (p *initPlugin) Scaffold() error { - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error to get the current path: %v", err) - } - - cfg := input.Config{ - AbsProjectPath: filepath.Join(projutil.MustGetwd()), - ProjectName: filepath.Base(dir), - } - - createOpts := helm.CreateChartOptions{ - ResourceAPIVersion: p.apiFlags.APIVersion, - ResourceKind: p.apiFlags.Kind, - Chart: p.apiFlags.HelmChartRef, - Version: p.apiFlags.HelmChartVersion, - Repo: p.apiFlags.HelmChartRepo, - CRDVersion: p.apiFlags.CRDVersion, - } - - if err := helm.Init(cfg, createOpts); err != nil { - return err - } - return nil -} diff --git a/internal/plugins/helm/v1/api.go b/internal/plugins/helm/v1/api.go new file mode 100644 index 00000000000..84641eb18d7 --- /dev/null +++ b/internal/plugins/helm/v1/api.go @@ -0,0 +1,179 @@ +// Copyright 2020 The Operator-SDK 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 v1 + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/model/resource" + "sigs.k8s.io/kubebuilder/pkg/plugin" + "sigs.k8s.io/kubebuilder/pkg/plugin/scaffold" + + "github.com/operator-framework/operator-sdk/internal/kubebuilder/cmdutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds" + utilplugins "github.com/operator-framework/operator-sdk/internal/util/plugins" +) + +type createAPIPlugin struct { + config *config.Config + + createOptions chartutil.CreateOptions +} + +var ( + _ plugin.CreateAPI = &createAPIPlugin{} + _ cmdutil.RunOptions = &createAPIPlugin{} +) + +// UpdateContext define plugin context +func (p createAPIPlugin) UpdateContext(ctx *plugin.Context) { + ctx.Description = `Scaffold a Kubernetes API that is backed by a Helm chart. +` + ctx.Examples = fmt.Sprintf(` $ %s create api \ + --group=apps --version=v1alpha1 \ + --kind=AppService + + $ %s create api \ + --group=apps --version=v1alpha1 \ + --kind=AppService \ + --helm-chart=myrepo/app + + $ %s create api \ + --helm-chart=myrepo/app + + $ %s create api \ + --helm-chart=myrepo/app \ + --helm-chart-version=1.2.3 + + $ %s create api \ + --helm-chart=app \ + --helm-chart-repo=https://charts.mycompany.com/ + + $ %s create api \ + --helm-chart=app \ + --helm-chart-repo=https://charts.mycompany.com/ \ + --helm-chart-version=1.2.3 + + $ %s create api \ + --helm-chart=/path/to/local/chart-directories/app/ + + $ %s create api \ + --helm-chart=/path/to/local/chart-archives/app-1.2.3.tgz +`, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ctx.CommandName, + ) +} + +const ( + groupFlag = "group" + versionFlag = "version" + kindFlag = "kind" + helmChartFlag = "helm-chart" + helmChartRepoFlag = "helm-chart-repo" + helmChartVersionFlag = "helm-chart-version" + crdVersionFlag = "crd-version" + + crdVersionV1 = "v1" + crdVersionV1beta1 = "v1beta1" +) + +// BindFlags will set the flags for the plugin +func (p *createAPIPlugin) BindFlags(fs *pflag.FlagSet) { + p.createOptions = chartutil.CreateOptions{} + fs.SortFlags = false + + fs.StringVar(&p.createOptions.GVK.Group, groupFlag, "", "resource Group") + fs.StringVar(&p.createOptions.GVK.Version, versionFlag, "", "resource Version") + fs.StringVar(&p.createOptions.GVK.Kind, kindFlag, "", "resource Kind") + + fs.StringVar(&p.createOptions.Chart, helmChartFlag, "", "helm chart") + fs.StringVar(&p.createOptions.Repo, helmChartRepoFlag, "", "helm chart repository") + fs.StringVar(&p.createOptions.Version, helmChartVersionFlag, "", "helm chart versionFlag (default: latest)") + + fs.StringVar(&p.createOptions.CRDVersion, crdVersionFlag, crdVersionV1, "crd versionFlag to generate") +} + +// InjectConfig will inject the PROJECT file/config in the plugin +func (p *createAPIPlugin) InjectConfig(c *config.Config) { + p.config = c +} + +// Run will call the plugin actions according to the definitions done in RunOptions interface +func (p *createAPIPlugin) Run() error { + return cmdutil.Run(p) +} + +// Validate perform the required validations for this plugin +func (p *createAPIPlugin) Validate() error { + if p.createOptions.CRDVersion != crdVersionV1 && p.createOptions.CRDVersion != crdVersionV1beta1 { + return fmt.Errorf("value of --%s must be either %q or %q", crdVersionFlag, crdVersionV1, crdVersionV1beta1) + } + + if len(strings.TrimSpace(p.createOptions.Chart)) == 0 { + if len(strings.TrimSpace(p.createOptions.Repo)) != 0 { + return fmt.Errorf("value of --%s can only be used with --%s", helmChartRepoFlag, helmChartFlag) + } else if len(p.createOptions.Version) != 0 { + return fmt.Errorf("value of --%s can only be used with --%s", helmChartVersionFlag, helmChartFlag) + } + } + + if len(strings.TrimSpace(p.createOptions.Chart)) == 0 { + if len(strings.TrimSpace(p.createOptions.GVK.Group)) == 0 { + return fmt.Errorf("value of --%s must not have empty value", groupFlag) + } + if len(strings.TrimSpace(p.createOptions.GVK.Version)) == 0 { + return fmt.Errorf("value of --%s must not have empty value", versionFlag) + } + if len(strings.TrimSpace(p.createOptions.GVK.Kind)) == 0 { + return fmt.Errorf("value of --%s must not have empty value", kindFlag) + } + + // Validate the resource. + r := resource.Options{ + Namespaced: true, + Group: p.createOptions.GVK.Group, + Version: p.createOptions.GVK.Version, + Kind: p.createOptions.GVK.Kind, + } + if err := r.Validate(); err != nil { + return err + } + } + + return nil +} + +// GetScaffolder returns scaffold.Scaffolder which will be executed due the RunOptions interface implementation +func (p *createAPIPlugin) GetScaffolder() (scaffold.Scaffolder, error) { + return scaffolds.NewAPIScaffolder(p.config, p.createOptions), nil +} + +// PostScaffold runs all actions that should be executed after the default plugin scaffold +func (p *createAPIPlugin) PostScaffold() error { + // Run the specific customizations for SDK + // TODO: rewrite this when plugins phase 2 is implemented. + return utilplugins.WriteSamplesKustomization(p.config) +} diff --git a/internal/plugins/helm/v1/chartutil/chart.go b/internal/plugins/helm/v1/chartutil/chart.go new file mode 100644 index 00000000000..0792b5585d6 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/chart.go @@ -0,0 +1,280 @@ +// Copyright 2018 The Operator-SDK 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 chartutil + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/iancoleman/strcase" + log "github.com/sirupsen/logrus" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kubebuilder/pkg/model/resource" +) + +const ( + + // HelmChartsDir is the relative directory within an SDK project where Helm + // charts are stored. + HelmChartsDir string = "helm-charts" + + // DefaultGroup is the Kubernetes CRD API Group used for fetched + // charts when the --group flag is not specified + DefaultGroup string = "charts" + + // DefaultVersion is the Kubernetes CRD API Version used for fetched + // charts when the --version flag is not specified + DefaultVersion string = "v1alpha1" +) + +// CreateOptions is used to configure how a Helm chart is scaffolded +// for a new Helm operator project. +type CreateOptions struct { + GVK schema.GroupVersionKind + + // Chart is a chart reference for a local or remote chart. + Chart string + + // Repo is a URL to a custom chart repository. + Repo string + + // Version is the version of the chart to fetch. + Version string + + // CRDVersion is the version of the `apiextensions.k8s.io` API which will be used to generate the CRD. + CRDVersion string +} + +// CreateChart scaffolds a new helm chart for the project rooted in projectDir +// based on the passed opts. +// +// It returns a scaffold.Resource that can be used by the caller to create +// other related files. opts.ResourceAPIVersion and opts.ResourceKind are +// used to create the resource and must be specified if opts.Chart is empty. +// +// If opts.Chart is not empty, opts.ResourceAPIVersion and opts.Kind can be +// left unset: opts.ResourceAPIVersion defaults to "charts.helm.k8s.io/v1alpha1" +// and opts.ResourceKind is deduced from the specified opts.Chart. +// +// CreateChart also returns a chart.Chart that references the newly created +// chart. +// +// If opts.Chart is empty, CreateChart scaffolds the default chart from helm's +// default template. +// +// If opts.Chart is a local file, CreateChart verifies that it is a valid helm +// chart archive and unpacks it into the project's helm charts directory. +// +// If opts.Chart is a local directory, CreateChart verifies that it is a valid +// helm chart directory and copies it into the project's helm charts directory. +// +// For any other value of opts.Chart, CreateChart attempts to fetch the helm chart +// from a remote repository. +// +// If opts.Repo is not specified, the following chart reference formats are supported: +// +// - /: Fetch the helm chart named chartName from the helm +// chart repository named repoName, as specified in the +// $HELM_HOME/repositories/repositories.yaml file. +// +// - : Fetch the helm chart archive at the specified URL. +// +// If opts.Repo is specified, only one chart reference format is supported: +// +// - : Fetch the helm chart named chartName in the helm chart repository +// specified by opts.Repo +// +// If opts.Version is not set, CreateChart will fetch the latest available version of +// the helm chart. Otherwise, CreateChart will fetch the specified version. +// opts.Version is not used when opts.Chart itself refers to a specific version, for +// example when it is a local path or a URL. +// +// CreateChart returns an error if an error occurs creating the scaffold.Resource or +// creating the chart. +func CreateChart(projectDir string, opts CreateOptions) (*resource.Options, *chart.Chart, error) { + chartsDir := filepath.Join(projectDir, HelmChartsDir) + err := os.MkdirAll(chartsDir, 0755) + if err != nil { + return nil, nil, fmt.Errorf("failed to create helm-charts directory: %v", err) + } + + var ( + r *resource.Options + c *chart.Chart + ) + + // If we don't have a helm chart reference, scaffold the default chart + // from Helm's default template. Otherwise, fetch it. + if len(opts.Chart) == 0 { + r, c, err = scaffoldChart(chartsDir, opts.GVK.Group, opts.GVK.Version, opts.GVK.Kind) + if err != nil { + return nil, nil, fmt.Errorf("failed to scaffold default chart: %v", err) + } + } else { + r, c, err = fetchChart(chartsDir, opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch chart: %v", err) + } + } + + relChartPath := filepath.Join(HelmChartsDir, c.Name()) + absChartPath := filepath.Join(projectDir, relChartPath) + if err := fetchChartDependencies(absChartPath); err != nil { + return nil, nil, fmt.Errorf("failed to fetch chart dependencies: %v", err) + } + + // Reload chart in case dependencies changed + c, err = loader.Load(absChartPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to load chart: %v", err) + } + + log.Infof("Created %s", relChartPath) + return r, c, nil +} + +func scaffoldChart(destDir, group, version, kind string) (*resource.Options, *chart.Chart, error) { + r := &resource.Options{ + Namespaced: true, + Group: group, + Version: version, + Kind: kind, + } + + chartPath, err := chartutil.Create(strings.ToLower(r.Kind), destDir) + if err != nil { + return nil, nil, err + } + + chart, err := loader.Load(chartPath) + if err != nil { + return nil, nil, err + } + return r, chart, nil +} + +func fetchChart(destDir string, opts CreateOptions) (*resource.Options, *chart.Chart, error) { + var ( + chart *chart.Chart + err error + ) + + if _, err = os.Stat(opts.Chart); err == nil { + chart, err = createChartFromDisk(destDir, opts.Chart) + } else { + chart, err = createChartFromRemote(destDir, opts) + } + if err != nil { + return nil, nil, err + } + + chartName := chart.Name() + if len(opts.GVK.Group) == 0 { + opts.GVK.Group = DefaultGroup + } + if len(opts.GVK.Version) == 0 { + opts.GVK.Version = DefaultVersion + } + if len(opts.GVK.Kind) == 0 { + opts.GVK.Kind = strcase.ToCamel(chartName) + } + + r := &resource.Options{ + Namespaced: true, + Group: opts.GVK.Group, + Version: opts.GVK.Version, + Kind: opts.GVK.Kind, + } + return r, chart, nil +} + +func createChartFromDisk(destDir, source string) (*chart.Chart, error) { + chart, err := loader.Load(source) + if err != nil { + return nil, err + } + + // Save it into our project's helm-charts directory. + if err := chartutil.SaveDir(chart, destDir); err != nil { + return nil, err + } + return chart, nil +} + +func createChartFromRemote(destDir string, opts CreateOptions) (*chart.Chart, error) { + settings := cli.New() + getters := getter.All(settings) + c := downloader.ChartDownloader{ + Out: os.Stderr, + Getters: getters, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + } + + if opts.Repo != "" { + chartURL, err := repo.FindChartInRepoURL(opts.Repo, opts.Chart, opts.Version, "", "", "", getters) + if err != nil { + return nil, err + } + opts.Chart = chartURL + } + + tmpDir, err := ioutil.TempDir("", "osdk-helm-chart") + if err != nil { + return nil, err + } + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Errorf("Failed to remove temporary directory %s: %s", tmpDir, err) + } + }() + + chartArchive, _, err := c.DownloadTo(opts.Chart, opts.Version, tmpDir) + if err != nil { + return nil, err + } + + return createChartFromDisk(destDir, chartArchive) +} + +func fetchChartDependencies(chartPath string) error { + settings := cli.New() + getters := getter.All(settings) + + out := &bytes.Buffer{} + man := &downloader.Manager{ + Out: out, + ChartPath: chartPath, + Getters: getters, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + } + if err := man.Build(); err != nil { + fmt.Println(out.String()) + return err + } + return nil +} diff --git a/internal/plugins/helm/v1/chartutil/chart_test.go b/internal/plugins/helm/v1/chartutil/chart_test.go new file mode 100644 index 00000000000..202bd68b53c --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/chart_test.go @@ -0,0 +1,264 @@ +// Copyright 2020 The Operator-SDK 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 chartutil_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/repo/repotest" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kubebuilder/pkg/model/resource" + + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil" +) + +func TestCreateChart(t *testing.T) { + srv, err := repotest.NewTempServer("testdata/*.tgz") + if err != nil { + t.Fatalf("Failed to create new temp server: %s", err) + } + defer srv.Stop() + + if err := srv.LinkIndices(); err != nil { + t.Fatalf("Failed to link server indices: %s", err) + } + + const ( + chartName = "test-chart" + latestVersion = "1.2.3" + previousVersion = "1.2.0" + nonExistentVersion = "0.0.1" + customGroup = "example.com" + customVersion = "v1" + customKind = "MyApp" + customExpectName = "myapp" + expectDerivedKind = "TestChart" + ) + + testCases := []createChartTestCase{ + { + name: "from scaffold no apiVersion", + expectErr: true, + }, + { + name: "from scaffold no kind", + expectErr: true, + }, + { + name: "version without helm chart", + helmChartVersion: latestVersion, + expectErr: true, + }, + { + name: "repo without helm chart", + helmChartRepo: srv.URL(), + expectErr: true, + }, + { + name: "non-existent version", + helmChart: "test/" + chartName, + helmChartVersion: nonExistentVersion, + expectErr: true, + }, + { + name: "from scaffold with apiVersion and kind", + group: customGroup, + version: customVersion, + kind: customKind, + expectResource: mustNewResource(customGroup, customVersion, customKind), + expectChartName: customExpectName, + expectChartVersion: "0.1.0", + }, + { + name: "from directory", + helmChart: filepath.Join(".", "testdata", chartName), + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from archive", + helmChart: filepath.Join(".", "testdata", fmt.Sprintf("%s-%s.tgz", chartName, latestVersion)), + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from url", + helmChart: fmt.Sprintf("%s/%s-%s.tgz", srv.URL(), chartName, latestVersion), + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name implicit latest", + helmChart: "test/" + chartName, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name implicit latest with apiVersion", + helmChart: "test/" + chartName, + group: customGroup, + version: customVersion, + expectResource: mustNewResource(customGroup, customVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name implicit latest with kind", + helmChart: "test/" + chartName, + kind: customKind, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, customKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name implicit latest with apiVersion and kind", + helmChart: "test/" + chartName, + group: customGroup, + version: customVersion, + kind: customKind, + expectResource: mustNewResource(customGroup, customVersion, customKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name explicit latest", + helmChart: "test/" + chartName, + helmChartVersion: latestVersion, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from repo and name explicit previous", + helmChart: "test/" + chartName, + helmChartVersion: previousVersion, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: previousVersion, + }, + { + name: "from name and repo url implicit latest", + helmChart: chartName, + helmChartRepo: srv.URL(), + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from name and repo url explicit latest", + helmChart: chartName, + helmChartRepo: srv.URL(), + helmChartVersion: latestVersion, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: latestVersion, + }, + { + name: "from name and repo url explicit previous", + helmChart: chartName, + helmChartRepo: srv.URL(), + helmChartVersion: previousVersion, + expectResource: mustNewResource(chartutil.DefaultGroup, chartutil.DefaultVersion, expectDerivedKind), + expectChartName: chartName, + expectChartVersion: previousVersion, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + runTestCase(t, srv.Root(), tc) + }) + } +} + +type createChartTestCase struct { + name string + + group string + version string + kind string + helmChart string + helmChartVersion string + helmChartRepo string + + expectResource *resource.Options + expectChartName string + expectChartVersion string + expectErr bool +} + +func mustNewResource(group, version, kind string) *resource.Options { + r := &resource.Options{ + Namespaced: true, + Group: group, + Version: version, + Kind: kind, + } + return r +} + +func runTestCase(t *testing.T, testDir string, tc createChartTestCase) { + outputDir := filepath.Join(testDir, "output") + assert.NoError(t, os.Mkdir(outputDir, 0755)) + defer os.RemoveAll(outputDir) + + os.Setenv("XDG_CONFIG_HOME", filepath.Join(testDir, ".config")) + os.Setenv("XDG_CACHE_HOME", filepath.Join(testDir, ".cache")) + os.Setenv("HELM_REPOSITORY_CONFIG", filepath.Join(testDir, "repositories.yaml")) + os.Setenv("HELM_REPOSITORY_CACHE", filepath.Join(testDir)) + defer os.Unsetenv("XDG_CONFIG_HOME") + defer os.Unsetenv("XDG_CACHE_HOME") + defer os.Unsetenv("HELM_REPOSITORY_CONFIG") + defer os.Unsetenv("HELM_REPOSITORY_CACHE") + + opts := chartutil.CreateOptions{ + GVK: schema.GroupVersionKind{ + Group: tc.group, + Version: tc.version, + Kind: tc.kind, + }, + Chart: tc.helmChart, + Version: tc.helmChartVersion, + Repo: tc.helmChartRepo, + } + resource, chrt, err := chartutil.CreateChart(outputDir, opts) + if tc.expectErr { + assert.Error(t, err) + return + } + + if !assert.NoError(t, err) { + return + } + assert.Equal(t, tc.expectResource, resource) + assert.Equal(t, tc.expectChartName, chrt.Name()) + assert.Equal(t, tc.expectChartVersion, chrt.Metadata.Version) + + loadedChart, err := loader.Load(filepath.Join(outputDir, chartutil.HelmChartsDir, chrt.Name())) + if err != nil { + t.Fatalf("Could not load chart from expected location: %s", err) + } + + assert.Equal(t, loadedChart, chrt) +} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.0.tgz b/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.0.tgz new file mode 100644 index 00000000000..c32376b22ec Binary files /dev/null and b/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.0.tgz differ diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.3.tgz b/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.3.tgz new file mode 100644 index 00000000000..9e5b271349c Binary files /dev/null and b/internal/plugins/helm/v1/chartutil/testdata/test-chart-1.2.3.tgz differ diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/.helmignore b/internal/plugins/helm/v1/chartutil/testdata/test-chart/.helmignore new file mode 100644 index 00000000000..50af0317254 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/Chart.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/Chart.yaml new file mode 100644 index 00000000000..bbe61e499db --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: test-chart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 1.2.3 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. +appVersion: 1.16.0 diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/NOTES.txt b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/NOTES.txt new file mode 100644 index 00000000000..8fee581ed83 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "test-chart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "test-chart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "test-chart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "test-chart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/_helpers.tpl b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/_helpers.tpl new file mode 100644 index 00000000000..b43406aead9 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "test-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "test-chart.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "test-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "test-chart.labels" -}} +helm.sh/chart: {{ include "test-chart.chart" . }} +{{ include "test-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "test-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "test-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "test-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "test-chart.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/deployment.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/deployment.yaml new file mode 100644 index 00000000000..e581bd0aefb --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "test-chart.fullname" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "test-chart.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "test-chart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "test-chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/ingress.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/ingress.yaml new file mode 100644 index 00000000000..d7d9f64e554 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/ingress.yaml @@ -0,0 +1,37 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "test-chart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/service.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/service.yaml new file mode 100644 index 00000000000..b8d5d516250 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "test-chart.fullname" . }} + labels: + {{- include "test-chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "test-chart.selectorLabels" . | nindent 4 }} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/serviceaccount.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/serviceaccount.yaml new file mode 100644 index 00000000000..65903e18795 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "test-chart.serviceAccountName" . }} + labels: +{{ include "test-chart.labels" . | nindent 4 }} +{{- end -}} diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/tests/test-connection.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/tests/test-connection.yaml new file mode 100644 index 00000000000..3a7fdeb4e8a --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "test-chart.fullname" . }}-test-connection" + labels: +{{ include "test-chart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "test-chart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/internal/plugins/helm/v1/chartutil/testdata/test-chart/values.yaml b/internal/plugins/helm/v1/chartutil/testdata/test-chart/values.yaml new file mode 100644 index 00000000000..5df5f37d626 --- /dev/null +++ b/internal/plugins/helm/v1/chartutil/testdata/test-chart/values.yaml @@ -0,0 +1,66 @@ +# Default values for test-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/internal/plugins/helm/v1/init.go b/internal/plugins/helm/v1/init.go new file mode 100644 index 00000000000..eddd889ad4b --- /dev/null +++ b/internal/plugins/helm/v1/init.go @@ -0,0 +1,183 @@ +// Copyright 2020 The Operator-SDK 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 v1 + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/plugin" + "sigs.k8s.io/kubebuilder/pkg/plugin/scaffold" + + "github.com/operator-framework/operator-sdk/internal/kubebuilder/cmdutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds" + utilplugins "github.com/operator-framework/operator-sdk/internal/util/plugins" +) + +type initPlugin struct { + config *config.Config + apiPlugin createAPIPlugin + doAPIScaffold bool + + // For help text. + commandName string +} + +var ( + _ plugin.Init = &initPlugin{} + _ cmdutil.RunOptions = &initPlugin{} +) + +// UpdateContext define plugin context +func (p *initPlugin) UpdateContext(ctx *plugin.Context) { + ctx.Description = `Initialize a new Helm-based operator project. + +Writes the following files: +- a helm-charts directory with the chart(s) to build releases from +- a watches.yaml file that defines the mapping between your API and a Helm chart +- a PROJECT file with the domain and project layout configuration +- a Makefile to build the project +- a Kustomization.yaml for customizating manifests +- a Patch file for customizing image for manager manifests +- a Patch file for enabling prometheus metrics +` + ctx.Examples = fmt.Sprintf(` $ %s init --plugins=%s \ + --domain=example.com \ + --group=apps --version=v1alpha1 \ + --kind=AppService + + $ %s init --plugins=%s \ + --domain=example.com \ + --group=apps --version=v1alpha1 \ + --kind=AppService \ + --helm-chart=myrepo/app + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=myrepo/app + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=myrepo/app \ + --helm-chart-version=1.2.3 + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=app \ + --helm-chart-repo=https://charts.mycompany.com/ + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=app \ + --helm-chart-repo=https://charts.mycompany.com/ \ + --helm-chart-version=1.2.3 + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=/path/to/local/chart-directories/app/ + + $ %s init --plugins=%s \ + --domain=example.com \ + --helm-chart=/path/to/local/chart-archives/app-1.2.3.tgz +`, + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ctx.CommandName, plugin.KeyFor(Plugin{}), + ) + + p.commandName = ctx.CommandName +} + +// BindFlags will set the flags for the plugin +func (p *initPlugin) BindFlags(fs *pflag.FlagSet) { + fs.SortFlags = false + fs.StringVar(&p.config.Domain, "domain", "my.domain", "domain for groups") + p.apiPlugin.BindFlags(fs) +} + +// InjectConfig will inject the PROJECT file/config in the plugin +func (p *initPlugin) InjectConfig(c *config.Config) { + // v3 project configs get a 'layout' value. + c.Layout = plugin.KeyFor(Plugin{}) + p.config = c + p.apiPlugin.config = p.config +} + +// Run will call the plugin actions +func (p *initPlugin) Run() error { + return cmdutil.Run(p) +} + +// Validate perform the required validations for this plugin +func (p *initPlugin) Validate() error { + // Check if the project name is a valid namespace according to k8s + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error to get the current path: %v", err) + } + projectName := filepath.Base(dir) + if err := validation.IsDNS1123Label(strings.ToLower(projectName)); err != nil { + return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) + } + + defaultOpts := chartutil.CreateOptions{CRDVersion: "v1"} + if !p.apiPlugin.createOptions.GVK.Empty() || p.apiPlugin.createOptions != defaultOpts { + p.doAPIScaffold = true + return p.apiPlugin.Validate() + } + + return nil +} + +// GetScaffolder returns scaffold.Scaffolder which will be executed due the RunOptions interface implementation +func (p *initPlugin) GetScaffolder() (scaffold.Scaffolder, error) { + var ( + apiScaffolder scaffold.Scaffolder + err error + ) + if p.doAPIScaffold { + apiScaffolder, err = p.apiPlugin.GetScaffolder() + if err != nil { + return nil, err + } + } + return scaffolds.NewInitScaffolder(p.config, apiScaffolder), nil +} + +// PostScaffold will run the required actions after the default plugin scaffold +func (p *initPlugin) PostScaffold() error { + // runs the SDK customizations (wrappers) + if err := utilplugins.UpdateMakefile(p.config); err != nil { + return err + } + + if !p.doAPIScaffold { + fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName) + } else { + return p.apiPlugin.PostScaffold() + } + return nil +} diff --git a/internal/plugins/helm/plugin.go b/internal/plugins/helm/v1/plugin.go similarity index 74% rename from internal/plugins/helm/plugin.go rename to internal/plugins/helm/v1/plugin.go index ce2006bae0c..74f5543221c 100644 --- a/internal/plugins/helm/plugin.go +++ b/internal/plugins/helm/v1/plugin.go @@ -12,13 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package helm +package v1 import ( + "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/plugin" + + "github.com/operator-framework/operator-sdk/internal/plugins" ) -const pluginName = "helm" + plugin.DefaultNameQualifier +const pluginName = "helm" + plugins.DefaultNameQualifier + +var ( + supportedProjectVersions = []string{config.Version3Alpha} + pluginVersion = plugin.Version{Number: 1} +) var ( _ plugin.Base = Plugin{} @@ -32,7 +40,7 @@ type Plugin struct { } func (Plugin) Name() string { return pluginName } -func (Plugin) Version() plugin.Version { return plugin.Version{} } -func (Plugin) SupportedProjectVersions() []string { return nil } +func (Plugin) Version() plugin.Version { return pluginVersion } +func (Plugin) SupportedProjectVersions() []string { return supportedProjectVersions } func (p Plugin) GetInitPlugin() plugin.Init { return &p.initPlugin } func (p Plugin) GetCreateAPIPlugin() plugin.CreateAPI { return &p.createAPIPlugin } diff --git a/internal/plugins/helm/v1/scaffolds/api.go b/internal/plugins/helm/v1/scaffolds/api.go new file mode 100644 index 00000000000..28e7117518e --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/api.go @@ -0,0 +1,134 @@ +/* +Copyright 2019 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 scaffolds + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/xenolf/lego/log" + "k8s.io/client-go/discovery" + crconfig "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/model/resource" + "sigs.k8s.io/kubebuilder/pkg/plugin/scaffold" + + "github.com/operator-framework/operator-sdk/internal/kubebuilder/machinery" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates/crd" +) + +var _ scaffold.Scaffolder = &apiScaffolder{} + +// apiScaffolder contains configuration for generating scaffolding for Go type +// representing the API and controller that implements the behavior for the API. +type apiScaffolder struct { + config *config.Config + opts chartutil.CreateOptions +} + +// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations +func NewAPIScaffolder(config *config.Config, opts chartutil.CreateOptions) scaffold.Scaffolder { + return &apiScaffolder{ + config: config, + opts: opts, + } +} + +// Scaffold implements Scaffolder +func (s *apiScaffolder) Scaffold() error { + return s.scaffold() +} + +func (s *apiScaffolder) newUniverse(r *resource.Resource) *model.Universe { + return model.NewUniverse( + model.WithConfig(s.config), + model.WithResource(r), + ) +} + +func (s *apiScaffolder) scaffold() error { + projectDir, err := os.Getwd() + if err != nil { + return err + } + r, chrt, err := chartutil.CreateChart(projectDir, s.opts) + if err != nil { + return err + } + + // Check that resource doesn't exist + if s.config.HasResource(r.GVK()) { + return errors.New("the API resource already exists") + } + // Check that the provided group can be added to the project + if !s.config.MultiGroup && len(s.config.Resources) != 0 && !s.config.HasGroup(r.Group) { + return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s", + "kubebuilder.io/migration/multi-group.html") + } + + res := r.NewResource(s.config, true) + s.config.AddResource(res.GVK()) + + chartPath := filepath.Join(chartutil.HelmChartsDir, chrt.Metadata.Name) + if err := machinery.NewScaffold().Execute( + s.newUniverse(res), + &templates.CRDSample{ChartPath: chartPath, Chart: chrt}, + &templates.CRDEditorRole{}, + &templates.CRDViewerRole{}, + &templates.WatchesUpdater{ChartPath: chartPath}, + &crd.CRD{CRDVersion: s.opts.CRDVersion}, + ); err != nil { + return fmt.Errorf("error scaffolding APIs: %v", err) + } + + if err := machinery.NewScaffold().Execute( + s.newUniverse(res), + &crd.Kustomization{}, + ); err != nil { + return fmt.Errorf("error scaffolding kustomization: %v", err) + } + + // TODO(joelanford): encapsulate this in the role discovery/generation into the scaffold? + roleScaffold := templates.DefaultRoleScaffold + if k8sCfg, err := crconfig.GetConfig(); err != nil { + log.Warnf("Using default RBAC rules: failed to get Kubernetes config: %s", err) + } else if dc, err := discovery.NewDiscoveryClientForConfig(k8sCfg); err != nil { + log.Warnf("Using default RBAC rules: failed to create Kubernetes discovery client: %s", err) + } else { + roleScaffold = templates.GenerateRoleScaffold(dc, chrt) + } + + if err := machinery.NewScaffold().Execute( + s.newUniverse(res), + &roleScaffold, + ); err != nil { + return fmt.Errorf("error scaffolding role: %v", err) + } + + if err = templates.MergeRoleForResource(res, projectDir, roleScaffold); err != nil { + return fmt.Errorf("failed to merge rules in the RBAC manifest for resource (%s/%s, %v): %v", + r.Group, r.Version, r.Kind, err) + } + + return nil +} diff --git a/internal/plugins/helm/v1/scaffolds/doc.go b/internal/plugins/helm/v1/scaffolds/doc.go new file mode 100644 index 00000000000..0ad4bba2c55 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2018 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 scaffolds contains libraries for scaffolding code to use with helm-operator +package scaffolds diff --git a/internal/plugins/helm/v1/scaffolds/init.go b/internal/plugins/helm/v1/scaffolds/init.go new file mode 100644 index 00000000000..bdd3000f545 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/init.go @@ -0,0 +1,115 @@ +/* +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 scaffolds + +import ( + "fmt" + "os" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/plugin/scaffold" + + "github.com/operator-framework/operator-sdk/internal/kubebuilder/machinery" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/chartutil" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates/manager" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates/metricsauth" + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates/prometheus" + "github.com/operator-framework/operator-sdk/version" +) + +const ( + // KustomizeVersion is the kubernetes-sigs/kustomize version to be used in the project + KustomizeVersion = "v3.5.4" + + imageName = "controller:latest" +) + +// HelmOperatorVersion is the version of the helm binary used in the Makefile +var HelmOperatorVersion = strings.TrimSuffix(version.Version, "+git") + +var _ scaffold.Scaffolder = &initScaffolder{} + +type initScaffolder struct { + config *config.Config + apiScaffolder scaffold.Scaffolder +} + +// NewInitScaffolder returns a new Scaffolder for project initialization operations +func NewInitScaffolder(config *config.Config, apiScaffolder scaffold.Scaffolder) scaffold.Scaffolder { + return &initScaffolder{ + config: config, + apiScaffolder: apiScaffolder, + } +} + +func (s *initScaffolder) newUniverse() *model.Universe { + return model.NewUniverse( + model.WithConfig(s.config), + ) +} + +// Scaffold implements Scaffolder +func (s *initScaffolder) Scaffold() error { + switch { + case s.config.IsV3(): + if err := s.scaffold(); err != nil { + return err + } + if s.apiScaffolder != nil { + return s.apiScaffolder.Scaffold() + } + return nil + default: + return fmt.Errorf("unknown project version %v", s.config.Version) + } +} + +func (s *initScaffolder) scaffold() error { + if err := os.MkdirAll(chartutil.HelmChartsDir, 0755); err != nil { + return err + } + return machinery.NewScaffold().Execute( + s.newUniverse(), + &templates.GitIgnore{}, + &templates.AuthProxyRole{}, + &templates.AuthProxyRoleBinding{}, + &metricsauth.AuthProxyPatch{}, + &metricsauth.AuthProxyService{}, + &metricsauth.ClientClusterRole{}, + &manager.Config{Image: imageName}, + &templates.Makefile{ + Image: imageName, + KustomizeVersion: KustomizeVersion, + HelmOperatorVersion: HelmOperatorVersion, + }, + &templates.Dockerfile{ + HelmOperatorVersion: HelmOperatorVersion, + }, + &templates.Kustomize{}, + &templates.ManagerRoleBinding{}, + &templates.LeaderElectionRole{}, + &templates.LeaderElectionRoleBinding{}, + &templates.KustomizeRBAC{}, + &templates.Watches{}, + &manager.Kustomization{}, + &prometheus.Kustomization{}, + &prometheus.ServiceMonitor{}, + ) +} diff --git a/internal/plugins/helm/v1/scaffolds/templates/authproxyrole.go b/internal/plugins/helm/v1/scaffolds/templates/authproxyrole.go new file mode 100644 index 00000000000..e168b11813c --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/authproxyrole.go @@ -0,0 +1,56 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &AuthProxyRole{} + +// AuthProxyRole scaffolds the config/rbac/auth_proxy_role.yaml file +type AuthProxyRole struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *AuthProxyRole) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "auth_proxy_role.yaml") + } + + f.TemplateBody = proxyRoleTemplate + + return nil +} + +const proxyRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/authproxyrolebinding.go b/internal/plugins/helm/v1/scaffolds/templates/authproxyrolebinding.go new file mode 100644 index 00000000000..59de387e42a --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/authproxyrolebinding.go @@ -0,0 +1,55 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &AuthProxyRoleBinding{} + +// AuthProxyRoleBinding scaffolds the config/rbac/auth_proxy_role_binding_rbac.yaml file +type AuthProxyRoleBinding struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *AuthProxyRoleBinding) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "auth_proxy_role_binding.yaml") + } + + f.TemplateBody = proxyRoleBindinggTemplate + + return nil +} + +const proxyRoleBindinggTemplate = `apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: system +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/crd/crd.go b/internal/plugins/helm/v1/scaffolds/templates/crd/crd.go new file mode 100644 index 00000000000..ec7d5a6e63f --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/crd/crd.go @@ -0,0 +1,117 @@ +/* +Copyright 2018 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 crd + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/kr/text" + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &CRD{} + +// CRD scaffolds a manifest for CRD sample. +type CRD struct { + file.TemplateMixin + file.ResourceMixin + + CRDVersion string +} + +// SetTemplateDefaults implements input.Template +func (f *CRD) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "crd", "bases", fmt.Sprintf("%s_%%[plural].yaml", f.Resource.Domain)) + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.IfExistsAction = file.Error + + if f.CRDVersion == "" { + f.CRDVersion = "v1" + } else if f.CRDVersion != "v1" && f.CRDVersion != "v1beta1" { + return errors.New("the CRD version value must be either 'v1' or 'v1beta1'") + } + f.TemplateBody = fmt.Sprintf(crdTemplate, + text.Indent(openAPIV3SchemaTemplate, " "), + text.Indent(openAPIV3SchemaTemplate, " "), + ) + return nil +} + +const crdTemplate = `--- +apiVersion: apiextensions.k8s.io/{{ .CRDVersion }} +kind: CustomResourceDefinition +metadata: + name: {{ .Resource.Plural }}.{{ .Resource.Domain }} +spec: + group: {{ .Resource.Domain }} + names: + kind: {{ .Resource.Kind }} + listKind: {{ .Resource.Kind }}List + plural: {{ .Resource.Plural }} + singular: {{ .Resource.Kind | lower }} + scope: Namespaced +{{- if eq .CRDVersion "v1beta1" }} + subresources: + status: {} + validation: +%s +{{- end }} + versions: + - name: {{ .Resource.Version }} +{{- if eq .CRDVersion "v1" }} + schema: +%s +{{- end }} + served: true + storage: true +{{- if eq .CRDVersion "v1" }} + subresources: + status: {} +{{- end }} +` + +const openAPIV3SchemaTemplate = `openAPIV3Schema: + description: {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of {{ .Resource.Kind }} + type: object + x-kubernetes-preserve-unknown-fields: true + status: + description: Status defines the observed state of {{ .Resource.Kind }} + type: object + x-kubernetes-preserve-unknown-fields: true + type: object +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/crd/kustomization.go b/internal/plugins/helm/v1/scaffolds/templates/crd/kustomization.go new file mode 100644 index 00000000000..ccc32aad964 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/crd/kustomization.go @@ -0,0 +1,86 @@ +/* +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 crd + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Kustomization{} +var _ file.Inserter = &Kustomization{} + +// Kustomization scaffolds the kustomization file in manager folder. +type Kustomization struct { + file.TemplateMixin + file.ResourceMixin +} + +// SetTemplateDefaults implements file.Template +func (f *Kustomization) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "crd", "kustomization.yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = fmt.Sprintf(kustomizationTemplate, + file.NewMarkerFor(f.Path, resourceMarker), + ) + + return nil +} + +const ( + resourceMarker = "crdkustomizeresource" +) + +// GetMarkers implements file.Inserter +func (f *Kustomization) GetMarkers() []file.Marker { + return []file.Marker{ + file.NewMarkerFor(f.Path, resourceMarker), + } +} + +const ( + resourceCodeFragment = `- bases/%s_%s.yaml +` +) + +// GetCodeFragments implements file.Inserter +func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { + fragments := make(file.CodeFragmentsMap, 3) + + // Generate resource code fragments + res := make([]string, 0) + res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.Domain, f.Resource.Plural)) + + // Only store code fragments in the map if the slices are non-empty + if len(res) != 0 { + fragments[file.NewMarkerFor(f.Path, resourceMarker)] = res + } + + return fragments +} + +var kustomizationTemplate = `# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +%s +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/crd_editor_rbac.go b/internal/plugins/helm/v1/scaffolds/templates/crd_editor_rbac.go new file mode 100644 index 00000000000..aa6019f5dc4 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/crd_editor_rbac.go @@ -0,0 +1,69 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &CRDEditorRole{} + +// CRDEditorRole scaffolds the config/rbac/_editor_role.yaml +type CRDEditorRole struct { + file.TemplateMixin + file.ResourceMixin +} + +// SetTemplateDefaults implements input.Template +func (f *CRDEditorRole) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "%[kind]_editor_role.yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = crdRoleEditorTemplate + + return nil +} + +const crdRoleEditorTemplate = `# permissions for end users to edit {{ .Resource.Plural }}. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ lower .Resource.Kind }}-editor-role +rules: +- apiGroups: + - {{ .Resource.Domain }} + resources: + - {{ .Resource.Plural }} + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - {{ .Resource.Domain }} + resources: + - {{ .Resource.Plural }}/status + verbs: + - get +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/crd_sample.go b/internal/plugins/helm/v1/scaffolds/templates/crd_sample.go new file mode 100644 index 00000000000..6fee335982f --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/crd_sample.go @@ -0,0 +1,92 @@ +/* +Copyright 2018 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 templates + +import ( + "fmt" + "path/filepath" + "strings" + "text/template" + + "helm.sh/helm/v3/pkg/chart" + "sigs.k8s.io/kubebuilder/pkg/model/file" + "sigs.k8s.io/yaml" +) + +var _ file.Template = &CRDSample{} +var _ file.UseCustomFuncMap = &CRDSample{} + +// CRDSample scaffolds a manifest for CRD sample. +type CRDSample struct { + file.TemplateMixin + file.ResourceMixin + + ChartPath string + Chart *chart.Chart + Spec string +} + +// SetTemplateDefaults implements input.Template +func (f *CRDSample) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "samples", "%[group]_%[version]_%[kind].yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.IfExistsAction = file.Error + + if len(f.Spec) == 0 { + f.Spec = defaultSpecTemplate + if f.Chart != nil { + spec, err := yaml.Marshal(f.Chart.Values) + if err != nil { + return fmt.Errorf("failed to get chart values: %v", err) + } + comment := "" + if len(f.ChartPath) != 0 { + comment = fmt.Sprintf("# Default values copied from /%s/values.yaml\n", f.ChartPath) + } + f.Spec = fmt.Sprintf("%s%s\n", comment, string(spec)) + } + } + + f.TemplateBody = crdSampleTemplate + return nil +} + +func indent(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) +} + +// GetFuncMap implements file.UseCustomFuncMap +func (f *CRDSample) GetFuncMap() template.FuncMap { + fm := file.DefaultFuncMap() + fm["indent"] = indent + return fm +} + +const defaultSpecTemplate = `foo: bar +` + +const crdSampleTemplate = `apiVersion: {{ .Resource.Domain }}/{{ .Resource.Version }} +kind: {{ .Resource.Kind }} +metadata: + name: {{ lower .Resource.Kind }}-sample +spec: +{{ .Spec | indent 2 }} +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/crd_viewer_rbac.go b/internal/plugins/helm/v1/scaffolds/templates/crd_viewer_rbac.go new file mode 100644 index 00000000000..67c38ae68f7 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/crd_viewer_rbac.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &CRDViewerRole{} + +// CRDViewerRole scaffolds the config/rbac/_viewer_role.yaml +type CRDViewerRole struct { + file.TemplateMixin + file.ResourceMixin +} + +// SetTemplateDefaults implements input.Template +func (f *CRDViewerRole) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "%[kind]_viewer_role.yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = crdRoleViewerTemplate + + return nil +} + +const crdRoleViewerTemplate = `# permissions for end users to view {{ .Resource.Plural }}. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ lower .Resource.Kind }}-viewer-role +rules: +- apiGroups: + - {{ .Resource.Domain }} + resources: + - {{ .Resource.Plural }} + verbs: + - get + - list + - watch +- apiGroups: + - {{ .Resource.Domain }} + resources: + - {{ .Resource.Plural }}/status + verbs: + - get +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/dockerfile.go b/internal/plugins/helm/v1/scaffolds/templates/dockerfile.go new file mode 100644 index 00000000000..94d95e1e8e8 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/dockerfile.go @@ -0,0 +1,52 @@ +/* +Copyright 2019 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 templates + +import ( + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Dockerfile{} + +// Dockerfile scaffolds a Dockerfile for building a main +type Dockerfile struct { + file.TemplateMixin + + // HelmOperatorVersion is the version of the base image and operator binary used in the project + HelmOperatorVersion string +} + +// SetTemplateDefaults implements input.Template +func (f *Dockerfile) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = "Dockerfile" + } + + f.TemplateBody = dockerfileTemplate + + return nil +} + +const dockerfileTemplate = `# Build the manager binary +FROM quay.io/operator-framework/helm-operator:{{.HelmOperatorVersion}} + +ENV HOME=/opt/helm +COPY watches.yaml ${HOME}/watches.yaml +COPY helm-charts ${HOME}/helm-charts +WORKDIR ${HOME} +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/gitignore.go b/internal/plugins/helm/v1/scaffolds/templates/gitignore.go new file mode 100644 index 00000000000..7a91371f982 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/gitignore.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 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 templates + +import ( + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &GitIgnore{} + +// GitIgnore scaffolds the .gitignore file +type GitIgnore struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *GitIgnore) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = ".gitignore" + } + + f.TemplateBody = gitignoreTemplate + + return nil +} + +const gitignoreTemplate = ` +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin + +# Test binary, build with ` + "`go test -c`" + ` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/kustomize.go b/internal/plugins/helm/v1/scaffolds/templates/kustomize.go new file mode 100644 index 00000000000..c094573fc56 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/kustomize.go @@ -0,0 +1,85 @@ +/* +Copyright 2018 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 templates + +import ( + "os" + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Kustomize{} + +// Kustomize scaffolds the Kustomization file for the default overlay +type Kustomize struct { + file.TemplateMixin + + // Prefix to use for name prefix customization + Prefix string +} + +// SetTemplateDefaults implements input.Template +func (f *Kustomize) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "default", "kustomization.yaml") + } + + f.TemplateBody = kustomizeTemplate + + f.IfExistsAction = file.Error + + if f.Prefix == "" { + // use directory name as prefix + dir, err := os.Getwd() + if err != nil { + return err + } + f.Prefix = strings.ToLower(filepath.Base(dir)) + } + + return nil +} + +const kustomizeTemplate = `# Adds namespace to all resources. +namespace: {{ .Prefix }}-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: {{ .Prefix }}- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +- ../rbac +- ../manager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: + # Protect the /metrics endpoint by putting it behind auth. + # If you want your controller-manager to expose the /metrics + # endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrole.go b/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrole.go new file mode 100644 index 00000000000..397438e7233 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrole.go @@ -0,0 +1,68 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &LeaderElectionRole{} + +// LeaderElectionRole scaffolds the config/rbac/leader_election_role.yaml file +type LeaderElectionRole struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *LeaderElectionRole) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "leader_election_role.yaml") + } + + f.TemplateBody = leaderElectionRoleTemplate + + return nil +} + +const leaderElectionRoleTemplate = `# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrolebinding.go b/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrolebinding.go new file mode 100644 index 00000000000..1d6b9b13728 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/leaderelectionrolebinding.go @@ -0,0 +1,55 @@ +/* +Copyright 2018 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &LeaderElectionRoleBinding{} + +// LeaderElectionRoleBinding scaffolds the config/rbac/leader_election_role_binding.yaml file +type LeaderElectionRoleBinding struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *LeaderElectionRoleBinding) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "leader_election_role_binding.yaml") + } + + f.TemplateBody = leaderElectionRoleBindingTemplate + + return nil +} + +const leaderElectionRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: system +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/makefile.go b/internal/plugins/helm/v1/scaffolds/templates/makefile.go new file mode 100644 index 00000000000..bedd21e1a23 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/makefile.go @@ -0,0 +1,136 @@ +/* +Copyright 2019 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 templates + +import ( + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model/file" + + "github.com/operator-framework/operator-sdk/version" +) + +var _ file.Template = &Makefile{} + +// Makefile scaffolds the Makefile +type Makefile struct { + file.TemplateMixin + + // Image is controller manager image name + Image string + + // Kustomize version to use in the project + KustomizeVersion string + + // HelmOperatorVersion is the version of the base image and operator binary used in the project + HelmOperatorVersion string +} + +// SetTemplateDefaults implements input.Template +func (f *Makefile) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = "Makefile" + } + + f.TemplateBody = makefileTemplate + + f.IfExistsAction = file.Error + + if f.Image == "" { + f.Image = "controller:latest" + } + + if f.KustomizeVersion == "" { + f.KustomizeVersion = "v3.5.4" + } + + if f.HelmOperatorVersion == "" { + f.HelmOperatorVersion = strings.TrimSuffix(version.Version, "+git") + } + + return nil +} + +const makefileTemplate = ` +# Image URL to use all building/pushing image targets +IMG ?= {{ .Image }} + +all: docker-build + +# Run against the configured Kubernetes cluster in ~/.kube/config +run: helm-operator + $(HELM_OPERATOR) run + +# Install CRDs into a cluster +install: kustomize + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +# Uninstall CRDs from a cluster +uninstall: kustomize + $(KUSTOMIZE) build config/crd | kubectl delete -f - + +# Deploy controller in the configured Kubernetes cluster in ~/.kube/config +deploy: kustomize + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +# Undeploy controller in the configured Kubernetes cluster in ~/.kube/config +undeploy: kustomize + $(KUSTOMIZE) build config/default | kubectl delete -f - + +# Build the docker image +docker-build: + docker build . -t ${IMG} + +# Push the docker image +docker-push: + docker push ${IMG} + +PATH := $(PATH):$(PWD)/bin +SHELL := env PATH=$(PATH) /bin/sh +OS = $(shell uname -s | tr '[:upper:]' '[:lower:]') +ARCH = $(shell uname -m | sed 's/x86_64/amd64/') +OSOPER = $(shell uname -s | tr '[:upper:]' '[:lower:]' | sed 's/darwin/apple-darwin/' | sed 's/linux/linux-gnu/') +ARCHOPER = $(shell uname -m ) + +kustomize: +ifeq (, $(shell which kustomize 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p bin ;\ + curl -sSLo - https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/{{ .KustomizeVersion }}/kustomize_{{ .KustomizeVersion }}_$(OS)_$(ARCH).tar.gz | tar xzf - -C bin/ ;\ + } +KUSTOMIZE=./bin/kustomize +else +KUSTOMIZE=$(shell which kustomize) +endif + +helm-operator: +ifeq (, $(shell which helm-operator 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p bin ;\ + curl -LO https://github.com/operator-framework/operator-sdk/releases/download/{{ .HelmOperatorVersion }}/helm-operator-{{ .HelmOperatorVersion }}-$(ARCHOPER)-$(OSOPER) ;\ + mv helm-operator-{{ .HelmOperatorVersion }}-$(ARCHOPER)-$(OSOPER) ./bin/helm-operator ;\ + chmod +x ./bin/helm-operator ;\ + } +HELM_OPERATOR=./bin/helm-operator +else +HELM_OPERATOR=$(shell which helm-operator) +endif +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/manager/config.go b/internal/plugins/helm/v1/scaffolds/templates/manager/config.go new file mode 100644 index 00000000000..880d495b4d2 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/manager/config.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 manager + +import ( + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Config{} + +// Config scaffolds yaml config for the manager. +type Config struct { + file.TemplateMixin + + // Image is controller manager image name + Image string + + // OperatorName will be used to create the pods + OperatorName string +} + +// SetTemplateDefaults implements input.Template +func (f *Config) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "manager", "manager.yaml") + } + + f.TemplateBody = configTemplate + + if f.OperatorName == "" { + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error to get the current path: %v", err) + } + f.OperatorName = filepath.Base(dir) + } + return nil +} + +// todo(camilamacedo86): add the arg --enable-leader-election for the manager +// More info: https://github.com/operator-framework/operator-sdk/issues/3356 + +// todo(camilamacedo86): add the arg --metrics-addr for the manager +// More info: https://github.com/operator-framework/operator-sdk/issues/3358 + +const configTemplate = `apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - manager + image: {{ .Image }} + name: manager + resources: + limits: + cpu: 100m + memory: 90Mi + requests: + cpu: 100m + memory: 60Mi + env: + - name: WATCH_NAMESPACE + value: "" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: {{ .OperatorName }} + terminationGracePeriodSeconds: 10 +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/manager/kustomization.go b/internal/plugins/helm/v1/scaffolds/templates/manager/kustomization.go new file mode 100644 index 00000000000..84af806d0e1 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/manager/kustomization.go @@ -0,0 +1,47 @@ +/* +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 manager + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Kustomization{} + +// Kustomization scaffolds the Kustomization file in manager folder. +type Kustomization struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *Kustomization) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "manager", "kustomization.yaml") + } + + f.TemplateBody = kustomizeManagerTemplate + + f.IfExistsAction = file.Error + + return nil +} + +const kustomizeManagerTemplate = `resources: +- manager.yaml +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/metricsauth/auth_proxy_patch.go b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/auth_proxy_patch.go new file mode 100644 index 00000000000..1ea25886b0f --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/auth_proxy_patch.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 The Kubernetes Authors. +Modifications copyright 2020 The Operator-SDK 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 metricsauth + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &AuthProxyPatch{} + +// AuthProxyPatch scaffolds the patch file for enabling +// prometheus metrics for manager Pod. +type AuthProxyPatch struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *AuthProxyPatch) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "default", "manager_auth_proxy_patch.yaml") + } + + f.TemplateBody = kustomizeAuthProxyPatchTemplate + + f.IfExistsAction = file.Error + + return nil +} + +// todo(camilamacedo86): add the arg --enable-leader-election for the manager +// More info: https://github.com/operator-framework/operator-sdk/issues/3356 + +// todo(camilamacedo86): add the arg --metrics-addr for the manager +// More info: https://github.com/operator-framework/operator-sdk/issues/3358 + +const kustomizeAuthProxyPatchTemplate = `# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8383/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/metricsauth/authproxyservice.go b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/authproxyservice.go new file mode 100644 index 00000000000..0021a5072df --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/authproxyservice.go @@ -0,0 +1,57 @@ +/* +Copyright 2018 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 metricsauth + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &AuthProxyService{} + +// AuthProxyService scaffolds the config/rbac/auth_proxy_service.yaml file +type AuthProxyService struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *AuthProxyService) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "auth_proxy_service.yaml") + } + + f.TemplateBody = authProxyServiceTemplate + + return nil +} + +const authProxyServiceTemplate = `apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/metricsauth/clientclusterrole.go b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/clientclusterrole.go new file mode 100644 index 00000000000..06de93bed3e --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/metricsauth/clientclusterrole.go @@ -0,0 +1,50 @@ +/* +Copyright 2018 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 metricsauth + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &ClientClusterRole{} + +// ClientClusterRole scaffolds the config/rbac/client_clusterrole.yaml file +type ClientClusterRole struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *ClientClusterRole) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "auth_proxy_client_clusterrole.yaml") + } + + f.TemplateBody = clientClusterRoleTemplate + + return nil +} + +const clientClusterRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: ["/metrics"] + verbs: ["get"] +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/mgrrolebinding.go b/internal/plugins/helm/v1/scaffolds/templates/mgrrolebinding.go new file mode 100644 index 00000000000..07af8c57edf --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/mgrrolebinding.go @@ -0,0 +1,55 @@ +/* +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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &ManagerRoleBinding{} + +// ManagerRoleBinding scaffolds the config/rbac/role_binding.yaml file +type ManagerRoleBinding struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *ManagerRoleBinding) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "role_binding.yaml") + } + + f.TemplateBody = managerBindingTemplate + + return nil +} + +const managerBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: system +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/prometheus/kustomize.go b/internal/plugins/helm/v1/scaffolds/templates/prometheus/kustomize.go new file mode 100644 index 00000000000..a630cddcf29 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/prometheus/kustomize.go @@ -0,0 +1,45 @@ +/* +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 prometheus + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Kustomization{} + +// Kustomization scaffolds the kustomizaiton in the prometheus folder +type Kustomization struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *Kustomization) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "prometheus", "kustomization.yaml") + } + + f.TemplateBody = kustomizationTemplate + + return nil +} + +const kustomizationTemplate = `resources: +- monitor.yaml +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/prometheus/monitor.go b/internal/plugins/helm/v1/scaffolds/templates/prometheus/monitor.go new file mode 100644 index 00000000000..d32402f15aa --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/prometheus/monitor.go @@ -0,0 +1,59 @@ +/* +Copyright 2020 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 prometheus + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &ServiceMonitor{} + +// ServiceMonitor scaffolds an issuer CR and a certificate CR +type ServiceMonitor struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *ServiceMonitor) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "prometheus", "monitor.yaml") + } + + f.TemplateBody = serviceMonitorTemplate + + return nil +} + +const serviceMonitorTemplate = ` +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + selector: + matchLabels: + control-plane: controller-manager +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/rbac.go b/internal/plugins/helm/v1/scaffolds/templates/rbac.go new file mode 100644 index 00000000000..39b146e9c2d --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/rbac.go @@ -0,0 +1,57 @@ +/* +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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &KustomizeRBAC{} + +// KustomizeRBAC scaffolds the Kustomization file in rbac folder. +type KustomizeRBAC struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *KustomizeRBAC) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "kustomization.yaml") + } + + f.TemplateBody = kustomizeRBACTemplate + + f.IfExistsAction = file.Error + + return nil +} + +const kustomizeRBACTemplate = `resources: +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml +` diff --git a/internal/plugins/helm/v1/scaffolds/templates/role.go b/internal/plugins/helm/v1/scaffolds/templates/role.go new file mode 100644 index 00000000000..9b0dece12da --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/role.go @@ -0,0 +1,506 @@ +// Copyright 2019 The Operator-SDK Authors +// Modifications copyright 2020 The Operator-SDK 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 templates + +import ( + "errors" + "fmt" + "io/ioutil" + "path/filepath" + "sort" + "strings" + + log "github.com/sirupsen/logrus" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/releaseutil" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/kubebuilder/pkg/model/file" + "sigs.k8s.io/kubebuilder/pkg/model/resource" + "sigs.k8s.io/yaml" +) + +var _ file.Template = &Role{} + +// Role scaffolds the config/rbac/auth_proxy_role.yaml file +type Role struct { + file.TemplateMixin + + SkipDefaultRules bool + CustomRules []rbacv1.PolicyRule +} + +// SetTemplateDefaults implements input.Template +func (f *Role) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("config", "rbac", "role.yaml") + } + + f.TemplateBody = roleTemplate + + return nil +} + +// todo(camilamacedo86): remove the roles added after the {{- end }} +// These roles were added because we are using the Helm pkg current implementation which +// requires the permissions for the metrics. +// More info: https://github.com/operator-framework/operator-sdk/issues/3354 + +const roleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +{{- if not .SkipDefaultRules }} +- apiGroups: + - "" + resources: + - pods + - services + - services/finalizers + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +{{- end }} +{{- range .CustomRules }} +- verbs: + {{- range .Verbs }} + - "{{ . }}" + {{- end }} + {{- with .APIGroups }} + apiGroups: + {{- range . }} + - "{{ . }}" + {{- end }} + {{- end }} + {{- with .Resources }} + resources: + {{- range . }} + - "{{ . }}" + {{- end }} + {{- end }} + {{- with .ResourceNames }} + resourceNames: + {{- range . }} + - "{{ . }}" + {{- end }} + {{- end }} + {{- with .NonResourceURLs }} + nonResourceURLs: + {{- range . }} + - "{{ . }}" + {{- end }} + {{- end }} +{{- end }} +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - "get" + - "create" +- apiGroups: + - apps + resources: + - deployments/finalizers + resourceNames: + - "" + verbs: + - "update" +- apiGroups: + - "" + resources: + - pods + verbs: + - get +- apiGroups: + - apps + resources: + - replicasets + - deployments + verbs: + - get +` + +// roleDiscoveryInterface is an interface that contains just the discovery +// methods needed by the Helm role scaffold generator. Requiring just this +// interface simplifies testing. +type roleDiscoveryInterface interface { + ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) +} + +var DefaultRoleScaffold = Role{ + SkipDefaultRules: false, + CustomRules: []rbacv1.PolicyRule{ + // We need this rule so the operator can read namespaces to ensure they exist + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get"}, + }, + + // We need this rule for leader election and release state storage to work + { + APIGroups: []string{""}, + Resources: []string{"configmaps", "secrets"}, + Verbs: []string{rbacv1.VerbAll}, + }, + + // We need this rule for creating Kubernetes events + { + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"create"}, + }, + }, +} + +// GenerateRoleScaffold generates a role scaffold from the provided helm chart. It +// renders a release manifest using the chart's default values and uses the Kubernetes +// discovery API to lookup each resource in the resulting manifest. +// The role scaffold will have IsClusterScoped=true if the chart lists cluster scoped resources +func GenerateRoleScaffold(dc roleDiscoveryInterface, chart *chart.Chart) Role { + log.Info("Generating RBAC rules") + + roleScaffold := DefaultRoleScaffold + clusterResourceRules, namespacedResourceRules, err := generateRoleRules(dc, chart) + if err != nil { + log.Warnf("Using default RBAC rules: failed to generate RBAC rules: %s", err) + return roleScaffold + } + + roleScaffold.SkipDefaultRules = true + roleScaffold.CustomRules = append(roleScaffold.CustomRules, append(clusterResourceRules, + namespacedResourceRules...)...) + + log.Warn("The RBAC rules generated in config/rbac/role.yaml are based on the chart's default manifest." + + " Some rules may be missing for resources that are only enabled with custom values, and" + + " some existing rules may be overly broad. Double check the rules generated in config/rbac/role.yaml" + + " to ensure they meet the operator's permission requirements.") + + return roleScaffold +} + +func generateRoleRules(dc roleDiscoveryInterface, chart *chart.Chart) ([]rbacv1.PolicyRule, + []rbacv1.PolicyRule, error) { + _, serverResources, err := dc.ServerGroupsAndResources() + if err != nil { + return nil, nil, fmt.Errorf("failed to get server resources: %v", err) + } + + manifests, err := getDefaultManifests(chart) + if err != nil { + return nil, nil, fmt.Errorf("failed to get default manifest: %v", err) + } + + // Use maps of sets of resources, keyed by their group. This helps us + // de-duplicate resources within a group as we traverse the manifests. + clusterGroups := map[string]map[string]struct{}{} + namespacedGroups := map[string]map[string]struct{}{} + + for _, m := range manifests { + name := m.Name + content := strings.TrimSpace(m.Content) + + // Ignore NOTES.txt, helper manifests, and empty manifests. + b := filepath.Base(name) + if b == "NOTES.txt" { + continue + } + if strings.HasPrefix(b, "_") { + continue + } + if content == "" || content == "---" { + continue + } + + // Extract the gvk from the template + resource := unstructured.Unstructured{} + err := yaml.Unmarshal([]byte(content), &resource) + if err != nil { + log.Warnf("Skipping rule generation for %s. Failed to parse manifest: %s", name, err) + continue + } + groupVersion := resource.GetAPIVersion() + group := resource.GroupVersionKind().Group + kind := resource.GroupVersionKind().Kind + + // If we don't have the group or the kind, we won't be able to + // create a valid role rule, log a warning and continue. + if groupVersion == "" { + log.Warnf("Skipping rule generation for %s. Failed to determine resource apiVersion.", name) + continue + } + if kind == "" { + log.Warnf("Skipping rule generation for %s. Failed to determine resource kind.", name) + continue + } + + if resourceName, namespaced, ok := getResource(serverResources, groupVersion, kind); ok { + if !namespaced { + if clusterGroups[group] == nil { + clusterGroups[group] = map[string]struct{}{} + } + clusterGroups[group][resourceName] = struct{}{} + } else { + if namespacedGroups[group] == nil { + namespacedGroups[group] = map[string]struct{}{} + } + namespacedGroups[group][resourceName] = struct{}{} + } + } else { + log.Warnf("Skipping rule generation for %s. Failed to determine resource scope for %s.", + name, resource.GroupVersionKind()) + continue + } + } + + // convert map[string]map[string]struct{} to []rbacv1.PolicyRule + clusterRules := buildRulesFromGroups(clusterGroups) + namespacedRules := buildRulesFromGroups(namespacedGroups) + + return clusterRules, namespacedRules, nil +} + +func getDefaultManifests(c *chart.Chart) ([]releaseutil.Manifest, error) { + install := action.NewInstall(&action.Configuration{}) + install.DryRun = true + install.ReleaseName = "RELEASE-NAME" + install.Replace = true + install.ClientOnly = true + rel, err := install.Run(c, nil) + if err != nil { + return nil, fmt.Errorf("failed to render chart templates: %v", err) + } + _, manifests, err := releaseutil.SortManifests(releaseutil.SplitManifests(rel.Manifest), + chartutil.DefaultVersionSet, releaseutil.InstallOrder) + return manifests, err +} + +func getResource(namespacedResourceList []*metav1.APIResourceList, groupVersion, kind string) (string, bool, bool) { + for _, apiResourceList := range namespacedResourceList { + if apiResourceList.GroupVersion == groupVersion { + for _, apiResource := range apiResourceList.APIResources { + if apiResource.Kind == kind { + return apiResource.Name, apiResource.Namespaced, true + } + } + } + } + return "", false, false +} + +func buildRulesFromGroups(groups map[string]map[string]struct{}) []rbacv1.PolicyRule { + rules := []rbacv1.PolicyRule{} + for group, resourceNames := range groups { + resources := []string{} + for resource := range resourceNames { + resources = append(resources, resource) + } + sort.Strings(resources) + rules = append(rules, rbacv1.PolicyRule{ + APIGroups: []string{group}, + Resources: resources, + Verbs: []string{rbacv1.VerbAll}, + }) + } + return rules +} + +func UpdateRoleForResource(r *resource.Resource, absProjectPath string) error { + // append rbac rule to deploy/role.yaml + roleFilePath := filepath.Join(absProjectPath, "config", "rbac", "role.yaml") + roleYAML, err := ioutil.ReadFile(roleFilePath) + if err != nil { + return fmt.Errorf("failed to read role manifest %v: %v", roleFilePath, err) + } + obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(roleYAML, nil, nil) + if err != nil { + return fmt.Errorf("failed to decode role manifest %v: %v", roleFilePath, err) + } + switch role := obj.(type) { + case *rbacv1.ClusterRole: + pr := &rbacv1.PolicyRule{} + apiGroupFound := false + for i := range role.Rules { + if role.Rules[i].APIGroups[0] == r.Domain { + apiGroupFound = true + pr = &role.Rules[i] + break + } + } + // check if the resource already exists + for _, resource := range pr.Resources { + if resource == r.Plural { + log.Infof("RBAC rules in deploy/role.yaml already up to date for the resource (%s/%s, %v)", r.Group, r.Version, r.Kind) + return nil + } + } + + pr.Resources = append(pr.Resources, r.Plural) + // create a new apiGroup if not found. + if !apiGroupFound { + pr.APIGroups = []string{r.Domain} + // Using "*" to allow access to the resource and all its subresources e.g "memcacheds" and "memcacheds/finalizers" + // https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement + pr.Resources = []string{"*"} + pr.Verbs = []string{ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch", + } + role.Rules = append(role.Rules, *pr) + } + + return updateRoleFile(&role, roleFilePath) + default: + return errors.New("failed to parse role.yaml as a ClusterRole") + } +} + +// MergeRoleForResource merges incoming new API resource rules with existing deploy/role.yaml +func MergeRoleForResource(r *resource.Resource, absProjectPath string, roleScaffold Role) error { + roleFilePath := filepath.Join(absProjectPath, "config", "rbac", "role.yaml") + roleYAML, err := ioutil.ReadFile(roleFilePath) + if err != nil { + return fmt.Errorf("failed to read role manifest %v: %v", roleFilePath, err) + } + // Check for existing role.yaml + if len(roleYAML) == 0 { + return fmt.Errorf("empty Role File at: %v", absProjectPath) + } + // Check for incoming new Role + if len(roleScaffold.CustomRules) == 0 { + return fmt.Errorf("customRules cannot be empty for new Role at: %s/%s", r.Group, r.Version) + } + + obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(roleYAML, nil, nil) + if err != nil { + return fmt.Errorf("failed to decode role manifest %v: %v", roleFilePath, err) + } + switch role := obj.(type) { + case *rbacv1.ClusterRole: + mergedClusterRoleRules := mergeRules(role.Rules, roleScaffold) + role.Rules = mergedClusterRoleRules + default: + log.Errorf("Failed to parse role.yaml as a role %v", err) + } + + if err := updateRoleFile(obj, roleFilePath); err != nil { + return fmt.Errorf("failed to update for resource (%s/%s, %v): %v", + r.Group, r.Version, r.Kind, err) + } + + return UpdateRoleForResource(r, absProjectPath) +} + +func ifMatches(pr []string, newPr []string) bool { + + sort.Strings(pr) + sort.Strings(newPr) + + if len(pr) != len(newPr) { + return false + } + for i, v := range pr { + if v != newPr[i] { + return false + } + } + return true +} + +func findResource(resources []string, search string) bool { + for _, r := range resources { + if r == search || r == "*" { + return true + } + } + return false +} + +func mergeRules(rules1 []rbacv1.PolicyRule, rules2 Role) []rbacv1.PolicyRule { + for j := range rules2.CustomRules { + ruleFound := false + prj := &rules2.CustomRules[j] + iLoop: + for i, pri := range rules1 { + // check if apiGroup, verbs, resourceName, and nonResourceURLS matches for new resource. + apiGroupsEqual := ifMatches(pri.APIGroups, prj.APIGroups) + verbsEqual := ifMatches(pri.Verbs, prj.Verbs) + resourceNamesEqual := ifMatches(pri.ResourceNames, prj.ResourceNames) + nonResourceURLsEqual := ifMatches(pri.NonResourceURLs, prj.NonResourceURLs) + + if apiGroupsEqual && verbsEqual && resourceNamesEqual && nonResourceURLsEqual { + for _, newResource := range prj.Resources { + if !findResource(pri.Resources, newResource) { + // append rbac rule to deploy/role.yaml + rules1[i].Resources = append(rules1[i].Resources, newResource) + } + } + ruleFound = true + break iLoop + } + } + if !ruleFound { + rules1 = append(rules1, *prj) + } + } + return rules1 +} +func updateRoleFile(role interface{}, roleFilePath string) error { + data, err := yaml.Marshal(&role) + if err != nil { + return fmt.Errorf("failed to marshal role(%+v): %v", role, err) + } + if err := ioutil.WriteFile(roleFilePath, data, 0664); err != nil { + return fmt.Errorf("failed to update %v: %v", roleFilePath, err) + } + + return nil +} diff --git a/internal/plugins/helm/v1/scaffolds/templates/role_test.go b/internal/plugins/helm/v1/scaffolds/templates/role_test.go new file mode 100644 index 00000000000..6151468ae3f --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/role_test.go @@ -0,0 +1,205 @@ +// Copyright 2019 The Operator-SDK 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 templates_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "helm.sh/helm/v3/pkg/chart" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/operator-framework/operator-sdk/internal/plugins/helm/v1/scaffolds/templates" +) + +func TestGenerateRoleScaffold(t *testing.T) { + validDiscoveryClient := &mockRoleDiscoveryClient{ + serverGroupsAndResources: func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + return simpleGroupList(), simpleResourcesList(), nil + }, + } + + brokenDiscoveryClient := &mockRoleDiscoveryClient{ + serverGroupsAndResources: func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + return nil, nil, errors.New("no server resources") + }, + } + + testCases := []roleScaffoldTestCase{ + { + name: "fallback to default with unparsable template", + chart: failChart(), + expectSkipDefaultRules: false, + expectLenCustomRules: 3, + }, + { + name: "skip rule for unknown API", + chart: unknownAPIChart(), + expectSkipDefaultRules: true, + expectLenCustomRules: 4, + }, + { + name: "namespaced manifest", + chart: namespacedChart(), + expectSkipDefaultRules: true, + expectLenCustomRules: 4, + }, + { + name: "cluster scoped manifest", + chart: clusterScopedChart(), + expectSkipDefaultRules: true, + expectLenCustomRules: 5, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s with valid discovery client", tc.name), func(t *testing.T) { + roleScaffold := templates.GenerateRoleScaffold(validDiscoveryClient, tc.chart) + assert.Equal(t, tc.expectSkipDefaultRules, roleScaffold.SkipDefaultRules) + assert.Equal(t, tc.expectLenCustomRules, len(roleScaffold.CustomRules)) + }) + + t.Run(fmt.Sprintf("%s with broken discovery client", tc.name), func(t *testing.T) { + roleScaffold := templates.GenerateRoleScaffold(brokenDiscoveryClient, tc.chart) + assert.Equal(t, false, roleScaffold.SkipDefaultRules) + assert.Equal(t, 3, len(roleScaffold.CustomRules)) + }) + } +} + +type mockRoleDiscoveryClient struct { + serverGroupsAndResources func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) +} + +func (dc *mockRoleDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + return dc.serverGroupsAndResources() +} + +func simpleGroupList() []*metav1.APIGroup { + return []*metav1.APIGroup{ + { + Name: "example", + }, + { + Name: "example2", + }, + } +} + +func simpleResourcesList() []*metav1.APIResourceList { + return []*metav1.APIResourceList{ + { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Name: "namespaces", + Kind: "Namespace", + Namespaced: false, + }, + { + Name: "pods", + Kind: "Pod", + Namespaced: true, + }, + }, + }, + } +} + +type roleScaffoldTestCase struct { + name string + chart *chart.Chart + expectSkipDefaultRules bool + expectLenCustomRules int +} + +func failChart() *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "broken", + }, + Templates: []*chart.File{ + {Name: "broken1.yaml", Data: []byte(`invalid {{ template`)}, + }, + } +} + +func unknownAPIChart() *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "unknown", + }, + Templates: []*chart.File{ + {Name: "unknown1.yaml", Data: testUnknownData("unknown1")}, + {Name: "pod1.yaml", Data: testPodData("pod1")}, + }, + } +} + +func namespacedChart() *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "namespaced", + }, + Templates: []*chart.File{ + {Name: "pod1.yaml", Data: testPodData("pod1")}, + {Name: "pod2.yaml", Data: testPodData("pod2")}, + }, + } +} + +func clusterScopedChart() *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "clusterscoped", + }, + Templates: []*chart.File{ + {Name: "pod1.yaml", Data: testPodData("pod1")}, + {Name: "pod2.yaml", Data: testPodData("pod2")}, + {Name: "ns1.yaml", Data: testNamespaceData("ns1")}, + {Name: "ns2.yaml", Data: testNamespaceData("ns2")}, + }, + } +} + +func testUnknownData(name string) []byte { + return []byte(fmt.Sprintf(`apiVersion: my-test-unknown.unknown.com/v1alpha1 +kind: UnknownKind +metadata: + name: %s`, name), + ) +} + +func testPodData(name string) []byte { + return []byte(fmt.Sprintf(`apiVersion: v1 +kind: Pod +metadata: + name: %s +spec: + containers: + - name: test + image: test`, name), + ) +} + +func testNamespaceData(name string) []byte { + return []byte(fmt.Sprintf(`apiVersion: v1 +kind: Namespace +metadata: + name: %s`, name), + ) +} diff --git a/internal/plugins/helm/v1/scaffolds/templates/watches.go b/internal/plugins/helm/v1/scaffolds/templates/watches.go new file mode 100644 index 00000000000..4204820a305 --- /dev/null +++ b/internal/plugins/helm/v1/scaffolds/templates/watches.go @@ -0,0 +1,98 @@ +// Copyright 2020 The Operator-SDK 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 templates + +import ( + "fmt" + + "sigs.k8s.io/kubebuilder/pkg/model/file" +) + +var _ file.Template = &Watches{} + +const defaultWatchesFile = "watches.yaml" + +// Watches scaffolds the watches.yaml file +type Watches struct { + file.TemplateMixin +} + +// SetTemplateDefaults implements input.Template +func (f *Watches) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = defaultWatchesFile + } + + f.TemplateBody = fmt.Sprintf(watchesTemplate, + file.NewMarkerFor(f.Path, watchMarker), + ) + return nil +} + +var _ file.Inserter = &WatchesUpdater{} + +type WatchesUpdater struct { + file.TemplateMixin + file.ResourceMixin + + ChartPath string +} + +func (*WatchesUpdater) GetPath() string { + return defaultWatchesFile +} + +func (*WatchesUpdater) GetIfExistsAction() file.IfExistsAction { + return file.Overwrite +} + +const ( + watchMarker = "watch" +) + +func (f *WatchesUpdater) GetMarkers() []file.Marker { + return []file.Marker{ + file.NewMarkerFor(defaultWatchesFile, watchMarker), + } +} + +func (f *WatchesUpdater) GetCodeFragments() file.CodeFragmentsMap { + fragments := make(file.CodeFragmentsMap, 1) + + // If resource is not being provided we are creating the file, not updating it + if f.Resource == nil { + return fragments + } + + // Generate watch fragments + watches := make([]string, 0) + watches = append(watches, + fmt.Sprintf(watchFragment, f.Resource.Domain, f.Resource.Version, f.Resource.Kind, f.ChartPath)) + + if len(watches) != 0 { + fragments[file.NewMarkerFor(defaultWatchesFile, watchMarker)] = watches + } + return fragments +} + +const watchFragment = `- group: %s + version: %s + kind: %s + chart: %s +` + +const watchesTemplate = `# Use the 'create api' subcommand to add watches to this file. +%s +` diff --git a/test/e2e/e2e_suite.go b/test/e2e/e2e_suite.go index 36b36819c56..5ae3d7cb763 100644 --- a/test/e2e/e2e_suite.go +++ b/test/e2e/e2e_suite.go @@ -19,8 +19,6 @@ package e2e import ( "bytes" "fmt" - "io/ioutil" - "os" "os/exec" "path" "path/filepath" @@ -167,7 +165,7 @@ var _ = Describe("operator-sdk", func() { By("generating the operator bundle") // Turn off interactive prompts for all generation tasks. replace := "operator-sdk generate kustomize manifests" - replaceInFile(filepath.Join(tc.Dir, "Makefile"), replace, replace+" --interactive=false") + testutils.ReplaceInFile(filepath.Join(tc.Dir, "Makefile"), replace, replace+" --interactive=false") err = tc.Make("bundle") Expect(err).NotTo(HaveOccurred()) @@ -214,14 +212,3 @@ var _ = Describe("operator-sdk", func() { }) }) }) - -// replaceInFile replaces all instances of old with new in the file at path. -func replaceInFile(path, old, new string) { - info, err := os.Stat(path) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - b, err := ioutil.ReadFile(path) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - s := strings.Replace(string(b), old, new, -1) - err = ioutil.WriteFile(path, []byte(s), info.Mode()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) -} diff --git a/test/internal/utils.go b/test/internal/utils.go index b63c544e3ef..3cd9f0cb98a 100644 --- a/test/internal/utils.go +++ b/test/internal/utils.go @@ -16,9 +16,13 @@ package internal import ( "fmt" + "io/ioutil" + "os" "os/exec" + "strings" . "github.com/onsi/ginkgo" //nolint:golint + . "github.com/onsi/gomega" //nolint:golint kbtestutils "sigs.k8s.io/kubebuilder/test/e2e/utils" ) @@ -53,3 +57,14 @@ func (tc TestContext) UninstallOLM() { func (tc TestContext) KustomizeBuild(dir string) ([]byte, error) { return tc.Run(exec.Command("kustomize", "build", dir)) } + +// ReplaceInFile replaces all instances of old with new in the file at path. +func ReplaceInFile(path, old, new string) { + info, err := os.Stat(path) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + b, err := ioutil.ReadFile(path) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + s := strings.Replace(string(b), old, new, -1) + err = ioutil.WriteFile(path, []byte(s), info.Mode()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) +} diff --git a/website/content/en/docs/new-cli/operator-sdk_init.md b/website/content/en/docs/new-cli/operator-sdk_init.md index 4de971bbd19..efed0bb110a 100644 --- a/website/content/en/docs/new-cli/operator-sdk_init.md +++ b/website/content/en/docs/new-cli/operator-sdk_init.md @@ -42,7 +42,7 @@ operator-sdk init [flags] -h, --help help for init --license string license to use to boilerplate, may be one of 'apache2', 'none' (default "apache2") --owner string owner to add to the copyright - --plugins strings Name and optionally version of the plugin to initialize the project with. Available plugins: ("go.kubebuilder.io/v2") + --plugins strings Name and optionally version of the plugin to initialize the project with. Available plugins: ("go.kubebuilder.io/v2", "helm.operator-sdk.io/v1") --project-version string project version, possible values: ("2", "3-alpha") (default "3-alpha") --repo string name to use for go module (e.g., github.com/user/repo), defaults to the go package of the current working directory. --skip-go-version-check if specified, skip checking the Go version