diff --git a/CHANGELOG.md b/CHANGELOG.md index 308e09514af..aeba2ad49b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,15 @@ ### Changed +- When Helm operator projects are created, the SDK now generates RBAC rules in `deploy/role.yaml` based on the chart's default manifest. ([#1188](https://github.com/operator-framework/operator-sdk/pull/1188)) +- When debug level is 3 or higher, we will set the klog verbosity to that level. ([#1322](https://github.com/operator-framework/operator-sdk/pull/1322)) + ### Deprecated ### Removed +- The SDK will no longer run `defaulter-gen` on running `operator-sdk generate k8s`. Defaulting for CRDs should be handled with mutating admission webhooks. ([#1288](https://github.com/operator-framework/operator-sdk/pull/1288)) + ### Bug Fixes ## v0.7.0 diff --git a/Gopkg.lock b/Gopkg.lock index d64eb291cfc..4371923f749 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1451,7 +1451,7 @@ revision = "e17681d19d3ac4837a019ece36c2a0ec31ffe985" [[projects]] - digest = "1:eb6da42f08a1a53b7375b7ee8188d70df15767691f66e030308b759b8a685141" + digest = "1:b93e3ee55e5e7b3ac9c21b145d0b481ed343af914d0fd8c09563a2e8968f4e46" name = "k8s.io/helm" packages = [ "pkg/chartutil", @@ -1473,6 +1473,7 @@ "pkg/provenance", "pkg/releasetesting", "pkg/releaseutil", + "pkg/renderutil", "pkg/repo", "pkg/resolver", "pkg/rudder", @@ -1735,6 +1736,7 @@ "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1", "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install", "github.com/pborman/uuid", + "github.com/pkg/errors", "github.com/prometheus/prometheus/util/promlint", "github.com/sergi/go-diff/diffmatchpatch", "github.com/sirupsen/logrus", @@ -1771,6 +1773,7 @@ "k8s.io/apimachinery/pkg/util/validation", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/util/yaml", + "k8s.io/apimachinery/pkg/version", "k8s.io/cli-runtime/pkg/genericclioptions/resource", "k8s.io/client-go/discovery", "k8s.io/client-go/discovery/cached", @@ -1791,15 +1794,18 @@ "k8s.io/helm/pkg/helm/environment", "k8s.io/helm/pkg/helm/helmpath", "k8s.io/helm/pkg/kube", + "k8s.io/helm/pkg/manifest", "k8s.io/helm/pkg/proto/hapi/chart", "k8s.io/helm/pkg/proto/hapi/release", "k8s.io/helm/pkg/proto/hapi/services", "k8s.io/helm/pkg/releaseutil", + "k8s.io/helm/pkg/renderutil", "k8s.io/helm/pkg/repo", "k8s.io/helm/pkg/storage", "k8s.io/helm/pkg/storage/driver", "k8s.io/helm/pkg/tiller", "k8s.io/helm/pkg/tiller/environment", + "k8s.io/klog", "sigs.k8s.io/controller-runtime/pkg/cache", "sigs.k8s.io/controller-runtime/pkg/client", "sigs.k8s.io/controller-runtime/pkg/client/config", diff --git a/cmd/operator-sdk/build/cmd.go b/cmd/operator-sdk/build/cmd.go index 7e8961895d9..e101be52c2d 100644 --- a/cmd/operator-sdk/build/cmd.go +++ b/cmd/operator-sdk/build/cmd.go @@ -188,7 +188,7 @@ func buildFunc(cmd *cobra.Command, args []string) error { } if enableTests { - if projutil.GetOperatorType() == projutil.OperatorTypeGo { + if projutil.IsOperatorGo() { testBinary := filepath.Join(absProjectPath, scaffold.BuildBinDir, projectName+"-test") goTestBuildArgs := append(append([]string{"test"}, goTrimFlags...), "-c", "-o", testBinary, testLocationBuild+"/...") buildTestCmd := exec.Command("go", goTestBuildArgs...) @@ -212,8 +212,7 @@ func buildFunc(cmd *cobra.Command, args []string) error { } s := &scaffold.Scaffold{} - t := projutil.GetOperatorType() - switch t { + switch t := projutil.GetOperatorType(); t { case projutil.OperatorTypeGo: err = s.Execute(cfg, &scaffold.TestFrameworkDockerfile{}, @@ -225,7 +224,7 @@ func buildFunc(cmd *cobra.Command, args []string) error { case projutil.OperatorTypeHelm: return fmt.Errorf("test scaffolding for Helm Operators is not implemented") default: - return fmt.Errorf("unknown operator type '%v'", t) + return projutil.ErrUnknownOperatorType{} } if err != nil { diff --git a/cmd/operator-sdk/internal/genutil/k8s.go b/cmd/operator-sdk/internal/genutil/k8s.go index 8fa216f5447..bf247ac85a8 100644 --- a/cmd/operator-sdk/internal/genutil/k8s.go +++ b/cmd/operator-sdk/internal/genutil/k8s.go @@ -38,7 +38,6 @@ func K8sCodegen(hf string) error { binDir := filepath.Join(wd, scaffold.BuildBinDir) genDirs := []string{ - "./cmd/defaulter-gen", "./cmd/client-gen", "./cmd/lister-gen", "./cmd/informer-gen", @@ -63,10 +62,6 @@ func K8sCodegen(hf string) error { if err = withHeaderFile(hf, fdc); err != nil { return err } - fd := func(a string) error { return defaulterGen(binDir, repoPkg, a, gvMap) } - if err = withHeaderFile(hf, fd); err != nil { - return err - } log.Info("Code-generation complete.") return nil @@ -88,19 +83,3 @@ func deepcopyGen(binDir, repoPkg, hf string, gvMap map[string][]string) (err err } return nil } - -func defaulterGen(binDir, repoPkg, hf string, gvMap map[string][]string) (err error) { - apisPkg := filepath.Join(repoPkg, scaffold.ApisDir) - args := []string{ - "--input-dirs", createFQApis(apisPkg, gvMap), - "--output-file-base", "zz_generated.defaults", - // defaulter-gen requires a boilerplate file. Either use header or an - // empty file if header is empty. - "--go-header-file", hf, - } - cmd := exec.Command(filepath.Join(binDir, "defaulter-gen"), args...) - if err = projutil.ExecCmd(cmd); err != nil { - return fmt.Errorf("failed to perform defaulter code-generation: %v", err) - } - return nil -} diff --git a/cmd/operator-sdk/new/cmd.go b/cmd/operator-sdk/new/cmd.go index e10d5968bea..0bb9d8da9b5 100644 --- a/cmd/operator-sdk/new/cmd.go +++ b/cmd/operator-sdk/new/cmd.go @@ -27,8 +27,10 @@ import ( "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" "github.com/operator-framework/operator-sdk/internal/util/projutil" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client/config" ) func NewCmd() *cobra.Command { @@ -280,6 +282,15 @@ func doHelmScaffold() error { valuesPath := filepath.Join("", helm.HelmChartsDir, chart.GetMetadata().GetName(), "values.yaml") crSpec := fmt.Sprintf("# Default values copied from %s\n\n%s", valuesPath, chart.GetValues().GetRaw()) + k8sCfg, err := config.GetConfig() + if err != nil { + return fmt.Errorf("failed to get kubernetes config: %s", err) + } + roleScaffold, err := helm.CreateRoleScaffold(k8sCfg, chart, isClusterScoped) + if err != nil { + return fmt.Errorf("failed to generate role scaffold: %s", err) + } + s := &scaffold.Scaffold{} err = s.Execute(cfg, &helm.Dockerfile{}, @@ -288,7 +299,7 @@ func doHelmScaffold() error { ChartName: chart.GetMetadata().GetName(), }, &scaffold.ServiceAccount{}, - &scaffold.Role{IsClusterScoped: isClusterScoped}, + roleScaffold, &scaffold.RoleBinding{IsClusterScoped: isClusterScoped}, &helm.Operator{IsClusterScoped: isClusterScoped}, &scaffold.CRD{Resource: resource}, @@ -309,7 +320,7 @@ func doHelmScaffold() error { func verifyFlags() error { if operatorType != projutil.OperatorTypeGo && operatorType != projutil.OperatorTypeAnsible && operatorType != projutil.OperatorTypeHelm { - return fmt.Errorf("value of --type can only be `go`, `ansible`, or `helm`") + return errors.Wrap(projutil.ErrUnknownOperatorType{Type: operatorType}, "value of --type can only be `go`, `ansible`, or `helm`") } if operatorType != projutil.OperatorTypeAnsible && generatePlaybook { return fmt.Errorf("value of --generate-playbook can only be used with --type `ansible`") diff --git a/cmd/operator-sdk/test/cluster.go b/cmd/operator-sdk/test/cluster.go index 30777583539..ee9171cf925 100755 --- a/cmd/operator-sdk/test/cluster.go +++ b/cmd/operator-sdk/test/cluster.go @@ -84,9 +84,9 @@ func testClusterFunc(cmd *cobra.Command, args []string) error { case projutil.OperatorTypeAnsible: testCmd = []string{"/" + ansible.BuildTestFrameworkAnsibleTestScriptFile} case projutil.OperatorTypeHelm: - log.Fatal("`test cluster` for Helm operators is not implemented") + return fmt.Errorf("`test cluster` for Helm operators is not implemented") default: - log.Fatal("Failed to determine operator type") + return projutil.ErrUnknownOperatorType{} } // cobra prints its help message on error; we silence that here because any errors below diff --git a/cmd/operator-sdk/test/local.go b/cmd/operator-sdk/test/local.go index b2894df155d..f6af04f65ed 100644 --- a/cmd/operator-sdk/test/local.go +++ b/cmd/operator-sdk/test/local.go @@ -75,8 +75,7 @@ func newTestLocalCmd() *cobra.Command { } func testLocalFunc(cmd *cobra.Command, args []string) error { - t := projutil.GetOperatorType() - switch t { + switch t := projutil.GetOperatorType(); t { case projutil.OperatorTypeGo: return testLocalGoFunc(cmd, args) case projutil.OperatorTypeAnsible: @@ -84,7 +83,7 @@ func testLocalFunc(cmd *cobra.Command, args []string) error { case projutil.OperatorTypeHelm: return fmt.Errorf("`test local` for Helm operators is not implemented") } - return fmt.Errorf("unknown operator type '%v'", t) + return projutil.ErrUnknownOperatorType{} } func testLocalAnsibleFunc(cmd *cobra.Command, args []string) error { diff --git a/cmd/operator-sdk/up/local.go b/cmd/operator-sdk/up/local.go index e7ed894c052..8bb6bc66608 100644 --- a/cmd/operator-sdk/up/local.go +++ b/cmd/operator-sdk/up/local.go @@ -87,8 +87,7 @@ func upLocalFunc(cmd *cobra.Command, args []string) error { } log.Infof("Using namespace %s.", namespace) - t := projutil.GetOperatorType() - switch t { + switch t := projutil.GetOperatorType(); t { case projutil.OperatorTypeGo: return upLocal() case projutil.OperatorTypeAnsible: @@ -96,7 +95,7 @@ func upLocalFunc(cmd *cobra.Command, args []string) error { case projutil.OperatorTypeHelm: return upLocalHelm() } - return fmt.Errorf("unknown operator type '%v'", t) + return projutil.ErrUnknownOperatorType{} } func upLocal() error { diff --git a/doc/ansible/user-guide.md b/doc/ansible/user-guide.md index a129c83bce7..418f854c05f 100644 --- a/doc/ansible/user-guide.md +++ b/doc/ansible/user-guide.md @@ -274,7 +274,12 @@ Kubernetes deployment manifests are generated in `deploy/operator.yaml`. The deployment image in this file needs to be modified from the placeholder `REPLACE_IMAGE` to the previous built image. To do this run: ``` -$ sed -i 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml +$ sed -i 's|{{ REPLACE_IMAGE }}|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml +``` + +The `imagePullPolicy` also requires an update. To do this run: +``` +$ sed -i 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml ``` If you created your operator using `--cluster-scoped=true`, update the service account namespace in the generated `ClusterRoleBinding` to match where you are deploying your operator. @@ -286,8 +291,9 @@ $ sed -i "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml **Note** If you are performing these steps on OSX, use the following commands instead: ``` -$ sed -i "" 's|REPLACE_IMAGE|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml +$ sed -i "" 's|{{ REPLACE_IMAGE }}|quay.io/example/memcached-operator:v0.0.1|g' deploy/operator.yaml $ sed -i "" "s|REPLACE_NAMESPACE|$OPERATOR_NAMESPACE|g" deploy/role_binding.yaml +$ sed -i "" 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml ``` Deploy the memcached-operator: diff --git a/doc/helm/user-guide.md b/doc/helm/user-guide.md index 6af442fa5ec..b3cf0826dc1 100644 --- a/doc/helm/user-guide.md +++ b/doc/helm/user-guide.md @@ -10,7 +10,7 @@ powered by Helm using tools and libraries provided by the Operator SDK. - [kubectl][kubectl_tool] version v1.11.3+. - [dep][dep_tool] version v0.5.0+. (Optional if you aren't installing from source) - [go][go_tool] version v1.10+. (Optional if you aren't installing from source) -- Access to a Kubernetes v.1.11.3+ cluster. +- Access to a Kubernetes v1.11.3+ cluster. **Note**: This guide uses [minikube][minikube_tool] version v0.25.0+ as the local Kubernetes cluster and [quay.io][quay_link] for the public registry. @@ -53,6 +53,11 @@ This creates the nginx-operator project specifically for watching the Nginx resource with APIVersion `example.com/v1alpha1` and Kind `Nginx`. +For Helm-based projects, `operator-sdk new` also generates the RBAC rules +in `deploy/role.yaml` based on the resources that would be deployed by the +chart's default manifest. Be sure to double check that the rules generated +in `deploy/role.yaml` meet the operator's permission requirements. + To learn more about the project directory structure, see the [project layout][layout_doc] doc. diff --git a/doc/sdk-cli-reference.md b/doc/sdk-cli-reference.md index 4318b89be81..40310d7f935 100644 --- a/doc/sdk-cli-reference.md +++ b/doc/sdk-cli-reference.md @@ -98,7 +98,6 @@ Prints the most recent Golang packages and versions required by operators. Print ```console $ operator-sdk print-deps --as-file required = [ - "k8s.io/code-generator/cmd/defaulter-gen", "k8s.io/code-generator/cmd/deepcopy-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/client-gen", diff --git a/doc/user/logging.md b/doc/user/logging.md index 20e04c13af9..94656c306ef 100644 --- a/doc/user/logging.md +++ b/doc/user/logging.md @@ -38,7 +38,7 @@ In the above example, we add the zap flagset to the operator's command line flag By default, `zap.Logger()` will return a logger that is ready for production use. It uses a JSON encoder, logs starting at the `info` level, and has [sampling][zap_sampling] enabled. To customize the default behavior, users can use the zap flagset and specify flags on the command line. The zap flagset includes the following flags that can be used to configure the logger: * `--zap-devel` - Enables the zap development config (changes defaults to console encoder, debug log level, and disables sampling) (default: `false`) * `--zap-encoder` string - Sets the zap log encoding (`json` or `console`) -* `--zap-level` string or integer - Sets the zap log level (`debug`, `info`, `error`, or an integer value greater than 0) +* `--zap-level` string or integer - Sets the zap log level (`debug`, `info`, `error`, or an integer value greater than 0). If 4 or greater the verbosity of client-go will be set to this level. * `--zap-sample` - Enables zap's sampling mode. Sampling will be disabled for integer log levels greater than 1. diff --git a/internal/pkg/scaffold/ansible/ao_logs.go b/internal/pkg/scaffold/ansible/ao_logs.go index 2c116aa99f9..2b00e86420d 100644 --- a/internal/pkg/scaffold/ansible/ao_logs.go +++ b/internal/pkg/scaffold/ansible/ao_logs.go @@ -22,7 +22,7 @@ import ( //DockerfileHybrid - Dockerfile for a hybrid operator type AoLogs struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/build_dockerfile.go b/internal/pkg/scaffold/ansible/build_dockerfile.go index 0b1a47ce1d6..7e74c545d75 100644 --- a/internal/pkg/scaffold/ansible/build_dockerfile.go +++ b/internal/pkg/scaffold/ansible/build_dockerfile.go @@ -38,17 +38,18 @@ func (b *BuildDockerfile) GetInput() (input.Input, error) { b.Path = filepath.Join(scaffold.BuildDir, BuildDockerfileFile) } b.TemplateBody = buildDockerfileAnsibleTmpl + b.Delims = AnsibleDelims b.RolesDir = RolesDir b.ImageTag = strings.TrimSuffix(version.Version, "+git") return b.Input, nil } -const buildDockerfileAnsibleTmpl = `FROM quay.io/operator-framework/ansible-operator:{{.ImageTag}} +const buildDockerfileAnsibleTmpl = `FROM quay.io/operator-framework/ansible-operator:[[.ImageTag]] COPY watches.yaml ${HOME}/watches.yaml -COPY {{.RolesDir}}/ ${HOME}/{{.RolesDir}}/ -{{- if .GeneratePlaybook }} +COPY [[.RolesDir]]/ ${HOME}/[[.RolesDir]]/ +[[- if .GeneratePlaybook ]] COPY playbook.yml ${HOME}/playbook.yml -{{- end }} +[[- end ]] ` diff --git a/internal/pkg/scaffold/ansible/build_test_framework_ansible_test_script.go b/internal/pkg/scaffold/ansible/build_test_framework_ansible_test_script.go index f390b9f4d17..f225c74077c 100644 --- a/internal/pkg/scaffold/ansible/build_test_framework_ansible_test_script.go +++ b/internal/pkg/scaffold/ansible/build_test_framework_ansible_test_script.go @@ -24,7 +24,7 @@ import ( const BuildTestFrameworkAnsibleTestScriptFile = "ansible-test.sh" type BuildTestFrameworkAnsibleTestScript struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/build_test_framework_dockerfile.go b/internal/pkg/scaffold/ansible/build_test_framework_dockerfile.go index 173ab04a265..2958d8f071e 100644 --- a/internal/pkg/scaffold/ansible/build_test_framework_dockerfile.go +++ b/internal/pkg/scaffold/ansible/build_test_framework_dockerfile.go @@ -24,7 +24,7 @@ import ( const BuildTestFrameworkDockerfileFile = "Dockerfile" type BuildTestFrameworkDockerfile struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/constants.go b/internal/pkg/scaffold/ansible/constants.go index c68825f1416..d6c1c395b51 100644 --- a/internal/pkg/scaffold/ansible/constants.go +++ b/internal/pkg/scaffold/ansible/constants.go @@ -26,3 +26,6 @@ const ( MoleculeDefaultDir = MoleculeDir + filePathSep + "default" MoleculeTestLocalDir = MoleculeDir + filePathSep + "test-local" ) + +// Arrays can't be constants but this should be a constant +var AnsibleDelims = [2]string{"[[", "]]"} diff --git a/internal/pkg/scaffold/ansible/deploy_operator.go b/internal/pkg/scaffold/ansible/deploy_operator.go index b620051d515..424f19c037c 100644 --- a/internal/pkg/scaffold/ansible/deploy_operator.go +++ b/internal/pkg/scaffold/ansible/deploy_operator.go @@ -34,6 +34,7 @@ func (d *DeployOperator) GetInput() (input.Input, error) { d.Path = filepath.Join(scaffold.DeployDir, DeployOperatorFile) } d.TemplateBody = deployOperatorAnsibleTmpl + d.Delims = AnsibleDelims return d.Input, nil } @@ -41,18 +42,18 @@ func (d *DeployOperator) GetInput() (input.Input, error) { const deployOperatorAnsibleTmpl = `apiVersion: apps/v1 kind: Deployment metadata: - name: {{.ProjectName}} + name: [[.ProjectName]] spec: replicas: 1 selector: matchLabels: - name: {{.ProjectName}} + name: [[.ProjectName]] template: metadata: labels: - name: {{.ProjectName}} + name: [[.ProjectName]] spec: - serviceAccountName: {{.ProjectName}} + serviceAccountName: [[.ProjectName]] containers: - name: ansible command: @@ -60,34 +61,34 @@ spec: - /tmp/ansible-operator/runner - stdout # Replace this with the built image name - image: "{{ "{{ REPLACE_IMAGE }}" }}" - imagePullPolicy: "{{ "{{ pull_policy|default('Always') }}"}}" + image: "{{ REPLACE_IMAGE }}" + imagePullPolicy: "{{ pull_policy|default('Always') }}" volumeMounts: - mountPath: /tmp/ansible-operator/runner name: runner readOnly: true - name: operator # Replace this with the built image name - image: "{{ "{{ REPLACE_IMAGE }}" }}" - imagePullPolicy: "{{ "{{ pull_policy|default('Always') }}"}}" + image: "{{ REPLACE_IMAGE }}" + imagePullPolicy: "{{ pull_policy|default('Always') }}" volumeMounts: - mountPath: /tmp/ansible-operator/runner name: runner env: - name: WATCH_NAMESPACE - {{- if .IsClusterScoped }} + [[- if .IsClusterScoped ]] value: "" - {{- else }} + [[- else ]] valueFrom: fieldRef: fieldPath: metadata.namespace - {{- end}} + [[- end]] - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: OPERATOR_NAME - value: "{{.ProjectName}}" + value: "[[.ProjectName]]" volumes: - name: runner emptyDir: {} diff --git a/internal/pkg/scaffold/ansible/dockerfilehybrid.go b/internal/pkg/scaffold/ansible/dockerfilehybrid.go index eb7db27dee1..62aceceb83f 100644 --- a/internal/pkg/scaffold/ansible/dockerfilehybrid.go +++ b/internal/pkg/scaffold/ansible/dockerfilehybrid.go @@ -41,6 +41,7 @@ func (d *DockerfileHybrid) GetInput() (input.Input, error) { d.Path = filepath.Join(scaffold.BuildDir, scaffold.DockerfileFile) } d.TemplateBody = dockerFileHybridAnsibleTmpl + d.Delims = AnsibleDelims return d.Input, nil } @@ -64,21 +65,21 @@ ENV OPERATOR=/usr/local/bin/ansible-operator \ USER_NAME=ansible-operator\ HOME=/opt/ansible -{{- if .Watches }} -COPY watches.yaml ${HOME}/watches.yaml{{ end }} +[[- if .Watches ]] +COPY watches.yaml ${HOME}/watches.yaml[[ end ]] # install operator binary -COPY build/_output/bin/{{.ProjectName}} ${OPERATOR} +COPY build/_output/bin/[[.ProjectName]] ${OPERATOR} # install k8s_status Ansible Module COPY library/k8s_status.py /usr/share/ansible/openshift/ COPY bin /usr/local/bin RUN /usr/local/bin/user_setup -{{- if .Roles }} -COPY roles/ ${HOME}/roles/{{ end }} -{{- if .Playbook }} -COPY playbook.yml ${HOME}/playbook.yml{{ end }} +[[- if .Roles ]] +COPY roles/ ${HOME}/roles/[[ end ]] +[[- if .Playbook ]] +COPY playbook.yml ${HOME}/playbook.yml[[ end ]] ENTRYPOINT ["/usr/local/bin/entrypoint"] diff --git a/internal/pkg/scaffold/ansible/entrypoint.go b/internal/pkg/scaffold/ansible/entrypoint.go index 0338cc5776f..5596f38f100 100644 --- a/internal/pkg/scaffold/ansible/entrypoint.go +++ b/internal/pkg/scaffold/ansible/entrypoint.go @@ -22,7 +22,7 @@ import ( // Entrypoint - entrypoint script type Entrypoint struct { - input.Input + StaticInput } func (e *Entrypoint) GetInput() (input.Input, error) { diff --git a/internal/pkg/scaffold/ansible/gopkgtoml.go b/internal/pkg/scaffold/ansible/gopkgtoml.go index 7fff1ce297a..7a4d9a37261 100644 --- a/internal/pkg/scaffold/ansible/gopkgtoml.go +++ b/internal/pkg/scaffold/ansible/gopkgtoml.go @@ -21,7 +21,7 @@ import ( // GopkgToml - the Gopkg.toml file for a hybrid operator type GopkgToml struct { - input.Input + StaticInput } func (s *GopkgToml) GetInput() (input.Input, error) { diff --git a/internal/pkg/scaffold/ansible/input.go b/internal/pkg/scaffold/ansible/input.go new file mode 100644 index 00000000000..ac3e8992d70 --- /dev/null +++ b/internal/pkg/scaffold/ansible/input.go @@ -0,0 +1,32 @@ +// 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 ansible + +import ( + "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" + "github.com/spf13/afero" +) + +// StaticInput is the input for scaffolding a static file with +// no parameteres +type StaticInput struct { + input.Input +} + +// CustomRender return the template body unmodified +func (s *StaticInput) CustomRender() ([]byte, error) { + return []byte(s.TemplateBody), nil +} + +func (s StaticInput) SetFS(_ afero.Fs) {} diff --git a/internal/pkg/scaffold/ansible/k8s_status.go b/internal/pkg/scaffold/ansible/k8s_status.go index aa6d61dcf1e..2c9c5b02780 100644 --- a/internal/pkg/scaffold/ansible/k8s_status.go +++ b/internal/pkg/scaffold/ansible/k8s_status.go @@ -16,15 +16,13 @@ package ansible import ( "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" - - "github.com/spf13/afero" ) const K8sStatusPythonFile = "library/k8s_status.py" // K8sStatus - the k8s status module tmpl wrapper type K8sStatus struct { - input.Input + StaticInput } // GetInput - gets the input @@ -32,13 +30,10 @@ func (k *K8sStatus) GetInput() (input.Input, error) { if k.Path == "" { k.Path = K8sStatusPythonFile } - return k.Input, nil -} -func (s K8sStatus) SetFS(_ afero.Fs) {} + k.TemplateBody = k8sStatusTmpl -func (k K8sStatus) CustomRender() ([]byte, error) { - return []byte(k8sStatusTmpl), nil + return k.Input, nil } const k8sStatusTmpl = `#!/usr/bin/python diff --git a/internal/pkg/scaffold/ansible/main.go b/internal/pkg/scaffold/ansible/main.go index b457d06b84d..267b439ccd9 100644 --- a/internal/pkg/scaffold/ansible/main.go +++ b/internal/pkg/scaffold/ansible/main.go @@ -22,7 +22,7 @@ import ( // Main - main source file for ansible operator type Main struct { - input.Input + StaticInput } func (m *Main) GetInput() (input.Input, error) { diff --git a/internal/pkg/scaffold/ansible/molecule_default_asserts.go b/internal/pkg/scaffold/ansible/molecule_default_asserts.go index 6ec54b3bef4..019f4bdce0d 100644 --- a/internal/pkg/scaffold/ansible/molecule_default_asserts.go +++ b/internal/pkg/scaffold/ansible/molecule_default_asserts.go @@ -32,6 +32,7 @@ func (m *MoleculeDefaultAsserts) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeDefaultDir, MoleculeDefaultAssertsFile) } m.TemplateBody = moleculeDefaultAssertsAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } @@ -42,13 +43,13 @@ const moleculeDefaultAssertsAnsibleTmpl = `--- hosts: localhost connection: local vars: - ansible_python_interpreter: '{{"{{ ansible_playbook_python }}"}}' + ansible_python_interpreter: '{{ ansible_playbook_python }}' tasks: - - name: Get all pods in {{"{{ namespace }}"}} + - name: Get all pods in {{ namespace }} k8s_facts: api_version: v1 kind: Pod - namespace: '{{"{{ namespace }}"}}' + namespace: '{{ namespace }}' register: pods - name: Output pods diff --git a/internal/pkg/scaffold/ansible/molecule_default_molecule.go b/internal/pkg/scaffold/ansible/molecule_default_molecule.go index 5416cd27516..d0fe314e811 100644 --- a/internal/pkg/scaffold/ansible/molecule_default_molecule.go +++ b/internal/pkg/scaffold/ansible/molecule_default_molecule.go @@ -23,7 +23,7 @@ import ( const MoleculeDefaultMoleculeFile = "molecule.yml" type MoleculeDefaultMolecule struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/molecule_default_playbook.go b/internal/pkg/scaffold/ansible/molecule_default_playbook.go index 28157ab40ff..6a039bb5638 100644 --- a/internal/pkg/scaffold/ansible/molecule_default_playbook.go +++ b/internal/pkg/scaffold/ansible/molecule_default_playbook.go @@ -35,24 +35,25 @@ func (m *MoleculeDefaultPlaybook) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeDefaultDir, MoleculeDefaultPlaybookFile) } m.TemplateBody = moleculeDefaultPlaybookAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } const moleculeDefaultPlaybookAnsibleTmpl = `--- -{{- if .GeneratePlaybook }} -- import_playbook: '{{"{{ playbook_dir }}/../../playbook.yml"}}' -{{- end }} +[[- if .GeneratePlaybook ]] +- import_playbook: '{{ playbook_dir }}/../../playbook.yml' +[[- end ]] - {{- if not .GeneratePlaybook }} + [[- if not .GeneratePlaybook ]] - name: Converge hosts: localhost connection: local vars: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' + ansible_python_interpreter: '{{ ansible_playbook_python }}' roles: - - {{.Resource.LowerKind}} - {{- end }} + - [[.Resource.LowerKind]] + [[- end ]] -- import_playbook: '{{"{{ playbook_dir }}/asserts.yml"}}' +- import_playbook: '{{ playbook_dir }}/asserts.yml' ` diff --git a/internal/pkg/scaffold/ansible/molecule_default_prepare.go b/internal/pkg/scaffold/ansible/molecule_default_prepare.go index b84808dc8ed..3f7ecda02f9 100644 --- a/internal/pkg/scaffold/ansible/molecule_default_prepare.go +++ b/internal/pkg/scaffold/ansible/molecule_default_prepare.go @@ -32,6 +32,7 @@ func (m *MoleculeDefaultPrepare) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeDefaultDir, MoleculeDefaultPrepareFile) } m.TemplateBody = moleculeDefaultPrepareAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } @@ -41,25 +42,25 @@ const moleculeDefaultPrepareAnsibleTmpl = `--- hosts: k8s gather_facts: no vars: - kubeconfig: "{{"{{ lookup('env', 'KUBECONFIG') }}"}}" + kubeconfig: "{{ lookup('env', 'KUBECONFIG') }}" tasks: - name: delete the kubeconfig if present file: - path: '{{"{{ kubeconfig }}"}}' + path: '{{ kubeconfig }}' state: absent delegate_to: localhost - name: Fetch the kubeconfig fetch: - dest: '{{ "{{ kubeconfig }}" }}' + dest: '{{ kubeconfig }}' flat: yes src: /root/.kube/config - name: Change the kubeconfig port to the proper value replace: regexp: 8443 - replace: "{{"{{ lookup('env', 'KIND_PORT') }}"}}" - path: '{{ "{{ kubeconfig }}" }}' + replace: "{{ lookup('env', 'KIND_PORT') }}" + path: '{{ kubeconfig }}' delegate_to: localhost - name: Wait for the Kubernetes API to become available (this could take a minute) diff --git a/internal/pkg/scaffold/ansible/molecule_test_cluster_molecule.go b/internal/pkg/scaffold/ansible/molecule_test_cluster_molecule.go index 930a42725cd..356f4fe9fda 100644 --- a/internal/pkg/scaffold/ansible/molecule_test_cluster_molecule.go +++ b/internal/pkg/scaffold/ansible/molecule_test_cluster_molecule.go @@ -23,7 +23,7 @@ import ( const MoleculeTestClusterMoleculeFile = "molecule.yml" type MoleculeTestClusterMolecule struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/molecule_test_cluster_playbook.go b/internal/pkg/scaffold/ansible/molecule_test_cluster_playbook.go index 255c9088417..fb57045d3aa 100644 --- a/internal/pkg/scaffold/ansible/molecule_test_cluster_playbook.go +++ b/internal/pkg/scaffold/ansible/molecule_test_cluster_playbook.go @@ -34,6 +34,7 @@ func (m *MoleculeTestClusterPlaybook) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeTestClusterDir, MoleculeTestClusterPlaybookFile) } m.TemplateBody = moleculeTestClusterPlaybookAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } @@ -44,31 +45,31 @@ const moleculeTestClusterPlaybookAnsibleTmpl = `--- hosts: localhost connection: local vars: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' - deploy_dir: "{{"{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"}}" - image_name: {{.Resource.FullGroup}}/{{.ProjectName}}:testing - custom_resource: "{{"{{"}} lookup('file', '/'.join([deploy_dir, 'crds/{{.Resource.Group}}_{{.Resource.Version}}_{{.Resource.LowerKind}}_cr.yaml'])) | from_yaml {{"}}"}}" + ansible_python_interpreter: '{{ ansible_playbook_python }}' + deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" + image_name: [[.Resource.FullGroup]]/[[.ProjectName]]:testing + custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/[[.Resource.Group]]_[[.Resource.Version]]_[[.Resource.LowerKind]]_cr.yaml'])) | from_yaml }}" tasks: - - name: Create the {{.Resource.FullGroup}}/{{.Resource.Version}}.{{.Resource.Kind}} + - name: Create the [[.Resource.FullGroup]]/[[.Resource.Version]].[[.Resource.Kind]] k8s: - namespace: '{{ "{{ namespace }}" }}' - definition: "{{"{{"}} lookup('file', '/'.join([deploy_dir, 'crds/{{.Resource.Group}}_{{.Resource.Version}}_{{.Resource.LowerKind}}_cr.yaml'])) {{"}}"}}" + namespace: '{{ namespace }}' + definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/[[.Resource.Group]]_[[.Resource.Version]]_[[.Resource.LowerKind]]_cr.yaml'])) }}" - name: Get the newly created Custom Resource debug: - msg: "{{"{{"}} lookup('k8s', group='{{.Resource.FullGroup}}', api_version='{{.Resource.Version}}', kind='{{.Resource.Kind}}', namespace=namespace, resource_name=custom_resource.metadata.name) {{"}}"}}" + msg: "{{ lookup('k8s', group='[[.Resource.FullGroup]]', api_version='[[.Resource.Version]]', kind='[[.Resource.Kind]]', namespace=namespace, resource_name=custom_resource.metadata.name) }}" - name: Wait 40s for reconciliation to run k8s_facts: - api_version: '{{.Resource.Version}}' - kind: '{{.Resource.Kind }}' - namespace: '{{"{{"}} namespace {{"}}"}}' - name: '{{"{{"}} custom_resource.metadata.name {{"}}"}}' + api_version: '[[.Resource.Version]]' + kind: '[[.Resource.Kind]]' + namespace: '{{ namespace }}' + name: '{{ custom_resource.metadata.name }}' register: reconcile_cr until: - "'Successful' in (reconcile_cr | json_query('resources[].status.conditions[].reason'))" delay: 4 retries: 10 -- import_playbook: "{{"{{ playbook_dir }}/../default/asserts.yml"}}" +- import_playbook: '{{ playbook_dir }}/../default/asserts.yml' ` diff --git a/internal/pkg/scaffold/ansible/molecule_test_local_molecule.go b/internal/pkg/scaffold/ansible/molecule_test_local_molecule.go index 2b26e167e37..47453b66b61 100644 --- a/internal/pkg/scaffold/ansible/molecule_test_local_molecule.go +++ b/internal/pkg/scaffold/ansible/molecule_test_local_molecule.go @@ -23,7 +23,7 @@ import ( const MoleculeTestLocalMoleculeFile = "molecule.yml" type MoleculeTestLocalMolecule struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/molecule_test_local_playbook.go b/internal/pkg/scaffold/ansible/molecule_test_local_playbook.go index d31d0e6e9ed..ddb056f26ea 100644 --- a/internal/pkg/scaffold/ansible/molecule_test_local_playbook.go +++ b/internal/pkg/scaffold/ansible/molecule_test_local_playbook.go @@ -34,6 +34,7 @@ func (m *MoleculeTestLocalPlaybook) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeTestLocalDir, MoleculeTestLocalPlaybookFile) } m.TemplateBody = moleculeTestLocalPlaybookAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } @@ -43,16 +44,16 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- - name: Build Operator in Kubernetes docker container hosts: k8s vars: - image_name: {{.Resource.FullGroup}}/{{.ProjectName}}:testing + image_name: [[.Resource.FullGroup]]/[[.ProjectName]]:testing tasks: # using command so we don't need to install any dependencies - name: Get existing image hash - command: docker images -q {{"{{image_name}}"}} + command: docker images -q {{ image_name }} register: prev_hash changed_when: false - name: Build Operator Image - command: docker build -f /build/build/Dockerfile -t {{"{{ image_name }}"}} /build + command: docker build -f /build/build/Dockerfile -t {{ image_name }} /build register: build_cmd changed_when: not prev_hash.stdout or (prev_hash.stdout and prev_hash.stdout not in ''.join(build_cmd.stdout_lines[-2:])) @@ -60,29 +61,29 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- hosts: localhost connection: local vars: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' - deploy_dir: "{{"{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"}}" + ansible_python_interpreter: '{{ ansible_playbook_python }}' + deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" pull_policy: Never - REPLACE_IMAGE: {{.Resource.FullGroup}}/{{.ProjectName}}:testing - custom_resource: "{{"{{"}} lookup('file', '/'.join([deploy_dir, 'crds/{{.Resource.Group}}_{{.Resource.Version}}_{{.Resource.LowerKind}}_cr.yaml'])) | from_yaml {{"}}"}}" + REPLACE_IMAGE: [[.Resource.FullGroup]]/[[.ProjectName]]:testing + custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/[[.Resource.Group]]_[[.Resource.Version]]_[[.Resource.LowerKind]]_cr.yaml'])) | from_yaml }}" tasks: - block: - name: Delete the Operator Deployment k8s: state: absent - namespace: '{{ "{{ namespace }}" }}' - definition: "{{"{{"}} lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) {{"}}"}}" + namespace: '{{ namespace }}' + definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" register: delete_deployment when: hostvars[groups.k8s.0].build_cmd.changed - name: Wait 30s for Operator Deployment to terminate k8s_facts: - api_version: '{{"{{"}} definition.apiVersion {{"}}"}}' - kind: '{{"{{"}} definition.kind {{"}}"}}' - namespace: '{{"{{"}} namespace {{"}}"}}' - name: '{{"{{"}} definition.metadata.name {{"}}"}}' + api_version: '{{ definition.apiVersion }}' + kind: '{{ definition.kind }}' + namespace: '{{ namespace }}' + name: '{{ definition.metadata.name }}' vars: - definition: "{{"{{"}} lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml {{"}}"}}" + definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" register: deployment until: not deployment.resources delay: 3 @@ -91,21 +92,21 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- - name: Create the Operator Deployment k8s: - namespace: '{{ "{{ namespace }}" }}' - definition: "{{"{{"}} lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) {{"}}"}}" + namespace: '{{ namespace }}' + definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}" - - name: Create the {{.Resource.FullGroup}}/{{.Resource.Version}}.{{.Resource.Kind}} + - name: Create the [[.Resource.FullGroup]]/[[.Resource.Version]].[[.Resource.Kind]] k8s: state: present - namespace: '{{ "{{ namespace }}" }}' - definition: "{{ "{{ custom_resource }}" }}" + namespace: '{{ namespace }}' + definition: '{{ custom_resource }}' - name: Wait 40s for reconciliation to run k8s_facts: - api_version: '{{"{{"}} custom_resource.apiVersion {{"}}"}}' - kind: '{{"{{"}} custom_resource.kind {{"}}"}}' - namespace: '{{"{{"}} namespace {{"}}"}}' - name: '{{"{{"}} custom_resource.metadata.name {{"}}"}}' + api_version: '{{ custom_resource.apiVersion }}' + kind: '{{ custom_resource.kind }}' + namespace: '{{ namespace }}' + name: '{{ custom_resource.metadata.name }}' register: cr until: - "'Successful' in (cr | json_query('resources[].status.conditions[].reason'))" @@ -118,12 +119,12 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- debug: var: debug_cr vars: - debug_cr: '{{"{{"}} lookup("k8s", + debug_cr: '{{ lookup("k8s", kind=custom_resource.kind, api_version=custom_resource.apiVersion, namespace=namespace, resource_name=custom_resource.metadata.name - ){{"}}"}}' + )}}' - name: debug memcached lookup ignore_errors: yes @@ -131,21 +132,21 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- debug: var: deploy vars: - deploy: '{{"{{"}} lookup("k8s", + deploy: '{{ lookup("k8s", kind="Deployment", api_version="apps/v1", namespace=namespace, label_selector="app=memcached" - ){{"}}"}}' + )}}' - name: get operator logs ignore_errors: yes failed_when: false - command: kubectl logs deployment/{{"{{"}} definition.metadata.name {{"}}"}} -n {{"{{"}} namespace {{"}}"}} + command: kubectl logs deployment/{{ definition.metadata.name }} -n {{ namespace }} environment: - KUBECONFIG: '{{"{{"}} lookup("env", "KUBECONFIG") {{"}}"}}' + KUBECONFIG: '{{ lookup("env", "KUBECONFIG") }}' vars: - definition: "{{"{{"}} lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml {{"}}"}}" + definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}" register: log - debug: var=log.stdout_lines @@ -153,5 +154,5 @@ const moleculeTestLocalPlaybookAnsibleTmpl = `--- - fail: msg: "Failed on action: converge" -- import_playbook: '{{"{{ playbook_dir }}/../default/asserts.yml"}}' +- import_playbook: '{{ playbook_dir }}/../default/asserts.yml' ` diff --git a/internal/pkg/scaffold/ansible/molecule_test_local_prepare.go b/internal/pkg/scaffold/ansible/molecule_test_local_prepare.go index 3603967c53c..3c0a8ccfa54 100644 --- a/internal/pkg/scaffold/ansible/molecule_test_local_prepare.go +++ b/internal/pkg/scaffold/ansible/molecule_test_local_prepare.go @@ -34,6 +34,7 @@ func (m *MoleculeTestLocalPrepare) GetInput() (input.Input, error) { m.Path = filepath.Join(MoleculeTestLocalDir, MoleculeTestLocalPrepareFile) } m.TemplateBody = moleculeTestLocalPrepareAnsibleTmpl + m.Delims = AnsibleDelims return m.Input, nil } @@ -45,23 +46,23 @@ const moleculeTestLocalPrepareAnsibleTmpl = `--- hosts: localhost connection: local vars: - ansible_python_interpreter: '{{ "{{ ansible_playbook_python }}" }}' - deploy_dir: "{{"{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"}}" + ansible_python_interpreter: '{{ ansible_playbook_python }}' + deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy" tasks: - name: Create Custom Resource Definition k8s: - definition: "{{"{{"}} lookup('file', '/'.join([deploy_dir, 'crds/{{.Resource.Group}}_{{.Resource.Version}}_{{.Resource.LowerKind}}_crd.yaml'])) {{"}}"}}" + definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/[[.Resource.Group]]_[[.Resource.Version]]_[[.Resource.LowerKind]]_crd.yaml'])) }}" - name: Ensure specified namespace is present k8s: api_version: v1 kind: Namespace - name: '{{ "{{ namespace }}" }}' + name: '{{ namespace }}' - name: Create RBAC resources k8s: - definition: "{{"{{"}} lookup('template', '/'.join([deploy_dir, item])) {{"}}"}}" - namespace: '{{ "{{ namespace }}" }}' + definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}" + namespace: '{{ namespace }}' with_items: - role.yaml - role_binding.yaml diff --git a/internal/pkg/scaffold/ansible/playbook.go b/internal/pkg/scaffold/ansible/playbook.go index 039172eb723..11dc031cf26 100644 --- a/internal/pkg/scaffold/ansible/playbook.go +++ b/internal/pkg/scaffold/ansible/playbook.go @@ -33,6 +33,7 @@ func (p *Playbook) GetInput() (input.Input, error) { p.Path = PlaybookYamlFile } p.TemplateBody = playbookTmpl + p.Delims = AnsibleDelims return p.Input, nil } @@ -40,5 +41,5 @@ const playbookTmpl = `- hosts: localhost gather_facts: no tasks: - import_role: - name: "{{.Resource.LowerKind}}" + name: "[[.Resource.LowerKind]]" ` diff --git a/internal/pkg/scaffold/ansible/roles_defaults_main.go b/internal/pkg/scaffold/ansible/roles_defaults_main.go index 0717ed8226c..5309b699850 100644 --- a/internal/pkg/scaffold/ansible/roles_defaults_main.go +++ b/internal/pkg/scaffold/ansible/roles_defaults_main.go @@ -34,10 +34,11 @@ func (r *RolesDefaultsMain) GetInput() (input.Input, error) { r.Path = filepath.Join(RolesDir, r.Resource.LowerKind, RolesDefaultsMainFile) } r.TemplateBody = rolesDefaultsMainAnsibleTmpl + r.Delims = AnsibleDelims return r.Input, nil } const rolesDefaultsMainAnsibleTmpl = `--- -# defaults file for {{.Resource.LowerKind}} +# defaults file for [[.Resource.LowerKind]] ` diff --git a/internal/pkg/scaffold/ansible/roles_files.go b/internal/pkg/scaffold/ansible/roles_files.go index d20205251c2..aebc567ca4f 100644 --- a/internal/pkg/scaffold/ansible/roles_files.go +++ b/internal/pkg/scaffold/ansible/roles_files.go @@ -24,7 +24,7 @@ import ( const RolesFilesDir = "files" + filePathSep + ".placeholder" type RolesFiles struct { - input.Input + StaticInput Resource scaffold.Resource } diff --git a/internal/pkg/scaffold/ansible/roles_handlers_main.go b/internal/pkg/scaffold/ansible/roles_handlers_main.go index 67a2a62c860..3ea9009100a 100644 --- a/internal/pkg/scaffold/ansible/roles_handlers_main.go +++ b/internal/pkg/scaffold/ansible/roles_handlers_main.go @@ -34,10 +34,11 @@ func (r *RolesHandlersMain) GetInput() (input.Input, error) { r.Path = filepath.Join(RolesDir, r.Resource.LowerKind, RolesHandlersMainFile) } r.TemplateBody = rolesHandlersMainAnsibleTmpl + r.Delims = AnsibleDelims return r.Input, nil } const rolesHandlersMainAnsibleTmpl = `--- -# handlers file for {{.Resource.LowerKind}} +# handlers file for [[.Resource.LowerKind]] ` diff --git a/internal/pkg/scaffold/ansible/roles_meta_main.go b/internal/pkg/scaffold/ansible/roles_meta_main.go index 3be742ca643..db18dad09db 100644 --- a/internal/pkg/scaffold/ansible/roles_meta_main.go +++ b/internal/pkg/scaffold/ansible/roles_meta_main.go @@ -24,7 +24,7 @@ import ( const RolesMetaMainFile = "meta" + filePathSep + "main.yml" type RolesMetaMain struct { - input.Input + StaticInput Resource scaffold.Resource } diff --git a/internal/pkg/scaffold/ansible/roles_readme.go b/internal/pkg/scaffold/ansible/roles_readme.go index fbb6f161098..2a9a5816f73 100644 --- a/internal/pkg/scaffold/ansible/roles_readme.go +++ b/internal/pkg/scaffold/ansible/roles_readme.go @@ -24,7 +24,7 @@ import ( const RolesReadmeFile = "README.md" type RolesReadme struct { - input.Input + StaticInput Resource scaffold.Resource } diff --git a/internal/pkg/scaffold/ansible/roles_tasks_main.go b/internal/pkg/scaffold/ansible/roles_tasks_main.go index 2c25a677d2a..08c9b002208 100644 --- a/internal/pkg/scaffold/ansible/roles_tasks_main.go +++ b/internal/pkg/scaffold/ansible/roles_tasks_main.go @@ -34,10 +34,11 @@ func (r *RolesTasksMain) GetInput() (input.Input, error) { r.Path = filepath.Join(RolesDir, r.Resource.LowerKind, RolesTasksMainFile) } r.TemplateBody = rolesTasksMainAnsibleTmpl + r.Delims = AnsibleDelims return r.Input, nil } const rolesTasksMainAnsibleTmpl = `--- -# tasks file for {{.Resource.LowerKind}} +# tasks file for [[.Resource.LowerKind]] ` diff --git a/internal/pkg/scaffold/ansible/roles_templates.go b/internal/pkg/scaffold/ansible/roles_templates.go index 1450be7750b..e81e2975309 100644 --- a/internal/pkg/scaffold/ansible/roles_templates.go +++ b/internal/pkg/scaffold/ansible/roles_templates.go @@ -24,7 +24,7 @@ import ( const RolesTemplatesDir = "templates" + filePathSep + ".placeholder" type RolesTemplates struct { - input.Input + StaticInput Resource scaffold.Resource } diff --git a/internal/pkg/scaffold/ansible/roles_vars_main.go b/internal/pkg/scaffold/ansible/roles_vars_main.go index b7e53f63db1..eeff5945764 100644 --- a/internal/pkg/scaffold/ansible/roles_vars_main.go +++ b/internal/pkg/scaffold/ansible/roles_vars_main.go @@ -34,10 +34,11 @@ func (r *RolesVarsMain) GetInput() (input.Input, error) { r.Path = filepath.Join(RolesDir, r.Resource.LowerKind, RolesVarsMainFile) } r.TemplateBody = rolesVarsMainAnsibleTmpl + r.Delims = AnsibleDelims return r.Input, nil } const rolesVarsMainAnsibleTmpl = `--- -# vars file for {{.Resource.LowerKind}} +# vars file for [[.Resource.LowerKind]] ` diff --git a/internal/pkg/scaffold/ansible/travis.go b/internal/pkg/scaffold/ansible/travis.go index 979f7caa611..43f5a14e3cc 100644 --- a/internal/pkg/scaffold/ansible/travis.go +++ b/internal/pkg/scaffold/ansible/travis.go @@ -19,7 +19,7 @@ import "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input" const TravisFile = ".travis.yml" type Travis struct { - input.Input + StaticInput } // GetInput - gets the input diff --git a/internal/pkg/scaffold/ansible/usersetup.go b/internal/pkg/scaffold/ansible/usersetup.go index c9da484255e..f43bdec305b 100644 --- a/internal/pkg/scaffold/ansible/usersetup.go +++ b/internal/pkg/scaffold/ansible/usersetup.go @@ -22,7 +22,7 @@ import ( // UserSetup - userSetup script type UserSetup struct { - input.Input + StaticInput } func (u *UserSetup) GetInput() (input.Input, error) { diff --git a/internal/pkg/scaffold/ansible/watches.go b/internal/pkg/scaffold/ansible/watches.go index 10ca417c935..b724658bce4 100644 --- a/internal/pkg/scaffold/ansible/watches.go +++ b/internal/pkg/scaffold/ansible/watches.go @@ -34,17 +34,18 @@ func (w *Watches) GetInput() (input.Input, error) { w.Path = WatchesFile } w.TemplateBody = watchesAnsibleTmpl + w.Delims = AnsibleDelims w.RolesDir = RolesDir return w.Input, nil } const watchesAnsibleTmpl = `--- -- version: {{.Resource.Version}} - group: {{.Resource.FullGroup}} - kind: {{.Resource.Kind}} - {{- if .GeneratePlaybook }} +- version: [[.Resource.Version]] + group: [[.Resource.FullGroup]] + kind: [[.Resource.Kind]] + [[- if .GeneratePlaybook ]] playbook: /opt/ansible/playbook.yml - {{- else }} - role: /opt/ansible/{{.RolesDir}}/{{.Resource.LowerKind}} - {{- end }} + [[- else ]] + role: /opt/ansible/[[.RolesDir]]/[[.Resource.LowerKind]] + [[- end ]] ` diff --git a/internal/pkg/scaffold/gopkgtoml.go b/internal/pkg/scaffold/gopkgtoml.go index 160c439be68..a669bd63df4 100644 --- a/internal/pkg/scaffold/gopkgtoml.go +++ b/internal/pkg/scaffold/gopkgtoml.go @@ -43,7 +43,6 @@ func (s *GopkgToml) GetInput() (input.Input, error) { const gopkgTomlTmpl = `# Force dep to vendor the code generators, which aren't imported just used at dev time. required = [ - "k8s.io/code-generator/cmd/defaulter-gen", "k8s.io/code-generator/cmd/deepcopy-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/client-gen", diff --git a/internal/pkg/scaffold/gopkgtoml_test.go b/internal/pkg/scaffold/gopkgtoml_test.go index eeb7c8d40fb..b2f52e25cdd 100644 --- a/internal/pkg/scaffold/gopkgtoml_test.go +++ b/internal/pkg/scaffold/gopkgtoml_test.go @@ -35,7 +35,6 @@ func TestGopkgtoml(t *testing.T) { const gopkgtomlExp = `# Force dep to vendor the code generators, which aren't imported just used at dev time. required = [ - "k8s.io/code-generator/cmd/defaulter-gen", "k8s.io/code-generator/cmd/deepcopy-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/client-gen", diff --git a/internal/pkg/scaffold/helm/chart.go b/internal/pkg/scaffold/helm/chart.go index 89fcc51bc11..7bbb83a3d57 100644 --- a/internal/pkg/scaffold/helm/chart.go +++ b/internal/pkg/scaffold/helm/chart.go @@ -15,6 +15,7 @@ package helm import ( + "bytes" "fmt" "io/ioutil" "os" @@ -116,7 +117,7 @@ func CreateChart(projectDir string, opts CreateChartOptions) (*scaffold.Resource chartsDir := filepath.Join(projectDir, HelmChartsDir) err := os.MkdirAll(chartsDir, 0755) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to create helm-charts directory: %s", err) } var ( @@ -128,13 +129,29 @@ func CreateChart(projectDir string, opts CreateChartOptions) (*scaffold.Resource // from Helm's default template. Otherwise, fetch it. if len(opts.Chart) == 0 { r, c, err = scaffoldChart(chartsDir, opts.ResourceAPIVersion, opts.ResourceKind) + if err != nil { + return nil, nil, fmt.Errorf("failed to scaffold default chart: %s", err) + } } else { r, c, err = fetchChart(chartsDir, opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch chart: %s", err) + } + } + + relChartPath := filepath.Join(HelmChartsDir, c.GetMetadata().GetName()) + absChartPath := filepath.Join(projectDir, relChartPath) + if err := fetchChartDependencies(absChartPath); err != nil { + return nil, nil, fmt.Errorf("failed to fetch chart dependencies: %s", err) } + + // Reload chart in case dependencies changed + c, err = chartutil.Load(absChartPath) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to load chart: %s", err) } - log.Infof("Created %s/%s/", HelmChartsDir, c.GetMetadata().GetName()) + + log.Infof("Created %s", relChartPath) return r, c, nil } @@ -159,7 +176,7 @@ func scaffoldChart(destDir, apiVersion, kind string) (*scaffold.Resource, *chart return nil, nil, err } - chart, err := chartutil.LoadDir(chartPath) + chart, err := chartutil.Load(chartPath) if err != nil { return nil, nil, err } @@ -198,17 +215,7 @@ func fetchChart(destDir string, opts CreateChartOptions) (*scaffold.Resource, *c } func createChartFromDisk(destDir, source string, isDir bool) (*chart.Chart, error) { - var ( - chart *chart.Chart - err error - ) - - // If source is a file or directory, attempt to load it - if isDir { - chart, err = chartutil.LoadDir(source) - } else { - chart, err = chartutil.LoadFile(source) - } + chart, err := chartutil.Load(source) if err != nil { return nil, err } @@ -265,3 +272,24 @@ func createChartFromRemote(destDir string, opts CreateChartOptions) (*chart.Char return createChartFromDisk(destDir, chartArchive, false) } + +func fetchChartDependencies(chartPath string) error { + helmHome, ok := os.LookupEnv(environment.HomeEnvVar) + if !ok { + helmHome = environment.DefaultHelmHome + } + getters := getter.All(environment.EnvSettings{}) + + out := &bytes.Buffer{} + man := &downloader.Manager{ + Out: out, + ChartPath: chartPath, + HelmHome: helmpath.Home(helmHome), + Getters: getters, + } + if err := man.Build(); err != nil { + fmt.Println(out.String()) + return err + } + return nil +} diff --git a/internal/pkg/scaffold/helm/role.go b/internal/pkg/scaffold/helm/role.go new file mode 100644 index 00000000000..385c9de384f --- /dev/null +++ b/internal/pkg/scaffold/helm/role.go @@ -0,0 +1,241 @@ +// 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 helm + +import ( + "errors" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/operator-framework/operator-sdk/internal/pkg/scaffold" + + "github.com/ghodss/yaml" + log "github.com/sirupsen/logrus" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/manifest" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/renderutil" + "k8s.io/helm/pkg/tiller" +) + +// CreateRoleScaffold 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. +func CreateRoleScaffold(cfg *rest.Config, chart *chart.Chart, isClusterScoped bool) (*scaffold.Role, error) { + log.Info("Generating RBAC rules") + + roleScaffold := &scaffold.Role{ + IsClusterScoped: isClusterScoped, + SkipDefaultRules: true, + // TODO: enable metrics in helm operator + SkipMetricsRules: true, + CustomRules: []rbacv1.PolicyRule{ + // We need this rule so tiller 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}, + }, + }, + } + + clusterResourceRules, namespacedResourceRules, err := generateRoleRules(cfg, chart) + if err != nil { + log.Warnf("Using default RBAC rules: failed to generate RBAC rules: %s", err) + roleScaffold.SkipDefaultRules = false + } + + if !isClusterScoped { + // If there are cluster-scoped resources, but we're creating a namespace-scoped operator, + // log all of the cluster-scoped resources, and return a helpful error message. + for _, rule := range clusterResourceRules { + for _, resource := range rule.Resources { + log.Errorf("Resource %s.%s is cluster-scoped, but --cluster-scoped was not set.", resource, rule.APIGroups[0]) + } + } + if len(clusterResourceRules) > 0 { + return nil, errors.New("must use --cluster-scoped with chart containing cluster-scoped resources") + } + + // If we're here, there are no cluster-scoped resources, so add just the rules for namespaced resources + roleScaffold.CustomRules = append(roleScaffold.CustomRules, namespacedResourceRules...) + } else { + // For a cluster-scoped operator, add all of the rules + roleScaffold.CustomRules = append(roleScaffold.CustomRules, append(clusterResourceRules, namespacedResourceRules...)...) + } + + log.Warn("The RBAC rules generated in deploy/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 deploy/role.yaml" + + " to ensure they meet the operator's permission requirements.") + + return roleScaffold, nil +} + +func generateRoleRules(cfg *rest.Config, chart *chart.Chart) ([]rbacv1.PolicyRule, []rbacv1.PolicyRule, error) { + kubeVersion, serverResources, err := getServerVersionAndResources(cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to get server info: %s", err) + } + + manifests, err := getDefaultManifests(chart, kubeVersion) + if err != nil { + return nil, nil, fmt.Errorf("failed to get default manifest: %s", 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 getServerVersionAndResources(cfg *rest.Config) (*version.Info, []*metav1.APIResourceList, error) { + dc, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, nil, fmt.Errorf("failed to create discovery client: %s", err) + } + kubeVersion, err := dc.ServerVersion() + if err != nil { + return nil, nil, fmt.Errorf("failed to get kubernetes server version: %s", err) + } + serverResources, err := dc.ServerResources() + if err != nil { + return nil, nil, fmt.Errorf("failed to get kubernetes server resources: %s", err) + } + return kubeVersion, serverResources, nil +} + +func getDefaultManifests(c *chart.Chart, kubeVersion *version.Info) ([]tiller.Manifest, error) { + renderOpts := renderutil.Options{ + ReleaseOptions: chartutil.ReleaseOptions{ + IsInstall: true, + IsUpgrade: false, + }, + KubeVersion: fmt.Sprintf("%s.%s", kubeVersion.Major, kubeVersion.Minor), + } + + renderedTemplates, err := renderutil.Render(c, &chart.Config{}, renderOpts) + if err != nil { + return nil, fmt.Errorf("failed to render chart templates: %s", err) + } + return tiller.SortByKind(manifest.SplitManifests(renderedTemplates)), nil +} + +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 +} diff --git a/internal/pkg/scaffold/input/input.go b/internal/pkg/scaffold/input/input.go index d4da73d400c..6d543cd368c 100644 --- a/internal/pkg/scaffold/input/input.go +++ b/internal/pkg/scaffold/input/input.go @@ -60,6 +60,10 @@ type Input struct { // ProjectName is the operator's name, ex. app-operator ProjectName string + + // Delims is a slice of two strings representing the left and right delimiter + // defaults to {{ }} + Delims [2]string } // Repo allows a repo to be set on an object diff --git a/internal/pkg/scaffold/role.go b/internal/pkg/scaffold/role.go index da1cbe827a6..ce028512f14 100644 --- a/internal/pkg/scaffold/role.go +++ b/internal/pkg/scaffold/role.go @@ -35,7 +35,10 @@ const RoleYamlFile = "role.yaml" type Role struct { input.Input - IsClusterScoped bool + IsClusterScoped bool + SkipDefaultRules bool + SkipMetricsRules bool + CustomRules []rbacv1.PolicyRule } func (s *Role) GetInput() (input.Input, error) { @@ -157,6 +160,7 @@ apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{.ProjectName}} rules: +{{- if not .SkipDefaultRules }} - apiGroups: - "" resources: @@ -169,12 +173,6 @@ rules: - secrets verbs: - "*" -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get - apiGroups: - apps resources: @@ -184,6 +182,38 @@ rules: - statefulsets verbs: - "*" +{{- 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 }} +{{- if not .SkipMetricsRules }} - apiGroups: - monitoring.coreos.com resources: @@ -199,4 +229,5 @@ rules: - {{ .ProjectName }} verbs: - "update" +{{- end }} ` diff --git a/internal/pkg/scaffold/role_test.go b/internal/pkg/scaffold/role_test.go index ef8b240b73a..1910fdd7b23 100644 --- a/internal/pkg/scaffold/role_test.go +++ b/internal/pkg/scaffold/role_test.go @@ -18,6 +18,8 @@ import ( "testing" "github.com/operator-framework/operator-sdk/internal/util/diffutil" + + rbacv1 "k8s.io/api/rbac/v1" ) func TestRole(t *testing.T) { @@ -46,6 +48,33 @@ func TestRoleClusterScoped(t *testing.T) { } } +func TestRoleCustomRules(t *testing.T) { + s, buf := setupScaffoldAndWriter() + err := s.Execute(appConfig, &Role{ + SkipDefaultRules: true, + SkipMetricsRules: true, + CustomRules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"policy"}, + Resources: []string{"poddisruptionbudgets"}, + Verbs: []string{rbacv1.VerbAll}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"roles", "rolebindings"}, + Verbs: []string{"get", "list", "watch"}, + }, + }}) + if err != nil { + t.Fatalf("Failed to execute the scaffold: (%v)", err) + } + + if roleCustomRulesExp != buf.String() { + diffs := diffutil.Diff(roleCustomRulesExp, buf.String()) + t.Fatalf("Expected vs actual differs.\n%v", diffs) + } +} + const roleExp = `kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -63,12 +92,6 @@ rules: - secrets verbs: - "*" -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get - apiGroups: - apps resources: @@ -112,12 +135,6 @@ rules: - secrets verbs: - "*" -- apiGroups: - - "" - resources: - - namespaces - verbs: - - get - apiGroups: - apps resources: @@ -143,3 +160,25 @@ rules: verbs: - "update" ` + +const roleCustomRulesExp = `kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: app-operator +rules: +- verbs: + - "*" + apiGroups: + - "policy" + resources: + - "poddisruptionbudgets" +- verbs: + - "get" + - "list" + - "watch" + apiGroups: + - "rbac.authorization.k8s.io" + resources: + - "roles" + - "rolebindings" +` diff --git a/internal/pkg/scaffold/scaffold.go b/internal/pkg/scaffold/scaffold.go index d84c08dd37a..5c7e281108d 100644 --- a/internal/pkg/scaffold/scaffold.go +++ b/internal/pkg/scaffold/scaffold.go @@ -195,5 +195,8 @@ func newTemplate(i input.Input) (*template.Template, error) { if len(i.TemplateFuncs) > 0 { t.Funcs(i.TemplateFuncs) } + if i.Delims[0] != "" && i.Delims[1] != "" { + t.Delims(i.Delims[0], i.Delims[1]) + } return t.Parse(i.TemplateBody) } diff --git a/internal/util/projutil/project_util.go b/internal/util/projutil/project_util.go index be56975e048..7aa8ee4076b 100644 --- a/internal/util/projutil/project_util.go +++ b/internal/util/projutil/project_util.go @@ -22,10 +22,6 @@ import ( "regexp" "strings" - "github.com/operator-framework/operator-sdk/internal/pkg/scaffold" - "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/ansible" - "github.com/operator-framework/operator-sdk/internal/pkg/scaffold/helm" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -34,9 +30,13 @@ const ( GopathEnv = "GOPATH" GoFlagsEnv = "GOFLAGS" SrcDir = "src" -) -var mainFile = filepath.Join(scaffold.ManagerDir, scaffold.CmdFile) + fsep = string(filepath.Separator) + mainFile = "cmd" + fsep + "manager" + fsep + "main.go" + buildDockerfile = "build" + fsep + "Dockerfile" + rolesDir = "roles" + helmChartsDir = "helm-charts" +) // OperatorType - the type of operator type OperatorType = string @@ -52,28 +52,35 @@ const ( OperatorTypeUnknown OperatorType = "unknown" ) -// MustInProjectRoot checks if the current dir is the project root and returns the current repo's import path -// e.g github.com/example-inc/app-operator +type ErrUnknownOperatorType struct { + Type string +} + +func (e ErrUnknownOperatorType) Error() string { + if e.Type == "" { + return "unknown operator type" + } + return fmt.Sprintf(`unknown operator type "%v"`, e.Type) +} + +// MustInProjectRoot checks if the current dir is the project root and returns +// the current repo's import path, ex github.com/example-inc/app-operator func MustInProjectRoot() { - // if the current directory has the "./build/dockerfile" file, then it is safe to say + // If the current directory has a "build/dockerfile", then it is safe to say // we are at the project root. - _, err := os.Stat(filepath.Join(scaffold.BuildDir, scaffold.DockerfileFile)) - if err != nil { + if _, err := os.Stat(buildDockerfile); err != nil { if os.IsNotExist(err) { - log.Fatal("Must run command in project root dir: project structure requires ./build/Dockerfile") + log.Fatalf("Must run command in project root dir: project structure requires %s", buildDockerfile) } log.Fatalf("Error while checking if current directory is the project root: (%v)", err) } } func CheckGoProjectCmd(cmd *cobra.Command) error { - t := GetOperatorType() - switch t { - case OperatorTypeGo: - default: - return fmt.Errorf("'%s' can only be run for Go operators; %s does not exist.", cmd.CommandPath(), mainFile) + if IsOperatorGo() { + return nil } - return nil + return fmt.Errorf("'%s' can only be run for Go operators; %s does not exist.", cmd.CommandPath(), mainFile) } func MustGetwd() string { @@ -90,30 +97,38 @@ func CheckAndGetProjectGoPkg() string { gopath := MustSetGopath(MustGetGopath()) goSrc := filepath.Join(gopath, SrcDir) wd := MustGetwd() - currPkg := strings.Replace(wd, goSrc+string(filepath.Separator), "", 1) + currPkg := strings.Replace(wd, goSrc+fsep, "", 1) // strip any "/" prefix from the repo path. - return strings.TrimPrefix(currPkg, string(filepath.Separator)) + return strings.TrimPrefix(currPkg, fsep) } -// GetOperatorType returns type of operator is in cwd -// This function should be called after verifying the user is in project root -// e.g: "go", "ansible" +// GetOperatorType returns type of operator is in cwd. +// This function should be called after verifying the user is in project root. func GetOperatorType() OperatorType { - // Assuming that if main.go exists then this is a Go operator - if _, err := os.Stat(mainFile); err == nil { + switch { + case IsOperatorGo(): return OperatorTypeGo - } - if stat, err := os.Stat(ansible.RolesDir); err == nil && stat.IsDir() { + case IsOperatorAnsible(): return OperatorTypeAnsible - } - if stat, err := os.Stat(helm.HelmChartsDir); err == nil && stat.IsDir() { + case IsOperatorHelm(): return OperatorTypeHelm } return OperatorTypeUnknown } func IsOperatorGo() bool { - return GetOperatorType() == OperatorTypeGo + _, err := os.Stat(mainFile) + return err == nil +} + +func IsOperatorAnsible() bool { + stat, err := os.Stat(rolesDir) + return err == nil && stat.IsDir() +} + +func IsOperatorHelm() bool { + stat, err := os.Stat(helmChartsDir) + return err == nil && stat.IsDir() } // MustGetGopath gets GOPATH and ensures it is set and non-empty. If GOPATH diff --git a/pkg/log/zap/flags.go b/pkg/log/zap/flags.go index cba8183a0a7..d94911d94b3 100644 --- a/pkg/log/zap/flags.go +++ b/pkg/log/zap/flags.go @@ -15,6 +15,7 @@ package zap import ( + "flag" "fmt" "strconv" "strings" @@ -22,6 +23,7 @@ import ( "github.com/spf13/pflag" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "k8s.io/klog" ) var ( @@ -41,6 +43,7 @@ func init() { zapFlagSet.Var(&sampleVal, "zap-sample", "Enable zap log sampling. Sampling will be disabled for integer log levels > 1") } +// FlagSet - The zap logging flagset. func FlagSet() *pflag.FlagSet { return zapFlagSet } @@ -112,6 +115,15 @@ func (v *levelValue) Set(l string) error { } } v.level = zapcore.Level(int8(lvl)) + // If log level is greater than debug, set glog/klog level to that level. + if lvl < -3 { + fs := flag.NewFlagSet("", flag.ContinueOnError) + klog.InitFlags(fs) + err := fs.Set("v", fmt.Sprintf("%v", -1*lvl)) + if err != nil { + return err + } + } return nil } diff --git a/vendor/k8s.io/helm/pkg/renderutil/deps.go b/vendor/k8s.io/helm/pkg/renderutil/deps.go new file mode 100644 index 00000000000..72e4d12a15c --- /dev/null +++ b/vendor/k8s.io/helm/pkg/renderutil/deps.go @@ -0,0 +1,50 @@ +/* +Copyright The Helm 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 renderutil + +import ( + "fmt" + "strings" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +// CheckDependencies will do a simple dependency check on the chart for local +// rendering +func CheckDependencies(ch *chart.Chart, reqs *chartutil.Requirements) error { + missing := []string{} + + deps := ch.GetDependencies() + for _, r := range reqs.Dependencies { + found := false + for _, d := range deps { + if d.Metadata.Name == r.Name { + found = true + break + } + } + if !found { + missing = append(missing, r.Name) + } + } + + if len(missing) > 0 { + return fmt.Errorf("found in requirements.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) + } + return nil +} diff --git a/vendor/k8s.io/helm/pkg/renderutil/doc.go b/vendor/k8s.io/helm/pkg/renderutil/doc.go new file mode 100644 index 00000000000..38c3ae60d23 --- /dev/null +++ b/vendor/k8s.io/helm/pkg/renderutil/doc.go @@ -0,0 +1,24 @@ +/* +Copyright The Helm 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 renderutil contains tools related to the local rendering of charts. + +Local rendering means rendering without the tiller; this is generally used for +local debugging and testing (see the `helm template` command for examples of +use). This package will not render charts exactly the same way as the tiller +will, but will be generally close enough for local debug purposes. +*/ +package renderutil // import "k8s.io/helm/pkg/renderutil" diff --git a/vendor/k8s.io/helm/pkg/renderutil/render.go b/vendor/k8s.io/helm/pkg/renderutil/render.go new file mode 100644 index 00000000000..1996e1dc297 --- /dev/null +++ b/vendor/k8s.io/helm/pkg/renderutil/render.go @@ -0,0 +1,88 @@ +/* +Copyright The Helm 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 renderutil + +import ( + "fmt" + + "github.com/Masterminds/semver" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/engine" + "k8s.io/helm/pkg/proto/hapi/chart" + tversion "k8s.io/helm/pkg/version" +) + +// Options are options for this simple local render +type Options struct { + ReleaseOptions chartutil.ReleaseOptions + KubeVersion string +} + +// Render chart templates locally and display the output. +// This does not require the Tiller. Any values that would normally be +// looked up or retrieved in-cluster will be faked locally. Additionally, none +// of the server-side testing of chart validity (e.g. whether an API is supported) +// is done. +// +// Note: a `nil` config passed here means "ignore the chart's default values"; +// if you want the normal behavior of merging the defaults with the new config, +// you should pass `&chart.Config{Raw: "{}"}, +func Render(c *chart.Chart, config *chart.Config, opts Options) (map[string]string, error) { + if req, err := chartutil.LoadRequirements(c); err == nil { + if err := CheckDependencies(c, req); err != nil { + return nil, err + } + } else if err != chartutil.ErrRequirementsNotFound { + return nil, fmt.Errorf("cannot load requirements: %v", err) + } + + err := chartutil.ProcessRequirementsEnabled(c, config) + if err != nil { + return nil, err + } + err = chartutil.ProcessRequirementsImportValues(c) + if err != nil { + return nil, err + } + + // Set up engine. + renderer := engine.New() + + caps := &chartutil.Capabilities{ + APIVersions: chartutil.DefaultVersionSet, + KubeVersion: chartutil.DefaultKubeVersion, + TillerVersion: tversion.GetVersionProto(), + } + + if opts.KubeVersion != "" { + kv, verErr := semver.NewVersion(opts.KubeVersion) + if verErr != nil { + return nil, fmt.Errorf("could not parse a kubernetes version: %v", verErr) + } + caps.KubeVersion.Major = fmt.Sprint(kv.Major()) + caps.KubeVersion.Minor = fmt.Sprint(kv.Minor()) + caps.KubeVersion.GitVersion = fmt.Sprintf("v%d.%d.0", kv.Major(), kv.Minor()) + } + + vals, err := chartutil.ToRenderValuesCaps(c, config, opts.ReleaseOptions, caps) + if err != nil { + return nil, err + } + + return renderer.Render(c, vals) +}