Skip to content

Commit f51f1ed

Browse files
committed
Add dryrun CLI flag to read cluster resources
ref: https://issues.redhat.com/browse/ACM-22932 Instead of providing input yaml files to the dryrun command args, set the --from-cluster flag to read cluster resources with the default kubeconfig. As before, the policies are set to Inform during evaluation with the fake runtime client, preventing modifications to the real cluster. Moved the input yaml creation code to a helper function. When --from-cluster enabled, point the reconciler's TargetK8sClient, TargetK8sDynamicClient, DynamicWatcher and SelectorReconciler to the real cluster. Signed-off-by: Janelle Law <[email protected]>
1 parent b76922e commit f51f1ed

File tree

2 files changed

+145
-70
lines changed

2 files changed

+145
-70
lines changed

pkg/dryrun/cmd.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type DryRunner struct {
2121
logPath string
2222
noColors bool
2323
fullDiffs bool
24+
fromCluster bool
2425
}
2526

2627
var ErrNonCompliant = errors.New("policy is NonCompliant")
@@ -105,6 +106,15 @@ func (d *DryRunner) GetCmd() *cobra.Command {
105106
"the DRYRUN_MAPPINGS_FILE environment variable.",
106107
)
107108

109+
cmd.Flags().BoolVar(
110+
&d.fromCluster,
111+
"from-cluster",
112+
false,
113+
"Read the current state of resources from a real Kubernetes cluster instead of "+
114+
"from input files. Uses the default kubeconfig or KUBECONFIG environment variable. "+
115+
"Any input files representing the cluster state will be ignored.",
116+
)
117+
108118
cmd.AddCommand(&cobra.Command{
109119
Use: "generate",
110120
Short: "Generate an API Mappings file",

pkg/dryrun/dryrun.go

Lines changed: 135 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import (
3030
"k8s.io/apimachinery/pkg/types"
3131
"k8s.io/client-go/dynamic"
3232
dynfake "k8s.io/client-go/dynamic/fake"
33+
"k8s.io/client-go/kubernetes"
3334
clientsetfake "k8s.io/client-go/kubernetes/fake"
3435
"k8s.io/client-go/kubernetes/scheme"
36+
"k8s.io/client-go/tools/clientcmd"
3537
"k8s.io/client-go/tools/record"
3638
klog "k8s.io/klog/v2"
3739
parentpolicyv1 "open-cluster-management.io/governance-policy-propagator/api/v1"
@@ -57,11 +59,6 @@ func (d *DryRunner) dryRun(cmd *cobra.Command, args []string) error {
5759
return fmt.Errorf("unable to read input policy: %w", err)
5860
}
5961

60-
inputObjects, err := d.readInputResources(cmd, args)
61-
if err != nil {
62-
return fmt.Errorf("unable to read input resources: %w", err)
63-
}
64-
6562
if err := d.setupLogs(); err != nil {
6663
return fmt.Errorf("unable to setup the logging configuration: %w", err)
6764
}
@@ -74,43 +71,15 @@ func (d *DryRunner) dryRun(cmd *cobra.Command, args []string) error {
7471
return fmt.Errorf("unable to setup the dryrun reconciler: %w", err)
7572
}
7673

77-
// Apply the user's resources to the fake cluster
78-
for _, obj := range inputObjects {
79-
gvk := obj.GroupVersionKind()
80-
81-
scopedGVR, err := rec.DynamicWatcher.GVKToGVR(gvk)
74+
if !d.fromCluster {
75+
inputObjects, err := d.readInputResources(cmd, args)
8276
if err != nil {
83-
if errors.Is(err, depclient.ErrNoVersionedResource) {
84-
return fmt.Errorf("%w for kind %v: if this is a custom resource, it may need an "+
85-
"entry in the mappings file", err, gvk.Kind)
86-
}
87-
88-
return fmt.Errorf("unable to apply an input resource: %w", err)
77+
return fmt.Errorf("unable to read input resources: %w", err)
8978
}
9079

91-
var resInt dynamic.ResourceInterface
92-
93-
if scopedGVR.Namespaced {
94-
if obj.GetNamespace() == "" {
95-
obj.SetNamespace("default")
96-
}
97-
98-
resInt = rec.TargetK8sDynamicClient.Resource(scopedGVR.GroupVersionResource).Namespace(obj.GetNamespace())
99-
} else {
100-
resInt = rec.TargetK8sDynamicClient.Resource(scopedGVR.GroupVersionResource)
101-
}
102-
103-
sanitizeForCreation(obj)
104-
105-
if _, err := resInt.Create(ctx, obj, metav1.CreateOptions{}); err != nil &&
106-
!k8serrors.IsAlreadyExists(err) {
107-
return fmt.Errorf("unable to apply an input resource: %w", err)
108-
}
109-
110-
// Manually convert resources from the dynamic client to the runtime client
111-
err = rec.Client.Create(ctx, obj)
112-
if err != nil && !k8serrors.IsAlreadyExists(err) {
113-
return err
80+
err = d.applyInputResources(ctx, rec, inputObjects)
81+
if err != nil {
82+
return fmt.Errorf("unable to apply input resources: %w", err)
11483
}
11584
}
11685

@@ -259,9 +228,59 @@ func (d *DryRunner) readPolicy(cmd *cobra.Command) (*policyv1.ConfigurationPolic
259228
Name: parentName,
260229
}}
261230

231+
if cfgpol.GetNamespace() == "" {
232+
cfgpol.SetNamespace("default")
233+
}
234+
262235
return &cfgpol, nil
263236
}
264237

238+
func (d *DryRunner) applyInputResources(
239+
ctx context.Context, rec *ctrl.ConfigurationPolicyReconciler, inputObjects []*unstructured.Unstructured,
240+
) error {
241+
// Apply the user's resources to the fake cluster
242+
for _, obj := range inputObjects {
243+
gvk := obj.GroupVersionKind()
244+
245+
scopedGVR, err := rec.DynamicWatcher.GVKToGVR(gvk)
246+
if err != nil {
247+
if errors.Is(err, depclient.ErrNoVersionedResource) {
248+
return fmt.Errorf("%w for kind %v: if this is a custom resource, it may need an "+
249+
"entry in the mappings file", err, gvk.Kind)
250+
}
251+
252+
return fmt.Errorf("unable to apply an input resource: %w", err)
253+
}
254+
255+
var resInt dynamic.ResourceInterface
256+
257+
if scopedGVR.Namespaced {
258+
if obj.GetNamespace() == "" {
259+
obj.SetNamespace("default")
260+
}
261+
262+
resInt = rec.TargetK8sDynamicClient.Resource(scopedGVR.GroupVersionResource).Namespace(obj.GetNamespace())
263+
} else {
264+
resInt = rec.TargetK8sDynamicClient.Resource(scopedGVR.GroupVersionResource)
265+
}
266+
267+
sanitizeForCreation(obj)
268+
269+
if _, err := resInt.Create(ctx, obj, metav1.CreateOptions{}); err != nil &&
270+
!k8serrors.IsAlreadyExists(err) {
271+
return fmt.Errorf("unable to apply an input resource: %w", err)
272+
}
273+
274+
// Manually convert resources from the dynamic client to the runtime client
275+
err = rec.Client.Create(ctx, obj)
276+
if err != nil && !k8serrors.IsAlreadyExists(err) {
277+
return err
278+
}
279+
}
280+
281+
return nil
282+
}
283+
265284
// readInputResources takes stdin and any paths given as "positional" arguments,
266285
// and decodes them from YAML into k8s resources. Directories can be passed in
267286
// arguments: in this case all files in that directory will be read and decoded
@@ -417,11 +436,60 @@ func (d *DryRunner) setupReconciler(
417436
return nil, err
418437
}
419438

420-
dynamicClient := dynfake.NewSimpleDynamicClient(scheme.Scheme)
421-
clientset := clientsetfake.NewSimpleClientset()
439+
runtimeClient := clientfake.NewClientBuilder().
440+
WithScheme(scheme.Scheme).
441+
WithObjects(configPolCRD, cfgPolicy).
442+
WithStatusSubresource(cfgPolicy).
443+
Build()
444+
445+
nsSelUpdatesChan := make(chan event.GenericEvent, 20)
446+
422447
watcherReconciler, _ := depclient.NewControllerRuntimeSource()
423448

424-
dynamicWatcher := depclient.NewWithClients(
449+
var dynamicWatcher depclient.DynamicWatcher
450+
var clientset kubernetes.Interface
451+
var dynamicClient dynamic.Interface
452+
var nsSelReconciler common.NamespaceSelectorReconciler
453+
454+
if d.fromCluster {
455+
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
456+
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
457+
loadingRules, &clientcmd.ConfigOverrides{},
458+
)
459+
460+
kubeConfig, err := clientConfig.ClientConfig()
461+
if err != nil {
462+
return nil, fmt.Errorf("unable to locate the default kubeconfig: %w", err)
463+
}
464+
465+
clientset, err = kubernetes.NewForConfig(kubeConfig)
466+
if err != nil {
467+
return nil, fmt.Errorf("failed to create kubernetes clientset for cluster: %w", err)
468+
}
469+
470+
dynamicClient, err = dynamic.NewForConfig(kubeConfig)
471+
if err != nil {
472+
return nil, fmt.Errorf("failed to create dynamic client for cluster: %w", err)
473+
}
474+
475+
readOnlyMode := true // Prevent modifications to the cluster
476+
477+
realRuntimeClient, err := client.New(kubeConfig, client.Options{
478+
Scheme: scheme.Scheme,
479+
DryRun: &readOnlyMode,
480+
})
481+
if err != nil {
482+
return nil, fmt.Errorf("failed to create runtime client for ns selector reconciler: %w", err)
483+
}
484+
485+
nsSelReconciler = common.NewNamespaceSelectorReconciler(realRuntimeClient, nsSelUpdatesChan)
486+
} else {
487+
dynamicClient = dynfake.NewSimpleDynamicClient(scheme.Scheme)
488+
clientset = clientsetfake.NewSimpleClientset()
489+
nsSelReconciler = common.NewNamespaceSelectorReconciler(runtimeClient, nsSelUpdatesChan)
490+
}
491+
492+
dynamicWatcher = depclient.NewWithClients(
425493
dynamicClient,
426494
clientset.Discovery(),
427495
watcherReconciler,
@@ -435,14 +503,28 @@ func (d *DryRunner) setupReconciler(
435503
}
436504
}()
437505

438-
runtimeClient := clientfake.NewClientBuilder().
439-
WithScheme(scheme.Scheme).
440-
WithObjects(configPolCRD, cfgPolicy).
441-
WithStatusSubresource(cfgPolicy).
442-
Build()
506+
rec := ctrl.ConfigurationPolicyReconciler{
507+
Client: runtimeClient,
508+
DecryptionConcurrency: 1,
509+
DynamicWatcher: dynamicWatcher,
510+
Scheme: scheme.Scheme,
511+
Recorder: record.NewFakeRecorder(8),
512+
InstanceName: "policy-cli",
513+
TargetK8sClient: clientset,
514+
TargetK8sDynamicClient: dynamicClient,
515+
SelectorReconciler: &nsSelReconciler,
516+
EnableMetrics: false,
517+
UninstallMode: false,
518+
EvalBackoffSeconds: 5,
519+
FullDiffs: d.fullDiffs,
520+
}
443521

444-
nsSelUpdatesChan := make(chan event.GenericEvent, 20)
445-
nsSelReconciler := common.NewNamespaceSelectorReconciler(runtimeClient, nsSelUpdatesChan)
522+
// wait for dynamic watcher to have started
523+
<-rec.DynamicWatcher.Started()
524+
525+
if d.fromCluster {
526+
return &rec, nil
527+
}
446528

447529
defaultNs := &unstructured.Unstructured{
448530
Object: map[string]interface{}{
@@ -467,21 +549,7 @@ func (d *DryRunner) setupReconciler(
467549
return nil, err
468550
}
469551

470-
rec := ctrl.ConfigurationPolicyReconciler{
471-
Client: runtimeClient,
472-
DecryptionConcurrency: 1,
473-
DynamicWatcher: dynamicWatcher,
474-
Scheme: scheme.Scheme,
475-
Recorder: record.NewFakeRecorder(8),
476-
InstanceName: "policy-cli",
477-
TargetK8sClient: clientset,
478-
TargetK8sDynamicClient: dynamicClient,
479-
SelectorReconciler: &nsSelReconciler,
480-
EnableMetrics: false,
481-
UninstallMode: false,
482-
EvalBackoffSeconds: 5,
483-
FullDiffs: d.fullDiffs,
484-
}
552+
fakeClientset := clientset.(*clientsetfake.Clientset)
485553

486554
if d.mappingsPath != "" {
487555
mFile, err := os.ReadFile(d.mappingsPath)
@@ -494,19 +562,16 @@ func (d *DryRunner) setupReconciler(
494562
return nil, err
495563
}
496564

497-
clientset.Resources = mappings.ResourceLists(apiMappings)
565+
fakeClientset.Resources = mappings.ResourceLists(apiMappings)
498566
} else {
499-
clientset.Resources, err = mappings.DefaultResourceLists()
567+
fakeClientset.Resources, err = mappings.DefaultResourceLists()
500568
if err != nil {
501569
return nil, err
502570
}
503571
}
504572

505573
// Add open-cluster-management policy CRD
506-
addSupportedResources(clientset)
507-
508-
// wait for dynamic watcher to have started
509-
<-rec.DynamicWatcher.Started()
574+
addSupportedResources(fakeClientset)
510575

511576
return &rec, nil
512577
}

0 commit comments

Comments
 (0)