77 "fmt"
88 "io"
99 "io/fs"
10+ "slices"
1011 "strings"
1112
1213 "helm.sh/helm/v3/pkg/action"
@@ -16,14 +17,17 @@ import (
1617 "helm.sh/helm/v3/pkg/release"
1718 "helm.sh/helm/v3/pkg/storage/driver"
1819 corev1 "k8s.io/api/core/v1"
20+ rbacv1 "k8s.io/api/rbac/v1"
1921 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2022 apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
23+ "k8s.io/apiserver/pkg/authentication/user"
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"
2528
2629 ocv1 "github.com/operator-framework/operator-controller/api/v1"
30+ "github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
2731 "github.com/operator-framework/operator-controller/internal/operator-controller/features"
2832 "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
2933 "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
@@ -56,6 +60,7 @@ type Preflight interface {
5660type Helm struct {
5761 ActionClientGetter helmclient.ActionClientGetter
5862 Preflights []Preflight
63+ PreAuthorizer authorization.PreAuthorizer
5964}
6065
6166// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
@@ -85,18 +90,46 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
8590 }
8691 values := chartutil.Values {}
8792
93+ post := & postrenderer {
94+ labels : objectLabels ,
95+ }
96+
97+ if features .OperatorControllerFeatureGate .Enabled (features .PreflightPermissions ) {
98+ tmplRel , err := h .template (ctx , ext , chrt , values , post )
99+ if err != nil {
100+ return nil , "" , fmt .Errorf ("failed to get release state using client-only dry-run: %w" , err )
101+ }
102+
103+ ceServiceAccount := user.DefaultInfo {Name : fmt .Sprintf ("system:serviceaccount:%s:%s" , ext .Spec .Namespace , ext .Spec .ServiceAccount .Name )}
104+ missingRules , err := h .PreAuthorizer .PreAuthorize (ctx , & ceServiceAccount , strings .NewReader (tmplRel .Manifest ))
105+
106+ var preAuthErrors []error
107+ if len (missingRules ) > 0 {
108+ var missingRuleDescriptions []string
109+ for ns , policyRules := range missingRules {
110+ for _ , rule := range policyRules {
111+ missingRuleDescriptions = append (missingRuleDescriptions , ruleDescription (ns , rule ))
112+ }
113+ }
114+ slices .Sort (missingRuleDescriptions )
115+ preAuthErrors = append (preAuthErrors , fmt .Errorf ("service account lacks permission to manage cluster extension:\n %s" , strings .Join (missingRuleDescriptions , "\n " )))
116+ }
117+ if err != nil {
118+ preAuthErrors = append (preAuthErrors , fmt .Errorf ("authorization evaluation error: %w" , err ))
119+ }
120+ if len (preAuthErrors ) > 0 {
121+ return nil , "" , fmt .Errorf ("pre-authorization failed: %v" , preAuthErrors )
122+ }
123+ }
124+
88125 ac , err := h .ActionClientGetter .ActionClientFor (ctx , ext )
89126 if err != nil {
90127 return nil , "" , err
91128 }
92129
93- post := & postrenderer {
94- labels : objectLabels ,
95- }
96-
97130 rel , desiredRel , state , err := h .getReleaseState (ac , ext , chrt , values , post )
98131 if err != nil {
99- return nil , "" , err
132+ return nil , "" , fmt . Errorf ( "failed to get release state using server-side dry-run: %w" , err )
100133 }
101134
102135 for _ , preflight := range h .Preflights {
@@ -152,6 +185,34 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
152185 return relObjects , state , nil
153186}
154187
188+ func (h * Helm ) template (ctx context.Context , ext * ocv1.ClusterExtension , chrt * chart.Chart , values chartutil.Values , post postrender.PostRenderer ) (* release.Release , error ) {
189+ // We need to get a separate action client because our template call below
190+ // permanently modifies the underlying action.Configuration for ClientOnly mode.
191+ ac , err := h .ActionClientGetter .ActionClientFor (ctx , ext )
192+ if err != nil {
193+ return nil , err
194+ }
195+
196+ isUpgrade := false
197+ currentRelease , err := ac .Get (ext .GetName ())
198+ if err != nil && ! errors .Is (err , driver .ErrReleaseNotFound ) {
199+ return nil , err
200+ }
201+ if currentRelease != nil {
202+ isUpgrade = true
203+ }
204+
205+ return ac .Install (ext .GetName (), ext .Spec .Namespace , chrt , values , func (i * action.Install ) error {
206+ i .DryRun = true
207+ i .ReleaseName = ext .GetName ()
208+ i .Replace = true
209+ i .ClientOnly = true
210+ i .IncludeCRDs = true
211+ i .IsUpgrade = isUpgrade
212+ return nil
213+ }, helmclient .AppendInstallPostRenderer (post ))
214+ }
215+
155216func (h * Helm ) getReleaseState (cl helmclient.ActionInterface , ext * ocv1.ClusterExtension , chrt * chart.Chart , values chartutil.Values , post postrender.PostRenderer ) (* release.Release , * release.Release , string , error ) {
156217 currentRelease , err := cl .Get (ext .GetName ())
157218 if errors .Is (err , driver .ErrReleaseNotFound ) {
@@ -161,10 +222,6 @@ func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterE
161222 return nil
162223 }, helmclient .AppendInstallPostRenderer (post ))
163224 if err != nil {
164- if features .OperatorControllerFeatureGate .Enabled (features .PreflightPermissions ) {
165- _ = struct {}{} // minimal no-op to satisfy linter
166- // probably need to break out this error as it's the one for helm dry-run as opposed to any returned later
167- }
168225 return nil , nil , StateError , err
169226 }
170227 return nil , desiredRelease , StateNeedsInstall , nil
@@ -220,3 +277,25 @@ func (p *postrenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, erro
220277 }
221278 return & buf , nil
222279}
280+
281+ func ruleDescription (ns string , rule rbacv1.PolicyRule ) string {
282+ var sb strings.Builder
283+ sb .WriteString (fmt .Sprintf ("Namespace:%q" , ns ))
284+
285+ if len (rule .APIGroups ) > 0 {
286+ sb .WriteString (fmt .Sprintf (" APIGroups:[%s]" , strings .Join (rule .APIGroups , "," )))
287+ }
288+ if len (rule .Resources ) > 0 {
289+ sb .WriteString (fmt .Sprintf (" Resources:[%s]" , strings .Join (rule .Resources , "," )))
290+ }
291+ if len (rule .ResourceNames ) > 0 {
292+ sb .WriteString (fmt .Sprintf (" ResourceNames:[%s]" , strings .Join (rule .ResourceNames , "," )))
293+ }
294+ if len (rule .Verbs ) > 0 {
295+ sb .WriteString (fmt .Sprintf (" Verbs:[%s]" , strings .Join (rule .Verbs , "," )))
296+ }
297+ if len (rule .NonResourceURLs ) > 0 {
298+ sb .WriteString (fmt .Sprintf (" NonResourceURLs:[%s]" , strings .Join (rule .NonResourceURLs , "," )))
299+ }
300+ return sb .String ()
301+ }
0 commit comments