Repository: FairwindsOps/pluto
Pull request #81 (2020-06-03 11:27:06)

Add Support for Istio and cert-manager
This pull request addresses issue #20.

This worked for the use cases I had but might not fit what you have in mind. Feel free to reject.

**Note**: The deprecations for Istio are to by best knowledge but I was unable to find an exhaustive list of deprecations.

sudermanjr: @jnodorp Thanks for taking the time to make this!

This is definitely one concept I had in mind for how to tackle this. My hope was that we could find a more generic way to go about this rather than hard-coding in every external project we want to examine.

I'm going to go over the idea with the other codeowners and see what we come up with. In the meantime, I'll try this version out.
sudermanjr: Also, you will need to update the venom tests in `e2e/tests` to reflect the new json and yaml output at a minimum
sudermanjr: I am going to merge this as-is so that we can continue this work in #83 

Thanks a bunch for the inspiration and the excellent PR!

Patch:
diff --git a/README.md b/README.md
index 5f20e70..02556d0 100644
--- a/README.md
+++ b/README.md
@@ -216,7 +216,11 @@ $ pluto detect-helm --helm-version 3 -ojson | jq .
       "removed": false
     }
   ],
-  "target-version": "v1.16.0"
+  "target-versions": {
+    "k8s": "v1.16.0",
+    "istio": "v1.6.0",
+    "cert-manager": "v1.15.0"
+  }
 }
 ```
 
@@ -260,7 +264,10 @@ items:
     replacement-api: apiextensions.k8s.io/v1
   deprecated: true
   removed: false
-target-version: v1.16.0
+target-version:
+  k8s: v1.16.0
+  istio: v1.6.0
+  cert-manager: v1.15.0
 ```
 
 ### CI Pipelines
@@ -294,3 +301,19 @@ $ echo $?
 ```
 
 Notice that there is a deprecated version, but it was reported as non-deprecated because it has not yet been deprecated in v1.15.0. This particular run exited 0.
+
+You can target the Istio version you are concerned with by using the `--istio-target-version` flag. For example:
+
+```
+$ pluto detect-helm --istio-target-version "v1.6.0"
+NAME      KIND      VERSION                        REPLACEMENT                   REMOVED   DEPRECATED  
+default   Gateway   networking.istio.io/v1alpha3   networking.istio.io/v1beta1   false     true
+```
+
+You can target the cert-manager version you are concerned with by using the `--cert-manager-target-version` flag. For example:
+
+```
+$ pluto detect-helm --cert-manager-target-version "v1.15.0"
+NAME      KIND            VERSION                       REPLACEMENT                REMOVED   DEPRECATED  
+default   ClusterIssuer   certmanager.k8s.io/v1alpha1   cert-manager.io/v1alpha2   true      true
+```
diff --git a/cmd/root.go b/cmd/root.go
index 8040589..c65ef2b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -32,17 +32,19 @@ import (
 )
 
 var (
-	version            string
-	versionCommit      string
-	directory          string
-	outputFormat       string
-	showAll            bool
-	helmVersion        string
-	helmStore          string
-	ignoreDeprecations bool
-	ignoreRemovals     bool
-	targetVersion      string
-	namespace          string
+	version                  string
+	versionCommit            string
+	directory                string
+	outputFormat             string
+	showAll                  bool
+	helmVersion              string
+	helmStore                string
+	ignoreDeprecations       bool
+	ignoreRemovals           bool
+	targetVersion            string
+	istioTargetVersion       string
+	certManagerTargetVersion string
+	namespace                string
 )
 
 func init() {
@@ -52,6 +54,8 @@ func init() {
 	rootCmd.PersistentFlags().BoolVar(&ignoreRemovals, "ignore-removals", false, "Ignore the default behavior to exit 3 if removed apiVersions are found.")
 
 	rootCmd.PersistentFlags().StringVarP(&targetVersion, "target-version", "t", "v1.16.0", "The version of Kubernetes you wish to check deprecations for.")
+	rootCmd.PersistentFlags().StringVar(&istioTargetVersion, "istio-target-version", "v1.6.0", "The version of Istio you wish to check deprecations for.")
+	rootCmd.PersistentFlags().StringVar(&certManagerTargetVersion, "cert-manager-target-version", "v1.15.0", "The version of cert-manager you wish to check deprecations for.")
 	rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "normal", "The output format to use. (normal|wide|json|yaml)")
 
 	detectFilesCmd.PersistentFlags().StringVarP(&directory, "directory", "d", "", "The directory to scan. If blank, defaults to current workding dir.")
@@ -82,15 +86,21 @@ var rootCmd = &cobra.Command{
 		os.Exit(1)
 	},
 	PersistentPreRun: func(cmd *cobra.Command, args []string) {
-		c, _ := utf8.DecodeRuneInString(targetVersion)
-		if c != 'v' {
-			fmt.Printf("Your --target-version must begin with a 'v'. Got '%s'\n", targetVersion)
-			os.Exit(1)
-		}
+		for flag, version := range map[string]string{
+			"target-version":              targetVersion,
+			"istio-target-version":        istioTargetVersion,
+			"cert-manager-target-version": certManagerTargetVersion,
+		} {
+			c, _ := utf8.DecodeRuneInString(version)
+			if c != 'v' {
+				fmt.Printf("Your --%s must begin with a 'v'. Got '%s'\n", flag, version)
+				os.Exit(1)
+			}
 
-		if !semver.IsValid(targetVersion) {
-			fmt.Printf("You must pass a valid semver to --target-version. Got '%s'\n", targetVersion)
-			os.Exit(1)
+			if !semver.IsValid(version) {
+				fmt.Printf("You must pass a valid semver to --%s. Got '%s'\n", flag, version)
+				os.Exit(1)
+			}
 		}
 	},
 }
@@ -113,7 +123,11 @@ var detectFilesCmd = &cobra.Command{
 		}
 
 		instance := &api.Instance{
-			TargetVersion:      targetVersion,
+			TargetVersions: map[string]string{
+				"k8s":          targetVersion,
+				"istio":        istioTargetVersion,
+				"cert-manager": certManagerTargetVersion,
+			},
 			OutputFormat:       outputFormat,
 			ShowAll:            showAll,
 			Outputs:            dir.Outputs,
@@ -144,7 +158,11 @@ var detectHelmCmd = &cobra.Command{
 			os.Exit(1)
 		}
 		instance := &api.Instance{
-			TargetVersion:      targetVersion,
+			TargetVersions: map[string]string{
+				"k8s":          targetVersion,
+				"istio":        istioTargetVersion,
+				"cert-manager": certManagerTargetVersion,
+			},
 			OutputFormat:       outputFormat,
 			ShowAll:            showAll,
 			IgnoreDeprecations: ignoreDeprecations,
@@ -197,7 +215,10 @@ var detectCmd = &cobra.Command{
 				os.Exit(1)
 			}
 			instance := &api.Instance{
-				TargetVersion:      targetVersion,
+				TargetVersions: map[string]string{
+					"k8s":   targetVersion,
+					"istio": istioTargetVersion,
+				},
 				OutputFormat:       outputFormat,
 				ShowAll:            showAll,
 				IgnoreDeprecations: ignoreDeprecations,
@@ -220,7 +241,11 @@ var detectCmd = &cobra.Command{
 			os.Exit(1)
 		}
 		instance := &api.Instance{
-			TargetVersion:      targetVersion,
+			TargetVersions: map[string]string{
+				"k8s":          targetVersion,
+				"istio":        istioTargetVersion,
+				"cert-manager": certManagerTargetVersion,
+			},
 			OutputFormat:       outputFormat,
 			ShowAll:            showAll,
 			IgnoreDeprecations: ignoreDeprecations,
diff --git a/pkg/api/list.go b/pkg/api/list.go
index 8dea63e..2fa168b 100644
--- a/pkg/api/list.go
+++ b/pkg/api/list.go
@@ -26,50 +26,63 @@ import (
 // VersionList is a set of apiVersions and if they are deprecated or not.
 var VersionList = []Version{
 	// Not Removed or Deprecated
-	{"apps/v1", "Deployment", "", "", ""},
-	{"networking.k8s.io/v1", "NetworkPolicy", "", "", ""},
-	{"policy/v1beta1", "PodSecurityPolicy", "", "", ""},
+	{"apps/v1", "Deployment", "", "", "", "k8s"},
+	{"networking.k8s.io/v1", "NetworkPolicy", "", "", "", "k8s"},
+	{"policy/v1beta1", "PodSecurityPolicy", "", "", "", "k8s"},
 
 	// Removed in 1.16
-	{"extensions/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1"},
-	{"apps/v1beta2", "Deployment", "v1.9.0", "v1.16.0", "apps/v1"},
-	{"apps/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1"},
+	{"extensions/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
+	{"apps/v1beta2", "Deployment", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
+	{"apps/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
 
-	{"apps/v1beta1", "StatefulSet", "v1.9.0", "v1.16.0", "apps/v1"},
-	{"apps/v1beta2", "StatefulSet", "v1.9.0", "v1.16.0", "apps/v1"},
+	{"apps/v1beta1", "StatefulSet", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
+	{"apps/v1beta2", "StatefulSet", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
 
-	{"extensions/v1beta1", "NetworkPolicy", "v1.9.0", "v1.16.0", "networking.k8s.io/v1"},
-	{"extensions/v1beta1", "Ingress", "v1.14.0", "v1.22.0", "networking.k8s.io/v1beta1"},
+	{"extensions/v1beta1", "NetworkPolicy", "v1.9.0", "v1.16.0", "networking.k8s.io/v1", "k8s"},
+	{"extensions/v1beta1", "Ingress", "v1.14.0", "v1.22.0", "networking.k8s.io/v1beta1", "k8s"},
 
-	{"apps/v1beta2", "DaemonSet", "v1.9.0", "v1.16.0", "apps/v1"},
-	{"extensions/v1beta1", "DaemonSet", "v1.9.0", "v1.16.0", "apps/v1"},
+	{"apps/v1beta2", "DaemonSet", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
+	{"extensions/v1beta1", "DaemonSet", "v1.9.0", "v1.16.0", "apps/v1", "k8s"},
 
-	{"extensions/v1beta1", "PodSecurityPolicy", "v1.10.0", "v1.16.0", "policy/v1beta1"},
+	{"extensions/v1beta1", "PodSecurityPolicy", "v1.10.0", "v1.16.0", "policy/v1beta1", "k8s"},
 
-	{"extensions/v1beta1", "ReplicaSet", "", "v1.16.0", "apps/v1"},
-	{"apps/v1beta1", "ReplicaSet", "", "v1.16.0", "apps/v1"},
-	{"apps/v1beta2", "ReplicaSet", "", "v1.16.0", "apps/v1"},
+	{"extensions/v1beta1", "ReplicaSet", "", "v1.16.0", "apps/v1", "k8s"},
+	{"apps/v1beta1", "ReplicaSet", "", "v1.16.0", "apps/v1", "k8s"},
+	{"apps/v1beta2", "ReplicaSet", "", "v1.16.0", "apps/v1", "k8s"},
 
 	// Removed in 1.17
-	{"scheduling.k8s.io/v1beta1", "PriorityClass", "v1.14.0", "v1.17.0", "scheduling.k8s.io/v1"},
-	{"scheduling.k8s.io/v1alpha1", "PriorityClass", "v1.14.0", "v1.17.0", "scheduling.k8s.io/v1"},
-	{"apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "v1.16.0", "v1.19.0", "apiextensions.k8s.io/v1"},
+	{"scheduling.k8s.io/v1beta1", "PriorityClass", "v1.14.0", "v1.17.0", "scheduling.k8s.io/v1", "k8s"},
+	{"scheduling.k8s.io/v1alpha1", "PriorityClass", "v1.14.0", "v1.17.0", "scheduling.k8s.io/v1", "k8s"},
+	{"apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", "v1.16.0", "v1.19.0", "apiextensions.k8s.io/v1", "k8s"},
 
 	// Removed in 1.19
-	{"admissionregistration.k8s.io/v1beta1", "MutatingWebhookConfiguration", "v1.16.0", "v1.19.0", "admissionregistration.k8s.io/v1"},
+	{"admissionregistration.k8s.io/v1beta1", "MutatingWebhookConfiguration", "v1.16.0", "v1.19.0", "admissionregistration.k8s.io/v1", "k8s"},
 
 	// Removed in 1.20
-	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleBinding", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRole", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleBindingList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "Role", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "RoleBinding", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "RoleList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
-	{"rbac.authorization.k8s.io/v1alpha1", "RoleBindingList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1"},
+	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleBinding", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRole", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleBindingList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "ClusterRoleList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "Role", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "RoleBinding", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "RoleList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
+	{"rbac.authorization.k8s.io/v1alpha1", "RoleBindingList", "v1.17.0", "v1.20.0", "rbac.authorization.k8s.io/v1", "k8s"},
 
 	// Unknown Removal, but deprecated
-	{"storage.k8s.io/v1beta1", "CSINode", "v1.17.0", "", ""},
+	{"storage.k8s.io/v1beta1", "CSINode", "v1.17.0", "", "", "k8s"},
+
+	// Istio API versions
+	{"rbac.istio.io", "AuthorizationPolicies", "v1.4.0", "v1.4.0", "security.istio.io/v1beta1", "istio"},
+	{"authentication.istio.io/v1alpha1", "", "v1.5.0", "v1.6.0", "security.istio.io/v1beta1", "istio"},
+	{"networking.istio.io/v1alpha3", "", "v1.5.0", "", "networking.istio.io/v1beta1", "istio"},
+
+	// cert-manager API versions.
+	{"certmanager.k8s.io/v1alpha1", "Certificate", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
+	{"certmanager.k8s.io/v1alpha1", "Issuer", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
+	{"certmanager.k8s.io/v1alpha1", "ClusterIssuer", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
+	{"certmanager.k8s.io/v1alpha1", "CertificateRequest", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
+	{"certmanager.k8s.io/v1alpha1", "Order", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
+	{"certmanager.k8s.io/v1alpha1", "Challenge", "v0.11.0", "v0.11.0", "cert-manager.io/v1alpha2", "cert-manager"},
 }
 
 // PrintVersionList prints out the list of versions
diff --git a/pkg/api/output.go b/pkg/api/output.go
index d1950f2..5efadcb 100644
--- a/pkg/api/output.go
+++ b/pkg/api/output.go
@@ -22,12 +22,12 @@ type Output struct {
 
 // Instance is an instance of the API. This holds configuration for a "run" of Pluto
 type Instance struct {
-	Outputs            []*Output `json:"items,omitempty" yaml:"items,omitempty"`
-	IgnoreDeprecations bool      `json:"-" yaml:"-"`
-	IgnoreRemovals     bool      `json:"-" yaml:"-"`
-	OutputFormat       string    `json:"-" yaml:"-"`
-	ShowAll            bool      `json:"show-all,omitempty" yaml:"show-all,omitempty"`
-	TargetVersion      string    `json:"target-version,omitempty" yaml:"target-version,omitempty"`
+	Outputs            []*Output         `json:"items,omitempty" yaml:"items,omitempty"`
+	IgnoreDeprecations bool              `json:"-" yaml:"-"`
+	IgnoreRemovals     bool              `json:"-" yaml:"-"`
+	OutputFormat       string            `json:"-" yaml:"-"`
+	ShowAll            bool              `json:"show-all,omitempty" yaml:"show-all,omitempty"`
+	TargetVersions     map[string]string `json:"target-versions,omitempty" yaml:"target-versions,omitempty"`
 }
 
 // DisplayOutput prints the output based on desired variables
@@ -81,11 +81,11 @@ func (instance *Instance) DisplayOutput() error {
 func (instance *Instance) filterOutput() {
 	var usableOutputs []*Output
 	for _, output := range instance.Outputs {
-		output.Deprecated = output.APIVersion.isDeprecatedIn(instance.TargetVersion)
-		output.Removed = output.APIVersion.isRemovedIn(instance.TargetVersion)
+		output.Deprecated = output.APIVersion.isDeprecatedIn(instance.TargetVersions)
+		output.Removed = output.APIVersion.isRemovedIn(instance.TargetVersions)
 		if instance.ShowAll {
 			usableOutputs = append(usableOutputs, output)
-		} else if output.APIVersion.isDeprecatedIn(instance.TargetVersion) || output.APIVersion.isRemovedIn(instance.TargetVersion) {
+		} else if output.APIVersion.isDeprecatedIn(instance.TargetVersions) || output.APIVersion.isRemovedIn(instance.TargetVersions) {
 			usableOutputs = append(usableOutputs, output)
 		}
 	}
@@ -153,11 +153,11 @@ func (instance *Instance) GetReturnCode() int {
 	var deprecations int
 	var removals int
 	for _, output := range instance.Outputs {
-		if output.APIVersion.isRemovedIn(instance.TargetVersion) {
+		if output.APIVersion.isRemovedIn(instance.TargetVersions) {
 
 			removals = removals + 1
 		}
-		if output.APIVersion.isDeprecatedIn(instance.TargetVersion) {
+		if output.APIVersion.isDeprecatedIn(instance.TargetVersions) {
 			deprecations = deprecations + 1
 		}
 	}
diff --git a/pkg/api/versions.go b/pkg/api/versions.go
index 5fd768a..333c1b0 100644
--- a/pkg/api/versions.go
+++ b/pkg/api/versions.go
@@ -52,12 +52,19 @@ type Version struct {
 	RemovedIn string `json:"removed-in" yaml:"removed-in"`
 	// ReplacementAPI is the apiVersion that replaces the deprecated one
 	ReplacementAPI string `json:"replacement-api" yaml:"replacement-api"`
+	// Component is the component associated with this version
+	Component string `json:"-" yaml:"-"`
 }
 
 func checkVersion(stub *Stub) *Version {
 	for _, version := range VersionList {
-		if version.Kind == stub.Kind {
+		// We allow empty kinds to deprecate whole APIs.
+		if version.Kind == "" || version.Kind == stub.Kind {
 			if version.Name == stub.APIVersion {
+				if version.Kind == "" {
+					version.Kind = stub.Kind
+				}
+
 				return &version
 			}
 		}
@@ -138,30 +145,50 @@ func yamlToStub(data []byte) ([]*Stub, error) {
 	return stubs, nil
 }
 
-// IsDeprecatedIn returns true if the version is deprecated in the targetVersion
+// IsDeprecatedIn returns true if the version is deprecated in the applicable targetVersion
 // Will return false if the targetVersion passed is not a valid semver string
-func (v *Version) isDeprecatedIn(targetVersion string) bool {
-	if !semver.IsValid(targetVersion) {
-		klog.V(3).Infof("targetVersion %s is not valid semVer", targetVersion)
-		return false
+func (v *Version) isDeprecatedIn(targetVersions map[string]string) bool {
+	for component, targetVersion := range targetVersions {
+		if !semver.IsValid(targetVersion) {
+			klog.V(3).Infof("targetVersion %s for %s is not valid semVer", targetVersion, component)
+			return false
+		}
 	}
+
 	if v.DeprecatedIn == "" {
 		return false
 	}
+
+	targetVersion, ok := targetVersions[v.Component]
+	if !ok {
+		klog.V(3).Infof("targetVersion missing for component %s", v.Component)
+		return false
+	}
+
 	comparison := semver.Compare(targetVersion, v.DeprecatedIn)
 	return comparison >= 0
 }
 
-// IsRemovedIn returns true if the version is deprecated in the targetVersion
+// IsRemovedIn returns true if the version is deprecated in the applicable targetVersion
 // Will return false if the targetVersion passed is not a valid semver string
-func (v *Version) isRemovedIn(targetVersion string) bool {
-	if !semver.IsValid(targetVersion) {
-		klog.V(3).Infof("targetVersion %s is not valid semVer", targetVersion)
-		return false
+func (v *Version) isRemovedIn(targetVersions map[string]string) bool {
+	for component, targetVersion := range targetVersions {
+		if !semver.IsValid(targetVersion) {
+			klog.V(3).Infof("targetVersion %s for %s is not valid semVer", targetVersion, component)
+			return false
+		}
 	}
+
 	if v.RemovedIn == "" {
 		return false
 	}
+
+	targetVersion, ok := targetVersions[v.Component]
+	if !ok {
+		klog.V(3).Infof("targetVersion missing for component %s", v.Component)
+		return false
+	}
+
 	comparison := semver.Compare(targetVersion, v.RemovedIn)
 	return comparison >= 0
 }


Test patch:
diff --git a/pkg/api/list_test.go b/pkg/api/list_test.go
index 285640b..a38c15b 100644
--- a/pkg/api/list_test.go
+++ b/pkg/api/list_test.go
@@ -35,7 +35,7 @@ func Test_VersionListIsValid(t *testing.T) {
 }
 
 var TestVersionList = []Version{
-	{"apps/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1"},
+	{"apps/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1", "foo"},
 }
 
 func Example_printVersionsTabular() {
@@ -73,6 +73,15 @@ func Example_printVersionsTabular() {
 	// RoleList---------------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
 	// RoleBindingList--------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
 	// CSINode----------------------- storage.k8s.io/v1beta1---------------- v1.17.0-------- n/a--------- n/a------------------------------
+	// AuthorizationPolicies--------- rbac.istio.io------------------------- v1.4.0--------- v1.4.0------ security.istio.io/v1beta1--------
+	// ------------------------------ authentication.istio.io/v1alpha1------ v1.5.0--------- v1.6.0------ security.istio.io/v1beta1--------
+	// ------------------------------ networking.istio.io/v1alpha3---------- v1.5.0--------- n/a--------- networking.istio.io/v1beta1------
+	// Certificate------------------- certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
+	// Issuer------------------------ certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
+	// ClusterIssuer----------------- certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
+	// CertificateRequest------------ certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
+	// Order------------------------- certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
+	// Challenge--------------------- certmanager.k8s.io/v1alpha1----------- v0.11.0-------- v0.11.0----- cert-manager.io/v1alpha2---------
 }
 
 func ExamplePrintVersionList_json() {
diff --git a/pkg/api/output_test.go b/pkg/api/output_test.go
index 8eba2b2..bad84d0 100644
--- a/pkg/api/output_test.go
+++ b/pkg/api/output_test.go
@@ -29,6 +29,7 @@ var testOutput1 = &Output{
 		DeprecatedIn:   "",
 		RemovedIn:      "",
 		ReplacementAPI: "",
+		Component:      "foo",
 	},
 }
 var testOutput2 = &Output{
@@ -39,6 +40,7 @@ var testOutput2 = &Output{
 		DeprecatedIn:   "v1.9.0",
 		RemovedIn:      "v1.16.0",
 		ReplacementAPI: "apps/v1",
+		Component:      "foo",
 	},
 }
 
@@ -48,7 +50,9 @@ func init() {
 
 func ExampleInstance_DisplayOutput_showAll_normal() {
 	instance := &Instance{
-		TargetVersion: "v1.15.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.15.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -66,7 +70,9 @@ func ExampleInstance_DisplayOutput_showAll_normal() {
 
 func ExampleInstance_DisplayOutput_showAll_wide() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -84,7 +90,9 @@ func ExampleInstance_DisplayOutput_showAll_wide() {
 
 func ExampleInstance_DisplayOutput_normal() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -101,7 +109,9 @@ func ExampleInstance_DisplayOutput_normal() {
 
 func ExampleInstance_DisplayOutput_wide() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -118,7 +128,9 @@ func ExampleInstance_DisplayOutput_wide() {
 
 func ExampleInstance_DisplayOutput_showAll_json() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -129,12 +141,14 @@ func ExampleInstance_DisplayOutput_showAll_json() {
 	_ = instance.DisplayOutput()
 
 	// Output:
-	// {"items":[{"name":"some name one","namespace":"pluto-namespace","api":{"version":"apps/v1","kind":"Deployment","deprecated-in":"","removed-in":"","replacement-api":""},"deprecated":false,"removed":false},{"name":"some name two","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true}],"show-all":true,"target-version":"v1.16.0"}
+	// {"items":[{"name":"some name one","namespace":"pluto-namespace","api":{"version":"apps/v1","kind":"Deployment","deprecated-in":"","removed-in":"","replacement-api":""},"deprecated":false,"removed":false},{"name":"some name two","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true}],"show-all":true,"target-versions":{"foo":"v1.16.0"}}
 }
 
 func ExampleInstance_DisplayOutput_showAll_yaml() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -166,12 +180,15 @@ func ExampleInstance_DisplayOutput_showAll_yaml() {
 	//   deprecated: true
 	//   removed: true
 	// show-all: true
-	// target-version: v1.16.0
+	// target-versions:
+	//   foo: v1.16.0
 }
 
 func ExampleInstance_DisplayOutput_noOutput() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 		},
@@ -185,7 +202,9 @@ func ExampleInstance_DisplayOutput_noOutput() {
 
 func ExampleInstance_DisplayOutput_badFormat() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
 		Outputs: []*Output{
 			testOutput1,
 			testOutput2,
@@ -200,10 +219,12 @@ func ExampleInstance_DisplayOutput_badFormat() {
 
 func ExampleInstance_DisplayOutput_zeroLength() {
 	instance := &Instance{
-		TargetVersion: "v1.16.0",
-		Outputs:       []*Output{},
-		OutputFormat:  "normal",
-		ShowAll:       false,
+		TargetVersions: map[string]string{
+			"foo": "v1.16.0",
+		},
+		Outputs:      []*Output{},
+		OutputFormat: "normal",
+		ShowAll:      false,
 	}
 	_ = instance.DisplayOutput()
 
@@ -238,6 +259,7 @@ func TestGetReturnCode(t *testing.T) {
 						APIVersion: &Version{
 							DeprecatedIn: "v1.16.0",
 							RemovedIn:    "v1.20.0",
+							Component:    "foo",
 						},
 					},
 				},
@@ -254,6 +276,7 @@ func TestGetReturnCode(t *testing.T) {
 						APIVersion: &Version{
 							DeprecatedIn: "v1.16.0",
 							RemovedIn:    "v1.20.0",
+							Component:    "foo",
 						},
 					},
 				},
@@ -270,6 +293,7 @@ func TestGetReturnCode(t *testing.T) {
 						APIVersion: &Version{
 							RemovedIn:    "v1.16.0",
 							DeprecatedIn: "v1.12.0",
+							Component:    "foo",
 						},
 					},
 				},
@@ -282,7 +306,9 @@ func TestGetReturnCode(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			instance := &Instance{
-				TargetVersion:      "v1.16.0",
+				TargetVersions: map[string]string{
+					"foo": "v1.16.0",
+				},
 				IgnoreDeprecations: tt.args.ignoreDeprecations,
 				IgnoreRemovals:     tt.args.ignoreRemovals,
 				Outputs:            tt.args.outputs,
diff --git a/pkg/api/versions_test.go b/pkg/api/versions_test.go
index 7f74e38..7adc85b 100644
--- a/pkg/api/versions_test.go
+++ b/pkg/api/versions_test.go
@@ -187,7 +187,7 @@ func Test_IsVersioned(t *testing.T) {
 		{
 			name:    "yaml has version",
 			data:    []byte("kind: Deployment\napiVersion: apps/v1"),
-			want:    []*Output{{APIVersion: &Version{Name: "apps/v1", Kind: "Deployment", DeprecatedIn: "", RemovedIn: "", ReplacementAPI: ""}}},
+			want:    []*Output{{APIVersion: &Version{Name: "apps/v1", Kind: "Deployment", DeprecatedIn: "", RemovedIn: "", ReplacementAPI: "", Component: "k8s"}}},
 			wantErr: false,
 		},
 		{
@@ -211,7 +211,7 @@ func Test_IsVersioned(t *testing.T) {
 		{
 			name:    "json has version",
 			data:    []byte(`{"kind": "Deployment", "apiVersion": "extensions/v1beta1"}`),
-			want:    []*Output{{APIVersion: &Version{Kind: "Deployment", Name: "extensions/v1beta1", RemovedIn: "v1.16.0", DeprecatedIn: "v1.9.0", ReplacementAPI: "apps/v1"}}},
+			want:    []*Output{{APIVersion: &Version{Kind: "Deployment", Name: "extensions/v1beta1", RemovedIn: "v1.16.0", DeprecatedIn: "v1.9.0", ReplacementAPI: "apps/v1", Component: "k8s"}}},
 			wantErr: false,
 		},
 	}
@@ -232,45 +232,45 @@ func Test_IsVersioned(t *testing.T) {
 func TestVersion_IsDeprecatedIn(t *testing.T) {
 
 	tests := []struct {
-		name          string
-		targetVersion string
-		want          bool
-		deprecatedIn  string
+		name           string
+		targetVersions map[string]string
+		want           bool
+		deprecatedIn   string
 	}{
 		{
-			name:          "not deprecated yet 1.15.0",
-			targetVersion: "v1.15.0",
-			deprecatedIn:  "v1.16.0",
-			want:          false,
+			name:           "not deprecated yet 1.15.0",
+			targetVersions: map[string]string{"foo": "v1.15.0"},
+			deprecatedIn:   "v1.16.0",
+			want:           false,
 		},
 		{
-			name:          "equal values",
-			targetVersion: "v1.16.0",
-			deprecatedIn:  "v1.16.0",
-			want:          true,
+			name:           "equal values",
+			targetVersions: map[string]string{"foo": "v1.16.0"},
+			deprecatedIn:   "v1.16.0",
+			want:           true,
 		},
 		{
-			name:          "greater than",
-			targetVersion: "v1.17.0",
-			deprecatedIn:  "v1.16.0",
-			want:          true,
+			name:           "greater than",
+			targetVersions: map[string]string{"foo": "v1.17.0"},
+			deprecatedIn:   "v1.16.0",
+			want:           true,
 		},
 		{
-			name:          "Bad semVer",
-			targetVersion: "foo",
-			deprecatedIn:  "v1.16.0",
-			want:          false,
+			name:           "Bad semVer",
+			targetVersions: map[string]string{"foo": "foo"},
+			deprecatedIn:   "v1.16.0",
+			want:           false,
 		},
 		{
-			name:          "blank deprecatedIn - not deprecated",
-			targetVersion: "v1.16.0",
-			deprecatedIn:  "",
-			want:          false,
+			name:           "blank deprecatedIn - not deprecated",
+			targetVersions: map[string]string{"foo": "v1.16.0"},
+			deprecatedIn:   "",
+			want:           false,
 		},
 	}
 	for _, tt := range tests {
-		deprecatedVersion := &Version{DeprecatedIn: tt.deprecatedIn}
-		got := deprecatedVersion.isDeprecatedIn(tt.targetVersion)
+		deprecatedVersion := &Version{DeprecatedIn: tt.deprecatedIn, Component: "foo"}
+		got := deprecatedVersion.isDeprecatedIn(tt.targetVersions)
 		assert.Equal(t, tt.want, got, "test failed: "+tt.name)
 	}
 }
@@ -278,45 +278,45 @@ func TestVersion_IsDeprecatedIn(t *testing.T) {
 func TestVersion_IsRemovedIn(t *testing.T) {
 
 	tests := []struct {
-		name          string
-		targetVersion string
-		want          bool
-		removedIn     string
+		name           string
+		targetVersions map[string]string
+		want           bool
+		removedIn      string
 	}{
 		{
-			name:          "not removed yet 1.15.0",
-			targetVersion: "v1.15.0",
-			removedIn:     "v1.16.0",
-			want:          false,
+			name:           "not removed yet 1.15.0",
+			targetVersions: map[string]string{"foo": "v1.15.0"},
+			removedIn:      "v1.16.0",
+			want:           false,
 		},
 		{
-			name:          "equal values",
-			targetVersion: "v1.16.0",
-			removedIn:     "v1.16.0",
-			want:          true,
+			name:           "equal values",
+			targetVersions: map[string]string{"foo": "v1.16.0"},
+			removedIn:      "v1.16.0",
+			want:           true,
 		},
 		{
-			name:          "greater than",
-			targetVersion: "v1.17.0",
-			removedIn:     "v1.16.0",
-			want:          true,
+			name:           "greater than",
+			targetVersions: map[string]string{"foo": "v1.17.0"},
+			removedIn:      "v1.16.0",
+			want:           true,
 		},
 		{
-			name:          "bad semVer",
-			targetVersion: "foo",
-			removedIn:     "v1.16.0",
-			want:          false,
+			name:           "bad semVer",
+			targetVersions: map[string]string{"foo": "foo"},
+			removedIn:      "v1.16.0",
+			want:           false,
 		},
 		{
-			name:          "blank removedIn - not removed",
-			targetVersion: "v1.16.0",
-			removedIn:     "",
-			want:          false,
+			name:           "blank removedIn - not removed",
+			targetVersions: map[string]string{"foo": "v1.16.0"},
+			removedIn:      "",
+			want:           false,
 		},
 	}
 	for _, tt := range tests {
-		removedVersion := &Version{RemovedIn: tt.removedIn}
-		got := removedVersion.isRemovedIn(tt.targetVersion)
+		removedVersion := &Version{RemovedIn: tt.removedIn, Component: "foo"}
+		got := removedVersion.isRemovedIn(tt.targetVersions)
 		assert.Equal(t, tt.want, got, "test failed: "+tt.name)
 	}
 }
diff --git a/pkg/finder/finder_test.go b/pkg/finder/finder_test.go
index ac1d681..a21e02a 100644
--- a/pkg/finder/finder_test.go
+++ b/pkg/finder/finder_test.go
@@ -32,6 +32,7 @@ var deploymentAppsV1YamlFile = []*api.Output{{
 		Name:         "apps/v1",
 		Kind:         "Deployment",
 		DeprecatedIn: "",
+		Component:    "k8s",
 	}},
 }
 
@@ -45,6 +46,7 @@ var deploymentExtensionsV1YamlFile = []*api.Output{{
 		DeprecatedIn:   "v1.9.0",
 		RemovedIn:      "v1.16.0",
 		ReplacementAPI: "apps/v1",
+		Component:      "k8s",
 	}},
 }
 
@@ -58,6 +60,7 @@ var deploymentExtensionsV1JSONFile = []*api.Output{{
 		DeprecatedIn:   "v1.9.0",
 		RemovedIn:      "v1.16.0",
 		ReplacementAPI: "apps/v1",
+		Component:      "k8s",
 	}},
 }
 
diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go
index 887446d..98928c6 100644
--- a/pkg/helm/helm_test.go
+++ b/pkg/helm/helm_test.go
@@ -97,6 +97,7 @@ var (
 				DeprecatedIn:   "v1.9.0",
 				RemovedIn:      "v1.16.0",
 				ReplacementAPI: "apps/v1",
+				Component:      "k8s",
 			},
 		},
 		{
@@ -108,6 +109,7 @@ var (
 				DeprecatedIn:   "",
 				RemovedIn:      "",
 				ReplacementAPI: "",
+				Component:      "k8s",
 			},
 		},
 	}
@@ -121,6 +123,7 @@ var (
 				DeprecatedIn:   "",
 				RemovedIn:      "",
 				ReplacementAPI: "",
+				Component:      "k8s",
 			},
 		},
 	}
@@ -159,7 +162,7 @@ func Test_checkForAPIVersion(t *testing.T) {
 		{
 			name:     "got version",
 			manifest: []byte("apiVersion: apps/v1\nkind: Deployment"),
-			want:     []*api.Output{{APIVersion: &api.Version{Name: "apps/v1", Kind: "Deployment", DeprecatedIn: "", RemovedIn: "", ReplacementAPI: ""}}},
+			want:     []*api.Output{{APIVersion: &api.Version{Name: "apps/v1", Kind: "Deployment", DeprecatedIn: "", RemovedIn: "", ReplacementAPI: "", Component: "k8s"}}},
 			wantErr:  false,
 		},
 	}