diff --git a/CHANGELOG.md b/CHANGELOG.md index 308e09514a..ba8cbf26cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 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)) + ### Deprecated ### Removed diff --git a/Gopkg.lock b/Gopkg.lock index 23ba044ca7..514a460a09 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", @@ -1773,6 +1774,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", @@ -1793,10 +1795,12 @@ "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", diff --git a/cmd/operator-sdk/new/cmd.go b/cmd/operator-sdk/new/cmd.go index c3dfc70f43..0bb9d8da9b 100644 --- a/cmd/operator-sdk/new/cmd.go +++ b/cmd/operator-sdk/new/cmd.go @@ -30,6 +30,7 @@ import ( "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 { @@ -281,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{}, @@ -289,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}, diff --git a/doc/helm/user-guide.md b/doc/helm/user-guide.md index 6af442fa5e..b3cf0826dc 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/internal/pkg/scaffold/helm/chart.go b/internal/pkg/scaffold/helm/chart.go index 89fcc51bc1..7bbb83a3d5 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 0000000000..385c9de384 --- /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/role.go b/internal/pkg/scaffold/role.go index da1cbe827a..ce028512f1 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 ef8b240b73..1910fdd7b2 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/vendor/k8s.io/helm/pkg/renderutil/deps.go b/vendor/k8s.io/helm/pkg/renderutil/deps.go new file mode 100644 index 0000000000..72e4d12a15 --- /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 0000000000..38c3ae60d2 --- /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 0000000000..1996e1dc29 --- /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) +}