Skip to content

Commit 65bc862

Browse files
committed
Add client-only helm dry-run to helm applier
Adds a check that makes sure the installer service account has the necessary get permissions to get all the resources it will need to inspect for a server-connected dry-run and returns errors detailing all the missing get permissions. This prevents a hung server-connected dry-run getting caught on individual missing get permissions. Signed-off-by: Tayler Geiger <[email protected]>
1 parent a46ff7d commit 65bc862

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

Diff for: cmd/operator-controller/main.go

+3
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,12 @@ func main() {
355355
crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()),
356356
}
357357

358+
acm := applier.NewAuthClientMapper(clientRestConfigMapper, mgr.GetConfig())
359+
358360
applier := &applier.Helm{
359361
ActionClientGetter: acg,
360362
Preflights: preflights,
363+
AuthClientMapper: acm,
361364
}
362365

363366
cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())

Diff for: go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ require (
3535
k8s.io/client-go v0.32.0
3636
k8s.io/component-base v0.32.0
3737
k8s.io/klog/v2 v2.130.1
38+
k8s.io/kubernetes v1.31.2
3839
k8s.io/utils v0.0.0-20241210054802-24370beab758
3940
sigs.k8s.io/controller-runtime v0.19.4
4041
sigs.k8s.io/yaml v1.4.0
@@ -243,6 +244,7 @@ require (
243244
gopkg.in/inf.v0 v0.9.1 // indirect
244245
gopkg.in/warnings.v0 v0.1.2 // indirect
245246
gopkg.in/yaml.v3 v3.0.1 // indirect
247+
k8s.io/component-helpers v0.32.0 // indirect
246248
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
247249
k8s.io/kubectl v0.32.0 // indirect
248250
oras.land/oras-go v1.2.5 // indirect

Diff for: go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -1006,12 +1006,16 @@ k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
10061006
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
10071007
k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU=
10081008
k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM=
1009+
k8s.io/component-helpers v0.32.0 h1:pQEEBmRt3pDJJX98cQvZshDgJFeKRM4YtYkMmfOlczw=
1010+
k8s.io/component-helpers v0.32.0/go.mod h1:9RuClQatbClcokXOcDWSzFKQm1huIf0FzQlPRpizlMc=
10091011
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
10101012
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
10111013
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
10121014
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
10131015
k8s.io/kubectl v0.32.0 h1:rpxl+ng9qeG79YA4Em9tLSfX0G8W0vfaiPVrc/WR7Xw=
10141016
k8s.io/kubectl v0.32.0/go.mod h1:qIjSX+QgPQUgdy8ps6eKsYNF+YmFOAO3WygfucIqFiE=
1017+
k8s.io/kubernetes v1.31.2 h1:VNSu4O7Xn5FFRsh9ePXyEPg6ucR21fOftarSdi053Gs=
1018+
k8s.io/kubernetes v1.31.2/go.mod h1:9xmT2buyTYj8TRKwRae7FcuY8k5+xlxv7VivvO0KKfs=
10151019
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
10161020
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
10171021
oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=

Diff for: internal/applier/helm.go

+132
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,24 @@ import (
1616
"helm.sh/helm/v3/pkg/release"
1717
"helm.sh/helm/v3/pkg/storage/driver"
1818
corev1 "k8s.io/api/core/v1"
19+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2021
apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
22+
"k8s.io/apiserver/pkg/authorization/authorizer"
23+
"k8s.io/client-go/rest"
2124
"sigs.k8s.io/controller-runtime/pkg/client"
2225
"sigs.k8s.io/controller-runtime/pkg/log"
2326

2427
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
28+
authv1 "k8s.io/api/authorization/v1"
29+
rbacv1 "k8s.io/api/rbac/v1"
30+
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
2531

2632
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2733
"github.com/operator-framework/operator-controller/internal/rukpak/convert"
2834
"github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety"
2935
"github.com/operator-framework/operator-controller/internal/rukpak/util"
36+
rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
3037
)
3138

3239
const (
@@ -52,9 +59,24 @@ type Preflight interface {
5259
Upgrade(context.Context, *release.Release) error
5360
}
5461

62+
type RestConfigMapper func(context.Context, client.Object, *rest.Config) (*rest.Config, error)
63+
64+
type AuthClientMapper struct {
65+
rcm RestConfigMapper
66+
baseCfg *rest.Config
67+
}
68+
5569
type Helm struct {
5670
ActionClientGetter helmclient.ActionClientGetter
5771
Preflights []Preflight
72+
AuthClientMapper AuthClientMapper
73+
}
74+
75+
func NewAuthClientMapper(rcm RestConfigMapper, baseCfg *rest.Config) AuthClientMapper {
76+
return AuthClientMapper{
77+
rcm: rcm,
78+
baseCfg: baseCfg,
79+
}
5880
}
5981

6082
// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
@@ -93,6 +115,21 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
93115
labels: objectLabels,
94116
}
95117

118+
authcfg, err := h.AuthClientMapper.rcm(ctx, ext, h.AuthClientMapper.baseCfg)
119+
if err != nil {
120+
return nil, "", err
121+
}
122+
123+
authclient, err := authorizationv1client.NewForConfig(authcfg)
124+
if err != nil {
125+
return nil, "", err
126+
}
127+
128+
err = h.checkGetPermissions(ctx, authclient, ac, ext, chrt, values, post)
129+
if err != nil {
130+
return nil, "", err
131+
}
132+
96133
rel, desiredRel, state, err := h.getReleaseState(ac, ext, chrt, values, post)
97134
if err != nil {
98135
return nil, "", err
@@ -151,8 +188,103 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
151188
return relObjects, state, nil
152189
}
153190

191+
func (h *Helm) checkGetPermissions(ctx context.Context, authcl *authorizationv1client.AuthorizationV1Client, cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) error {
192+
// client-only dry run
193+
clientDryRunRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
194+
i.DryRun = true
195+
i.DryRunOption = "client"
196+
return nil
197+
}, helmclient.AppendInstallPostRenderer(post))
198+
if err != nil {
199+
return err
200+
}
201+
objects, err := util.ManifestObjects(strings.NewReader(clientDryRunRelease.Manifest), fmt.Sprintf("%s-release-manifest", clientDryRunRelease.Name))
202+
203+
if err != nil {
204+
return err
205+
}
206+
207+
ssrr := &authv1.SelfSubjectRulesReview{
208+
Spec: authv1.SelfSubjectRulesReviewSpec{
209+
Namespace: ext.Spec.Namespace,
210+
},
211+
}
212+
213+
ssrr, err = authcl.SelfSubjectRulesReviews().Create(ctx, ssrr, v1.CreateOptions{})
214+
if err != nil {
215+
return err
216+
}
217+
218+
rules := []rbacv1.PolicyRule{}
219+
for _, rule := range ssrr.Status.ResourceRules {
220+
rules = append(rules, rbacv1.PolicyRule{
221+
Verbs: rule.Verbs,
222+
APIGroups: rule.APIGroups,
223+
Resources: rule.Resources,
224+
ResourceNames: rule.ResourceNames,
225+
})
226+
}
227+
228+
for _, rule := range ssrr.Status.NonResourceRules {
229+
rules = append(rules, rbacv1.PolicyRule{
230+
Verbs: rule.Verbs,
231+
NonResourceURLs: rule.NonResourceURLs,
232+
})
233+
}
234+
235+
resAttrs := []authorizer.AttributesRecord{}
236+
errs := []error{}
237+
238+
checked := make(map[string]bool)
239+
for _, o := range objects {
240+
if !checked[o.GetObjectKind().GroupVersionKind().String()] {
241+
checked[o.GetObjectKind().GroupVersionKind().String()] = true
242+
resAttrs = append(resAttrs, authorizer.AttributesRecord{
243+
Namespace: o.GetNamespace(),
244+
Verb: "get",
245+
APIGroup: o.GetObjectKind().GroupVersionKind().Group,
246+
APIVersion: o.GetObjectKind().GroupVersionKind().Version,
247+
Resource: o.GetObjectKind().GroupVersionKind().Kind,
248+
ResourceRequest: true,
249+
})
250+
}
251+
}
252+
253+
for _, resAttr := range resAttrs {
254+
if !rbacauthorizer.RulesAllow(resAttr, rules...) {
255+
errs = append(errs, fmt.Errorf("%s is not permitted to get %ss",
256+
ext.Spec.ServiceAccount.Name,
257+
resAttr.Resource))
258+
}
259+
}
260+
if len(errs) > 0 {
261+
errs = append([]error{fmt.Errorf("installer service account %s is missing required get permissions", ext.Spec.ServiceAccount.Name)}, errs...)
262+
}
263+
264+
return errors.Join(errs...)
265+
266+
// for _, o := range objects {
267+
// ssar := &authv1.SelfSubjectAccessReview{
268+
// Spec: authv1.SelfSubjectAccessReviewSpec{
269+
// ResourceAttributes: &authv1.ResourceAttributes{
270+
// Namespace: ext.Spec.Namespace,
271+
// Verb: "get",
272+
// Resource: o.GetObjectKind().GroupVersionKind().Kind,
273+
// Group: o.GetObjectKind().GroupVersionKind().Group,
274+
// },
275+
// },
276+
// }
277+
// ssar, err = authcl.SelfSubjectAccessReviews().Create(ctx, ssar, v1.CreateOptions{})
278+
// if err != nil {
279+
// return err
280+
// }
281+
// }
282+
283+
}
284+
154285
func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) {
155286
currentRelease, err := cl.Get(ext.GetName())
287+
156288
if errors.Is(err, driver.ErrReleaseNotFound) {
157289
desiredRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
158290
i.DryRun = true

0 commit comments

Comments
 (0)