github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/helm/role.go (about)

     1  // Copyright 2019 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helm
    16  
    17  import (
    18  	"fmt"
    19  	"path/filepath"
    20  	"sort"
    21  	"strings"
    22  
    23  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    24  
    25  	"github.com/ghodss/yaml"
    26  	log "github.com/sirupsen/logrus"
    27  	rbacv1 "k8s.io/api/rbac/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/version"
    31  	"k8s.io/client-go/discovery"
    32  	"k8s.io/client-go/rest"
    33  	"k8s.io/helm/pkg/chartutil"
    34  	"k8s.io/helm/pkg/manifest"
    35  	"k8s.io/helm/pkg/proto/hapi/chart"
    36  	"k8s.io/helm/pkg/renderutil"
    37  	"k8s.io/helm/pkg/tiller"
    38  )
    39  
    40  // CreateRoleScaffold generates a role scaffold from the provided helm chart. It
    41  // renders a release manifest using the chart's default values and uses the Kubernetes
    42  // discovery API to lookup each resource in the resulting manifest.
    43  // The role scaffold will have IsClusterScoped=true if the chart lists cluster scoped resources
    44  func CreateRoleScaffold(cfg *rest.Config, chart *chart.Chart) (*scaffold.Role, error) {
    45  	log.Info("Generating RBAC rules")
    46  
    47  	roleScaffold := &scaffold.Role{
    48  		IsClusterScoped:  false,
    49  		SkipDefaultRules: true,
    50  		// TODO: enable metrics in helm operator
    51  		SkipMetricsRules: true,
    52  		CustomRules: []rbacv1.PolicyRule{
    53  			// We need this rule so tiller can read namespaces to ensure they exist
    54  			{
    55  				APIGroups: []string{""},
    56  				Resources: []string{"namespaces"},
    57  				Verbs:     []string{"get"},
    58  			},
    59  
    60  			// We need this rule for leader election and release state storage to work
    61  			{
    62  				APIGroups: []string{""},
    63  				Resources: []string{"configmaps", "secrets"},
    64  				Verbs:     []string{rbacv1.VerbAll},
    65  			},
    66  		},
    67  	}
    68  
    69  	clusterResourceRules, namespacedResourceRules, err := generateRoleRules(cfg, chart)
    70  	if err != nil {
    71  		log.Warnf("Using default RBAC rules: failed to generate RBAC rules: %s", err)
    72  		roleScaffold.SkipDefaultRules = false
    73  		return roleScaffold, nil
    74  	}
    75  
    76  	// Use a ClusterRole if cluster scoped resources are listed in the chart
    77  	if len(clusterResourceRules) > 0 {
    78  		log.Info("Scaffolding ClusterRole and ClusterRolebinding for cluster scoped resources in the helm chart")
    79  		roleScaffold.IsClusterScoped = true
    80  	}
    81  	roleScaffold.CustomRules = append(roleScaffold.CustomRules, append(clusterResourceRules, namespacedResourceRules...)...)
    82  
    83  	log.Warn("The RBAC rules generated in deploy/role.yaml are based on the chart's default manifest." +
    84  		" Some rules may be missing for resources that are only enabled with custom values, and" +
    85  		" some existing rules may be overly broad. Double check the rules generated in deploy/role.yaml" +
    86  		" to ensure they meet the operator's permission requirements.")
    87  
    88  	return roleScaffold, nil
    89  }
    90  
    91  func generateRoleRules(cfg *rest.Config, chart *chart.Chart) ([]rbacv1.PolicyRule, []rbacv1.PolicyRule, error) {
    92  	kubeVersion, serverResources, err := getServerVersionAndResources(cfg)
    93  	if err != nil {
    94  		return nil, nil, fmt.Errorf("failed to get server info: %s", err)
    95  	}
    96  
    97  	manifests, err := getDefaultManifests(chart, kubeVersion)
    98  	if err != nil {
    99  		return nil, nil, fmt.Errorf("failed to get default manifest: %s", err)
   100  	}
   101  
   102  	// Use maps of sets of resources, keyed by their group. This helps us
   103  	// de-duplicate resources within a group as we traverse the manifests.
   104  	clusterGroups := map[string]map[string]struct{}{}
   105  	namespacedGroups := map[string]map[string]struct{}{}
   106  
   107  	for _, m := range manifests {
   108  		name := m.Name
   109  		content := strings.TrimSpace(m.Content)
   110  
   111  		// Ignore NOTES.txt, helper manifests, and empty manifests.
   112  		b := filepath.Base(name)
   113  		if b == "NOTES.txt" {
   114  			continue
   115  		}
   116  		if strings.HasPrefix(b, "_") {
   117  			continue
   118  		}
   119  		if content == "" || content == "---" {
   120  			continue
   121  		}
   122  
   123  		// Extract the gvk from the template
   124  		resource := unstructured.Unstructured{}
   125  		err := yaml.Unmarshal([]byte(content), &resource)
   126  		if err != nil {
   127  			log.Warnf("Skipping rule generation for %s. Failed to parse manifest: %s", name, err)
   128  			continue
   129  		}
   130  		groupVersion := resource.GetAPIVersion()
   131  		group := resource.GroupVersionKind().Group
   132  		kind := resource.GroupVersionKind().Kind
   133  
   134  		// If we don't have the group or the kind, we won't be able to
   135  		// create a valid role rule, log a warning and continue.
   136  		if groupVersion == "" {
   137  			log.Warnf("Skipping rule generation for %s. Failed to determine resource apiVersion.", name)
   138  			continue
   139  		}
   140  		if kind == "" {
   141  			log.Warnf("Skipping rule generation for %s. Failed to determine resource kind.", name)
   142  			continue
   143  		}
   144  
   145  		if resourceName, namespaced, ok := getResource(serverResources, groupVersion, kind); ok {
   146  			if !namespaced {
   147  				if clusterGroups[group] == nil {
   148  					clusterGroups[group] = map[string]struct{}{}
   149  				}
   150  				clusterGroups[group][resourceName] = struct{}{}
   151  			} else {
   152  				if namespacedGroups[group] == nil {
   153  					namespacedGroups[group] = map[string]struct{}{}
   154  				}
   155  				namespacedGroups[group][resourceName] = struct{}{}
   156  			}
   157  		} else {
   158  			log.Warnf("Skipping rule generation for %s. Failed to determine resource scope for %s.", name, resource.GroupVersionKind())
   159  			continue
   160  		}
   161  	}
   162  
   163  	// convert map[string]map[string]struct{} to []rbacv1.PolicyRule
   164  	clusterRules := buildRulesFromGroups(clusterGroups)
   165  	namespacedRules := buildRulesFromGroups(namespacedGroups)
   166  
   167  	return clusterRules, namespacedRules, nil
   168  }
   169  
   170  func getServerVersionAndResources(cfg *rest.Config) (*version.Info, []*metav1.APIResourceList, error) {
   171  	dc, err := discovery.NewDiscoveryClientForConfig(cfg)
   172  	if err != nil {
   173  		return nil, nil, fmt.Errorf("failed to create discovery client: %s", err)
   174  	}
   175  	kubeVersion, err := dc.ServerVersion()
   176  	if err != nil {
   177  		return nil, nil, fmt.Errorf("failed to get kubernetes server version: %s", err)
   178  	}
   179  	serverResources, err := dc.ServerResources()
   180  	if err != nil {
   181  		return nil, nil, fmt.Errorf("failed to get kubernetes server resources: %s", err)
   182  	}
   183  	return kubeVersion, serverResources, nil
   184  }
   185  
   186  func getDefaultManifests(c *chart.Chart, kubeVersion *version.Info) ([]tiller.Manifest, error) {
   187  	v := strings.TrimSuffix(fmt.Sprintf("%s.%s", kubeVersion.Major, kubeVersion.Minor), "+")
   188  	renderOpts := renderutil.Options{
   189  		ReleaseOptions: chartutil.ReleaseOptions{
   190  			IsInstall: true,
   191  			IsUpgrade: false,
   192  		},
   193  		KubeVersion: v,
   194  	}
   195  
   196  	renderedTemplates, err := renderutil.Render(c, &chart.Config{}, renderOpts)
   197  	if err != nil {
   198  		return nil, fmt.Errorf("failed to render chart templates: %s", err)
   199  	}
   200  	return tiller.SortByKind(manifest.SplitManifests(renderedTemplates)), nil
   201  }
   202  
   203  func getResource(namespacedResourceList []*metav1.APIResourceList, groupVersion, kind string) (string, bool, bool) {
   204  	for _, apiResourceList := range namespacedResourceList {
   205  		if apiResourceList.GroupVersion == groupVersion {
   206  			for _, apiResource := range apiResourceList.APIResources {
   207  				if apiResource.Kind == kind {
   208  					return apiResource.Name, apiResource.Namespaced, true
   209  				}
   210  			}
   211  		}
   212  	}
   213  	return "", false, false
   214  }
   215  
   216  func buildRulesFromGroups(groups map[string]map[string]struct{}) []rbacv1.PolicyRule {
   217  	rules := []rbacv1.PolicyRule{}
   218  	for group, resourceNames := range groups {
   219  		resources := []string{}
   220  		for resource := range resourceNames {
   221  			resources = append(resources, resource)
   222  		}
   223  		sort.Strings(resources)
   224  		rules = append(rules, rbacv1.PolicyRule{
   225  			APIGroups: []string{group},
   226  			Resources: resources,
   227  			Verbs:     []string{rbacv1.VerbAll},
   228  		})
   229  	}
   230  	return rules
   231  }