Skip to content

Commit

Permalink
Add client-only helm dry-run to helm applier
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
trgeiger committed Feb 5, 2025
1 parent a46ff7d commit 65bc862
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,12 @@ func main() {
crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()),
}

acm := applier.NewAuthClientMapper(clientRestConfigMapper, mgr.GetConfig())

applier := &applier.Helm{
ActionClientGetter: acg,
Preflights: preflights,
AuthClientMapper: acm,
}

cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
k8s.io/client-go v0.32.0
k8s.io/component-base v0.32.0
k8s.io/klog/v2 v2.130.1
k8s.io/kubernetes v1.31.2
k8s.io/utils v0.0.0-20241210054802-24370beab758
sigs.k8s.io/controller-runtime v0.19.4
sigs.k8s.io/yaml v1.4.0
Expand Down Expand Up @@ -243,6 +244,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/component-helpers v0.32.0 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/kubectl v0.32.0 // indirect
oras.land/oras-go v1.2.5 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,16 @@ k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU=
k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM=
k8s.io/component-helpers v0.32.0 h1:pQEEBmRt3pDJJX98cQvZshDgJFeKRM4YtYkMmfOlczw=
k8s.io/component-helpers v0.32.0/go.mod h1:9RuClQatbClcokXOcDWSzFKQm1huIf0FzQlPRpizlMc=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.0 h1:rpxl+ng9qeG79YA4Em9tLSfX0G8W0vfaiPVrc/WR7Xw=
k8s.io/kubectl v0.32.0/go.mod h1:qIjSX+QgPQUgdy8ps6eKsYNF+YmFOAO3WygfucIqFiE=
k8s.io/kubernetes v1.31.2 h1:VNSu4O7Xn5FFRsh9ePXyEPg6ucR21fOftarSdi053Gs=
k8s.io/kubernetes v1.31.2/go.mod h1:9xmT2buyTYj8TRKwRae7FcuY8k5+xlxv7VivvO0KKfs=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
Expand Down
132 changes: 132 additions & 0 deletions internal/applier/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@ import (
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

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

ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/rukpak/convert"
"github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety"
"github.com/operator-framework/operator-controller/internal/rukpak/util"
rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
)

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

type RestConfigMapper func(context.Context, client.Object, *rest.Config) (*rest.Config, error)

type AuthClientMapper struct {
rcm RestConfigMapper
baseCfg *rest.Config
}

type Helm struct {
ActionClientGetter helmclient.ActionClientGetter
Preflights []Preflight
AuthClientMapper AuthClientMapper
}

func NewAuthClientMapper(rcm RestConfigMapper, baseCfg *rest.Config) AuthClientMapper {
return AuthClientMapper{
rcm: rcm,
baseCfg: baseCfg,
}
}

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

authcfg, err := h.AuthClientMapper.rcm(ctx, ext, h.AuthClientMapper.baseCfg)
if err != nil {
return nil, "", err
}

authclient, err := authorizationv1client.NewForConfig(authcfg)
if err != nil {
return nil, "", err
}

err = h.checkGetPermissions(ctx, authclient, ac, ext, chrt, values, post)
if err != nil {
return nil, "", err
}

rel, desiredRel, state, err := h.getReleaseState(ac, ext, chrt, values, post)
if err != nil {
return nil, "", err
Expand Down Expand Up @@ -151,8 +188,103 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
return relObjects, state, nil
}

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 {
// client-only dry run
clientDryRunRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
i.DryRun = true
i.DryRunOption = "client"
return nil
}, helmclient.AppendInstallPostRenderer(post))
if err != nil {
return err
}
objects, err := util.ManifestObjects(strings.NewReader(clientDryRunRelease.Manifest), fmt.Sprintf("%s-release-manifest", clientDryRunRelease.Name))

if err != nil {
return err
}

ssrr := &authv1.SelfSubjectRulesReview{
Spec: authv1.SelfSubjectRulesReviewSpec{
Namespace: ext.Spec.Namespace,
},
}

ssrr, err = authcl.SelfSubjectRulesReviews().Create(ctx, ssrr, v1.CreateOptions{})
if err != nil {
return err
}

rules := []rbacv1.PolicyRule{}
for _, rule := range ssrr.Status.ResourceRules {
rules = append(rules, rbacv1.PolicyRule{
Verbs: rule.Verbs,
APIGroups: rule.APIGroups,
Resources: rule.Resources,
ResourceNames: rule.ResourceNames,
})
}

for _, rule := range ssrr.Status.NonResourceRules {
rules = append(rules, rbacv1.PolicyRule{
Verbs: rule.Verbs,
NonResourceURLs: rule.NonResourceURLs,
})
}

resAttrs := []authorizer.AttributesRecord{}
errs := []error{}

checked := make(map[string]bool)
for _, o := range objects {
if !checked[o.GetObjectKind().GroupVersionKind().String()] {
checked[o.GetObjectKind().GroupVersionKind().String()] = true
resAttrs = append(resAttrs, authorizer.AttributesRecord{
Namespace: o.GetNamespace(),
Verb: "get",
APIGroup: o.GetObjectKind().GroupVersionKind().Group,
APIVersion: o.GetObjectKind().GroupVersionKind().Version,
Resource: o.GetObjectKind().GroupVersionKind().Kind,
ResourceRequest: true,
})
}
}

for _, resAttr := range resAttrs {
if !rbacauthorizer.RulesAllow(resAttr, rules...) {
errs = append(errs, fmt.Errorf("%s is not permitted to get %ss",
ext.Spec.ServiceAccount.Name,
resAttr.Resource))
}
}
if len(errs) > 0 {
errs = append([]error{fmt.Errorf("installer service account %s is missing required get permissions", ext.Spec.ServiceAccount.Name)}, errs...)
}

return errors.Join(errs...)

// for _, o := range objects {
// ssar := &authv1.SelfSubjectAccessReview{
// Spec: authv1.SelfSubjectAccessReviewSpec{
// ResourceAttributes: &authv1.ResourceAttributes{
// Namespace: ext.Spec.Namespace,
// Verb: "get",
// Resource: o.GetObjectKind().GroupVersionKind().Kind,
// Group: o.GetObjectKind().GroupVersionKind().Group,
// },
// },
// }
// ssar, err = authcl.SelfSubjectAccessReviews().Create(ctx, ssar, v1.CreateOptions{})
// if err != nil {
// return err
// }
// }

}

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) {
currentRelease, err := cl.Get(ext.GetName())

if errors.Is(err, driver.ErrReleaseNotFound) {
desiredRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
i.DryRun = true
Expand Down

0 comments on commit 65bc862

Please sign in to comment.