Skip to content

Commit

Permalink
Allow templates to specific if that they can allow-undefined-extras
Browse files Browse the repository at this point in the history
This can be done by addign the comment
\# cluster-compare: allow-undefined-extras=true
to the file. Also define a template helper function called "merge" which
does this for a block, as well as another called "onlyMatch" which does
the inverse merge to allow blocks to only match when using the template
wide merging.
  • Loading branch information
nocturnalastro committed Jun 24, 2024
1 parent 8817fc8 commit 33c56d2
Show file tree
Hide file tree
Showing 49 changed files with 1,067 additions and 38 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ linters-settings:
- .Errorf(
- errors.New(
- errors.Unwrap(
- errors.Join(
- .Wrap(
- .Wrapf(
- .WithMessage(
Expand Down
129 changes: 128 additions & 1 deletion docs/reference-config-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Thus, the file `metadata.yaml` includes an array denoted by `Parts` of one or mo
```yaml
# Every part denotes typically denotes a workload or set of workloads
Parts:
- name: ExamplePart1
- name: ExamplePart1
Components:
- name: ExampleComponent1
---- here goes ExampleComponent1 configuration ----
Expand Down Expand Up @@ -105,3 +105,130 @@ spec:
tier: frontend
{{- end }}
```

# Per-template configuration

## Pre-merging

If you don't want to check live-manifest exactly matches your template you can enable merging.
This will do a strategic merge of the template into the manifest which will remove features not mentioned in the template from the diff.
This can be useful when dealing with annotation or labels which may be of no consiquence to your check.
Note that this could cover up differences in things you do care about so use it with care.
This can be configured for a manifest by adding
`# cluster-compare: allow-undefined-extras=true` in your template

example when comparing the template:

```yaml
# cluster-compare: allow-undefined-extras=true
apiVersion: v1
kind: Namespace
metadata:
name: openshift-storage
annotations:
workload.openshift.io/allowed: management
labels:
openshift.io/cluster-monitoring: "true"
```
to the manifest:
```yaml
apiVersion: v1
kind: Namespace
metadata:
annotations:
openshift.io/sa.scc.mcs: s0:c29,c14
openshift.io/sa.scc.supplemental-groups: 1000840000/10000
openshift.io/sa.scc.uid-range: 1000840000/10000
reclaimspace.csiaddons.openshift.io/schedule: '@weekly'
creationTimestamp: "2024-06-07T17:40:07Z"
labels:
kubernetes.io/metadata.name: openshift-storage
olm.operatorgroup.uid/ffcf3f2d-3e37-4772-97bc-983cdfce128b: ""
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/audit-version: v1.24
pod-security.kubernetes.io/warn: privileged
pod-security.kubernetes.io/warn-version: v1.24
security.openshift.io/scc.podSecurityLabelSync: "true"
name: openshift-storage
resourceVersion: "13323419"
uid: 507a5a4e-4fca-4dc3-b246-36359cbe07bf
spec:
finalizers:
- kubernetes
status:
phase: Active
```
you will get a diff of:
```shell
**********************************

Cluster CR: v1_Namespace_openshift-storage
Reference File: namespace.yaml
Diff Output: diff -u -N TEMP/v1_namespace_openshift-storage TEMP/v1_namespace_openshift-storage
--- TEMP/v1_namespace_openshift-storage DATE
+++ TEMP/v1_namespace_openshift-storage DATE
@@ -6,11 +6,9 @@
openshift.io/sa.scc.supplemental-groups: 1000840000/10000
openshift.io/sa.scc.uid-range: 1000840000/10000
reclaimspace.csiaddons.openshift.io/schedule: '@weekly'
- workload.openshift.io/allowed: management
labels:
kubernetes.io/metadata.name: openshift-storage
olm.operatorgroup.uid/ffcf3f2d-3e37-4772-97bc-983cdfce128b: ""
- openshift.io/cluster-monitoring: "true"
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/audit-version: v1.24
pod-security.kubernetes.io/warn: privileged

**********************************

Summary
CRs with diffs: 1
No CRs are missing from the cluster
No CRs are unmatched to reference CRs
```

instead of

```shell
**********************************

Cluster CR: v1_Namespace_openshift-storage
Reference File: namespace.yaml
Diff Output: diff -u -N TEMP/v1_namespace_openshift-storage TEMP/v1_namespace_openshift-storage
--- TEMP/v1_namespace_openshift-storage DATE
+++ TEMP/v1_namespace_openshift-storage DATE
@@ -2,7 +2,19 @@
kind: Namespace
metadata:
annotations:
- workload.openshift.io/allowed: management
+ openshift.io/sa.scc.mcs: s0:c29,c14
+ openshift.io/sa.scc.supplemental-groups: 1000840000/10000
+ openshift.io/sa.scc.uid-range: 1000840000/10000
+ reclaimspace.csiaddons.openshift.io/schedule: '@weekly'
labels:
- openshift.io/cluster-monitoring: "true"
+ kubernetes.io/metadata.name: openshift-storage
+ olm.operatorgroup.uid/ffcf3f2d-3e37-4772-97bc-983cdfce128b: ""
+ pod-security.kubernetes.io/audit: privileged
+ pod-security.kubernetes.io/audit-version: v1.24
+ pod-security.kubernetes.io/warn: privileged
+ pod-security.kubernetes.io/warn-version: v1.24
+ security.openshift.io/scc.podSecurityLabelSync: "true"
name: openshift-storage
+spec:
+ finalizers:
+ - kubernetes

**********************************

Summary
CRs with diffs: 1
No CRs are missing from the cluster
No CRs are unmatched to reference CRs
```
65 changes: 56 additions & 9 deletions pkg/compare/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"text/template"

jsonpatch "github.com/evanphx/json-patch"
"github.com/gosimple/slug"
"github.com/openshift/kube-compare/pkg/groups"
"github.com/samber/lo"
Expand Down Expand Up @@ -114,10 +115,11 @@ type Options struct {
diffAll bool
ShowManagedFields bool
OutputFormat string
showMore bool

builder *resource.Builder
correlator *MetricsCorrelatorDecorator
templates []*template.Template
templates []*ReferenceTemplate
local bool
types []string
ref Reference
Expand Down Expand Up @@ -171,6 +173,7 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
cmd.Flags().BoolVarP(&options.diffAll, "all-resources", "A", options.diffAll,
"If present, In live mode will try to match all resources that are from the types mentioned in the reference. "+
"In local mode will try to match all resources passed to the command")
cmd.Flags().BoolVarP(&options.showMore, "show-more", "v", options.showMore, "Will match reference exactly")

cmd.Flags().StringVarP(&options.OutputFormat, "output", "o", "", fmt.Sprintf(`Output format. One of: (%s)`, strings.Join(OutputFormats, ", ")))
kcmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
Expand Down Expand Up @@ -242,7 +245,7 @@ func (o *Options) Complete(f kcmdutil.Factory, cmd *cobra.Command, args []string
return err
}
}
o.templates, err = parseTemplates(o.ref.getTemplates(), o.ref.TemplateFunctionFiles, fs)
o.templates, err = parseTemplates(o.ref.getTemplates(), o.ref.TemplateFunctionFiles, fs, o)
if err != nil {
return err
}
Expand Down Expand Up @@ -371,7 +374,7 @@ func getSupportedResourceTypes(client discovery.CachedDiscoveryInterface) (map[s

// findAllRequestedSupportedTypes divides the requested types in to two groups: supported types and unsupported types based on if they are specified as supported.
// The list of supported types will include the types in the form of {kind}.{group}.
func findAllRequestedSupportedTypes(supportedTypesWithGroups map[string][]string, requestedTypes map[string][]*template.Template) ([]string, []string) {
func findAllRequestedSupportedTypes(supportedTypesWithGroups map[string][]string, requestedTypes map[string][]*ReferenceTemplate) ([]string, []string) {
var typesIncludingGroup []string
var notSupportedTypes []string
for kind := range requestedTypes {
Expand Down Expand Up @@ -438,22 +441,31 @@ func (o *Options) Run() error {

err := r.Visit(func(info *resource.Info, _ error) error { // ignoring previous errors
clusterCRMapping, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object)
clusterCR := unstructured.Unstructured{Object: clusterCRMapping}
clusterCR := &unstructured.Unstructured{Object: clusterCRMapping}

temp, err := o.correlator.Match(&clusterCR)
temp, err := o.correlator.Match(clusterCR)
if err != nil {
return err
}

localRef, err := executeYAMLTemplate(temp, clusterCR.Object)
localRef, err := temp.Exec(clusterCR.Object)
if err != nil {
return err
}




allowMerge := temp.allowMerge
if o.showMore {
allowMerge = false
}

obj := InfoObject{
injectedObjFromTemplate: localRef,
clusterObj: &clusterCR,
clusterObj: clusterCR,
FieldsToOmit: o.ref.FieldsToOmit,
allowMerge: allowMerge,
}
diffOutput, err := runDiff(obj, o.IOStreams, o.ShowManagedFields)
if err != nil {
Expand All @@ -462,7 +474,8 @@ func (o *Options) Run() error {
if diffOutput.Len() > 0 {
numDiffCRs += 1
}
diffs = append(diffs, DiffSum{DiffOutput: diffOutput.String(), CorrelatedTemplate: temp.Name(), CRName: apiKindNamespaceName(&clusterCR)})

diffs = append(diffs, DiffSum{DiffOutput: diffOutput.String(), CorrelatedTemplate: temp.Name(), CRName: apiKindNamespaceName(clusterCR)})
return err
})
if err != nil {
Expand All @@ -488,6 +501,7 @@ type InfoObject struct {
injectedObjFromTemplate *unstructured.Unstructured
clusterObj *unstructured.Unstructured
FieldsToOmit [][]string
allowMerge bool
}

// Live Returns the cluster version of the object
Expand All @@ -498,8 +512,15 @@ func (obj InfoObject) Live() runtime.Object {

// Merged Returns the Injected Reference Version of the Resource
func (obj InfoObject) Merged() (runtime.Object, error) {
var err error
if obj.allowMerge {
obj.injectedObjFromTemplate, err = mergeManifests(obj.injectedObjFromTemplate, obj.clusterObj)
if err != nil {
klog.Errorf("failed to properly merge the manifests for %s some diff may be incorrect: %s", apiKindNamespaceName(obj.clusterObj), err)
}
}
omitFields(obj.injectedObjFromTemplate.Object, obj.FieldsToOmit)
return obj.injectedObjFromTemplate, nil
return obj.injectedObjFromTemplate, err
}

func omitFields(object map[string]any, fields [][]string) {
Expand All @@ -514,6 +535,32 @@ func omitFields(object map[string]any, fields [][]string) {
}
}

// mergeManifests will return an attempt to update the localRef with the clusterCR. In the case of an error it will return an unmodified localRef.
func mergeManifests(localRef, clusterCR *unstructured.Unstructured) (updateLocalRef *unstructured.Unstructured, err error) {
localRefData, err := json.Marshal(localRef)
if err != nil {
return localRef, fmt.Errorf("failed to marshal reference CR: %w", err)
}

clusterCRData, err := json.Marshal(clusterCR.Object)
if err != nil {
return localRef, fmt.Errorf("failed to marshal cluster CR: %w", err)
}

localRefUpdatedData, err := jsonpatch.MergePatch(clusterCRData, localRefData)
if err != nil {
return localRef, fmt.Errorf("failed to merge cluster and reference CRs: %w", err)
}

localRefUpdatedObj := make(map[string]any)
err = json.Unmarshal(localRefUpdatedData, &localRefUpdatedObj)
if err != nil {
return localRef, fmt.Errorf("failed to unmarshal updated manifest: %w", err)
}

return &unstructured.Unstructured{Object: localRefUpdatedObj}, nil
}

func (obj InfoObject) Name() string {
return slug.Make(apiKindNamespaceName(obj.clusterObj))
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/compare/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type Test struct {
shouldDiffAll bool
outputFormat string
checks Checks
showMore bool
}

func (test *Test) getTestDir() string {
Expand Down Expand Up @@ -329,6 +330,47 @@ error code:2`),
outputFormat: Json,
checks: defaultChecks,
},
{
name: "Allow Undefined Extras",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Check No Overwrite",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Check Probes Merge",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Only Match Partial",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Only Match Full",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Check Probes Only Match",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
{
name: "Show More Flag",
mode: []Mode{DefaultMode},
showMore: true,
checks: defaultChecks,
},
{
name: "Allow Undefined Extras Via Merge Helper",
mode: []Mode{DefaultMode},
checks: defaultChecks,
},
}
tf := cmdtesting.NewTestFactory()
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
Expand Down Expand Up @@ -409,6 +451,7 @@ func getCommand(t *testing.T, test *Test, modeIndex int, tf *cmdtesting.TestFact
require.NoError(t, cmd.Flags().Set("reference", path.Join(test.getTestDir(), TestRefDirName)))
}
}
require.NoError(t, cmd.Flags().Set("show-more", fmt.Sprintf("%v", test.showMore)))
return cmd
}

Expand Down
Loading

0 comments on commit 33c56d2

Please sign in to comment.