-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
Copy pathrole.go
232 lines (205 loc) · 7.81 KB
/
role.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// 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 (
"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/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"
)
// roleDiscoveryInterface is an interface that contains just the discovery
// methods needed by the Helm role scaffold generator. Requiring just this
// interface simplifies testing.
type roleDiscoveryInterface interface {
discovery.ServerVersionInterface
ServerResources() ([]*metav1.APIResourceList, error)
}
// 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.
// The role scaffold will have IsClusterScoped=true if the chart lists cluster scoped resources
func CreateRoleScaffold(dc roleDiscoveryInterface, chart *chart.Chart) (*scaffold.Role, error) {
log.Info("Generating RBAC rules")
roleScaffold := &scaffold.Role{
IsClusterScoped: false,
SkipDefaultRules: 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(dc, chart)
if err != nil {
log.Warnf("Using default RBAC rules: failed to generate RBAC rules: %s", err)
roleScaffold.SkipDefaultRules = false
return roleScaffold, nil
}
// Use a ClusterRole if cluster scoped resources are listed in the chart
if len(clusterResourceRules) > 0 {
log.Info("Scaffolding ClusterRole and ClusterRolebinding for cluster scoped resources in the helm chart")
roleScaffold.IsClusterScoped = true
}
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(dc roleDiscoveryInterface, chart *chart.Chart) ([]rbacv1.PolicyRule, []rbacv1.PolicyRule, error) {
kubeVersion, serverResources, err := getServerVersionAndResources(dc)
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(dc roleDiscoveryInterface) (*version.Info, []*metav1.APIResourceList, error) {
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) {
v := strings.TrimSuffix(fmt.Sprintf("%s.%s", kubeVersion.Major, kubeVersion.Minor), "+")
renderOpts := renderutil.Options{
ReleaseOptions: chartutil.ReleaseOptions{
IsInstall: true,
IsUpgrade: false,
},
KubeVersion: v,
}
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
}