Skip to content

Commit

Permalink
Add option to format output as JSON/YAML
Browse files Browse the repository at this point in the history
A new flag '-o' ('output') is introduced, allowing the user to choose the output format of the command. The options are json/yaml. The default behavior remains the same. The flag supports auto-completion for the output format options, implemented in the code of the command. To enable auto-completion when using the command as a plugin, an additional file 'kubectl_complete-cluster_compare' was added. The file should be present in a location in PATH along with the plugin to enable auto-completion. Also added supporting tests for JSON and YAML output modes.

Signed-off-by: Alina Sudakov <asudakov@redhat.com>
  • Loading branch information
AlinaSecret committed May 28, 2024
1 parent 1d8b12f commit 433d21c
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 12 deletions.
3 changes: 3 additions & 0 deletions kubectl_complete-cluster_compare
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

kubectl cluster-compare __complete "$@"
69 changes: 58 additions & 11 deletions pkg/compare/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ package compare

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
Expand All @@ -27,6 +29,7 @@ import (
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/exec"
"sigs.k8s.io/yaml"
)

var (
Expand Down Expand Up @@ -96,12 +99,20 @@ const (
DiffSeparator = "**********************************"
)

const (
Json string = "json"
Yaml = "yaml"
)

var OutputFormats = []string{Json, Yaml}

type Options struct {
CRs resource.FilenameOptions
templatesDir string
diffConfigFileName string
diffAll bool
ShowManagedFields bool
OutputFormat string

builder *resource.Builder
corelator *MetricsCorelatorDecorator
Expand Down Expand Up @@ -149,7 +160,6 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
kcmdutil.CheckDiffErr(kcmdutil.UsageErrorf(cmd, err.Error()))
return nil
})

cmd.Flags().IntVar(&options.Concurrency, "concurrency", 4,
"Number of objects to process in parallel when diffing against the live version. Larger number = faster,"+
" but more memory, I/O and CPU over that shorter period of time.")
Expand All @@ -161,6 +171,20 @@ func NewCmd(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Comma
"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().StringVarP(&options.OutputFormat, "output", "o", "", fmt.Sprintf(`Output format. One of: (%s)`, strings.Join(OutputFormats, ", ")))
kcmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
"output",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var comps []string
for _, format := range OutputFormats {
if strings.HasPrefix(format, toComplete) {
comps = append(comps, format)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
},
))

return cmd
}

Expand Down Expand Up @@ -439,7 +463,11 @@ func (o *Options) Run() error {
return err
}
sum := newSummary(&o.reff, o.corelator, numDiffCRs)
o.Out.Write([]byte(Output{Summary: sum, Diffs: &diffs}.String()))

_, err = Output{Summary: sum, Diffs: &diffs}.Print(o.OutputFormat, o.Out)
if err != nil {
return err
}

// We will return exit code 1 in case there are differences between the reference CRs and cluster CRs.
//The differences can be differences found in specific CRs or the absence of CRs from the cluster.
Expand Down Expand Up @@ -487,9 +515,9 @@ func (obj InfoObject) Name() string {

// DiffSum Contains the diff output and correlation info of a specific CR
type DiffSum struct {
DiffOutput string
CorrelatedTemplate string
CRName string
DiffOutput string `json:"DiffOutput"`
CorrelatedTemplate string `json:"CorrelatedTemplate"`
CRName string `json:"CRName"`
}

func (s DiffSum) String() string {
Expand All @@ -509,10 +537,10 @@ Diff Output: None

// Summary Contains all info included in the Summary output of the compare command
type Summary struct {
RequiredCRS map[string]map[string][]string
NumMissing int
UnmatchedCRS []string
NumDiffCRs int
RequiredCRS map[string]map[string][]string `json:"RequiredCRS"`
NumMissing int `json:"NumMissing"`
UnmatchedCRS []string `json:"UnmatchedCRS"`
NumDiffCRs int `json:"NumDiffCRs"`
}

func newSummary(reference *Reference, c *MetricsCorelatorDecorator, numDiffCRs int) *Summary {
Expand Down Expand Up @@ -549,8 +577,8 @@ No CRs are unmatched to reference CRs

// Output Contains the complete output of the command
type Output struct {
Summary *Summary
Diffs *[]DiffSum
Summary *Summary `json:"Summary"`
Diffs *[]DiffSum `json:"Diffs"`
}

func (o Output) String() string {
Expand All @@ -563,3 +591,22 @@ func (o Output) String() string {
}
return fmt.Sprintf("\n%s\n%s%s", DiffSeparator, str, o.Summary.String())
}

func (o Output) Print(format string, out io.Writer) (int, error) {
switch format {
case Json:
content, err := json.Marshal(o)
if err != nil {
return 0, err
}
return out.Write(append(content, []byte("\n")...))
case Yaml:
content, err := yaml.Marshal(o)
if err != nil {
return 0, err
}
return out.Write(content)
default:
return out.Write([]byte(o.String()))
}
}
16 changes: 15 additions & 1 deletion pkg/compare/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Test struct {
mode []Mode
shouldPassUserConfig bool
shouldDiffAll bool
outputFormat string
}

func (test *Test) getTestDir() string {
Expand Down Expand Up @@ -198,6 +199,16 @@ func TestCompareRun(t *testing.T) {
name: "Reff With Template Functions Renders As Expected",
mode: []Mode{{Live, LocalReff}, {Local, LocalReff}, {Local, URL}},
},
{
name: "YAML Output",
mode: []Mode{DefaultMode},
outputFormat: string(Yaml),
},
{
name: "JSON Output",
mode: []Mode{DefaultMode},
outputFormat: string(Json),
},
}
tf := cmdtesting.NewTestFactory()
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
Expand Down Expand Up @@ -230,7 +241,7 @@ func removeInconsistentInfo(t *testing.T, text string) []byte {
re := regexp.MustCompile("\\/tmp\\/(?:LIVE|MERGED)-[0-9]*")
text = re.ReplaceAllString(text, "TEMP")
//remove diff datetime
re = regexp.MustCompile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{9} [+-]\\d{4})")
re = regexp.MustCompile("(\\d{4}-\\d{2}-\\d{2}\\s*\\d{2}:\\d{2}:\\d{2}\\.\\d{9} [+-]\\d{4})")
text = re.ReplaceAllString(text, "DATE")
pwd, err := os.Getwd()
require.NoError(t, err)
Expand Down Expand Up @@ -262,6 +273,9 @@ func getCommand(t *testing.T, test *Test, modeIndex int, tf *cmdtesting.TestFact
if test.shouldPassUserConfig {
require.NoError(t, cmd.Flags().Set("diff-config", path.Join(test.getTestDir(), userConfigFileName)))
}
if test.outputFormat != "" {
require.NoError(t, cmd.Flags().Set("output", test.outputFormat))
}
resourcesDir := path.Join(test.getTestDir(), resourceDirName)
switch mode.crSource {
case Local:
Expand Down
2 changes: 2 additions & 0 deletions pkg/compare/testdata/JSONOutput/localerr.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

error code:1
1 change: 1 addition & 0 deletions pkg/compare/testdata/JSONOutput/localout.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Summary":{"RequiredCRS":{"ExamplePart":{"Dashboard":["deploymentDashboard.yaml"]}},"NumMissing":1,"UnmatchedCRS":[],"NumDiffCRs":1},"Diffs":[{"DiffOutput":"diff -u -N TEMP/apps-v1_deployment_kubernetes-dashboard_dashboard-metrics-scraper TEMP/apps-v1_deployment_kubernetes-dashboard_dashboard-metrics-scraper\n--- TEMP/apps-v1_deployment_kubernetes-dashboard_dashboard-metrics-scraper\tDATE\n+++ TEMP/apps-v1_deployment_kubernetes-dashboard_dashboard-metrics-scraper\tDATE\n@@ -10,7 +10,7 @@\n revisionHistoryLimit: 10\n selector:\n matchLabels:\n- k8s-app: dashboard-metrics-scraper\n+ k8s-app: dashboard-metrics-scraper-diff\n template:\n metadata:\n labels:\n","CorrelatedTemplate":"deploymentMetrics.yaml","CRName":"apps/v1_Deployment_kubernetes-dashboard_dashboard-metrics-scraper"}]}
66 changes: 66 additions & 0 deletions pkg/compare/testdata/JSONOutput/reference/deploymentDashboard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: kubernetes-dashboard
template:
metadata:
labels:
k8s-app: kubernetes-dashboard
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: kubernetes-dashboard
image: kubernetesui/dashboard:v2.7.0
imagePullPolicy: Always
ports:
- containerPort: 8443
protocol: TCP
args:
- --auto-generate-certificates
- --namespace=kubernetes-dashboard
# Uncomment the following line to manually specify Kubernetes API server Host
# If not specified, Dashboard will attempt to auto discover the API server and connect
# to it. Uncomment only if the default does not work.
# - --apiserver-host=http://my-address:port
volumeMounts:
- name: kubernetes-dashboard-certs
mountPath: /certs
# Create on-disk volume to store exec logs
- mountPath: /tmp
name: tmp-volume
livenessProbe:
httpGet:
scheme: HTTPS
path: /
port: 8443
initialDelaySeconds: 30
timeoutSeconds: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 1001
runAsGroup: 2001
volumes:
- name: kubernetes-dashboard-certs
secret:
secretName: kubernetes-dashboard-certs
- name: tmp-volume
emptyDir: { }
serviceAccountName: kubernetes-dashboard
nodeSelector:
"kubernetes.io/os": linux
# Comment the following tolerations if Dashboard must not be deployed on master
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
19 changes: 19 additions & 0 deletions pkg/compare/testdata/JSONOutput/reference/deploymentMetrics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: dashboard-metrics-scraper
name: dashboard-metrics-scraper
namespace: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: dashboard-metrics-scraper
template:
metadata:
labels:
k8s-app: dashboard-metrics-scraper
spec:
{{ if .spec.template.spec }}{{ .spec.template.spec | toYaml | indent 6 }}{{ end }}
8 changes: 8 additions & 0 deletions pkg/compare/testdata/JSONOutput/reference/metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Parts:
- name: ExamplePart
Components:
- name: Dashboard
type: Required
requiredTemplates:
- deploymentDashboard.yaml
- deploymentMetrics.yaml
52 changes: 52 additions & 0 deletions pkg/compare/testdata/JSONOutput/resources/d2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: dashboard-metrics-scraper
name: dashboard-metrics-scraper
namespace: kubernetes-dashboard
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: dashboard-metrics-scraper-diff
template:
metadata:
labels:
k8s-app: dashboard-metrics-scraper
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: dashboard-metrics-scraper
image: kubernetesui/metrics-scraper:v1.0.8
ports:
- containerPort: 8000
protocol: TCP
livenessProbe:
httpGet:
scheme: HTTP
path: /
port: 8000
initialDelaySeconds: 30
timeoutSeconds: 30
volumeMounts:
- mountPath: /tmp
name: tmp-volume
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 1001
runAsGroup: 2001
serviceAccountName: kubernetes-dashboard
nodeSelector:
"kubernetes.io/os": linux
# Comment the following tolerations if Dashboard must not be deployed on master
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
volumes:
- name: tmp-volume
emptyDir: { }
2 changes: 2 additions & 0 deletions pkg/compare/testdata/YAMLOutput/localerr.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

error code:1
16 changes: 16 additions & 0 deletions pkg/compare/testdata/YAMLOutput/localout.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Diffs:
- CRName: apps/v1_Deployment_kubernetes-dashboard_kubernetes-dashboard
CorrelatedTemplate: deploymentDashboard.yaml
DiffOutput: "diff -u -N TEMP/apps-v1_deployment_kubernetes-dashboard_kubernetes-dashboard
TEMP/apps-v1_deployment_kubernetes-dashboard_kubernetes-dashboard\n---
TEMP/apps-v1_deployment_kubernetes-dashboard_kubernetes-dashboard\tDATE\n+++ TEMP/apps-v1_deployment_kubernetes-dashboard_kubernetes-dashboard\tDATE\n@@ -14,7 +14,7 @@\n template:\n metadata:\n labels:\n-
\ k8s-app: kubernetes-dashboard\n+ k8s-app: kubernetes-dashboard-diff\n
\ spec:\n containers:\n - args:\n"
Summary:
NumDiffCRs: 1
NumMissing: 1
RequiredCRS:
ExamplePart:
Dashboard:
- deploymentMetrics.yaml
UnmatchedCRS: []
Loading

0 comments on commit 433d21c

Please sign in to comment.