7
7
"fmt"
8
8
"io"
9
9
"io/fs"
10
+ "slices"
10
11
"strings"
11
12
12
13
"helm.sh/helm/v3/pkg/action"
@@ -16,14 +17,17 @@ import (
16
17
"helm.sh/helm/v3/pkg/release"
17
18
"helm.sh/helm/v3/pkg/storage/driver"
18
19
corev1 "k8s.io/api/core/v1"
20
+ rbacv1 "k8s.io/api/rbac/v1"
19
21
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20
22
apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
23
+ "k8s.io/apiserver/pkg/authentication/user"
21
24
"sigs.k8s.io/controller-runtime/pkg/client"
22
25
"sigs.k8s.io/controller-runtime/pkg/log"
23
26
24
27
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
25
28
26
29
ocv1 "github.com/operator-framework/operator-controller/api/v1"
30
+ "github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
27
31
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
28
32
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
29
33
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
@@ -56,6 +60,7 @@ type Preflight interface {
56
60
type Helm struct {
57
61
ActionClientGetter helmclient.ActionClientGetter
58
62
Preflights []Preflight
63
+ PreAuthorizer authorization.PreAuthorizer
59
64
}
60
65
61
66
// 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
85
90
}
86
91
values := chartutil.Values {}
87
92
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
+
88
125
ac , err := h .ActionClientGetter .ActionClientFor (ctx , ext )
89
126
if err != nil {
90
127
return nil , "" , err
91
128
}
92
129
93
- post := & postrenderer {
94
- labels : objectLabels ,
95
- }
96
-
97
130
rel , desiredRel , state , err := h .getReleaseState (ac , ext , chrt , values , post )
98
131
if err != nil {
99
- return nil , "" , err
132
+ return nil , "" , fmt . Errorf ( "failed to get release state using server-side dry-run: %w" , err )
100
133
}
101
134
102
135
for _ , preflight := range h .Preflights {
@@ -152,6 +185,34 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
152
185
return relObjects , state , nil
153
186
}
154
187
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
+
155
216
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 ) {
156
217
currentRelease , err := cl .Get (ext .GetName ())
157
218
if errors .Is (err , driver .ErrReleaseNotFound ) {
@@ -161,10 +222,6 @@ func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterE
161
222
return nil
162
223
}, helmclient .AppendInstallPostRenderer (post ))
163
224
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
- }
168
225
return nil , nil , StateError , err
169
226
}
170
227
return nil , desiredRelease , StateNeedsInstall , nil
@@ -220,3 +277,25 @@ func (p *postrenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, erro
220
277
}
221
278
return & buf , nil
222
279
}
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