diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go
new file mode 100644
index 0000000000000000000000000000000000000000..eee3da033891439730756d621b03b683ed71ebb4
--- /dev/null
+++ b/cmd/flux/trace.go
@@ -0,0 +1,435 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"text/template"
+
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/fluxcd/flux2/internal/utils"
+	helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
+	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
+	fluxmeta "github.com/fluxcd/pkg/apis/meta"
+	sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
+)
+
+var traceCmd = &cobra.Command{
+	Use:   "trace [name]",
+	Short: "trace an in-cluster object throughout the GitOps delivery pipeline",
+	Long: `The trace command shows how an object is managed by Flux,
+from which source and revision it comes, and what's the latest reconciliation status.'`,
+	Example: `  # Trace a Kubernetes Deployment
+  flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps`,
+	RunE: traceCmdRun,
+}
+
+type traceFlags struct {
+	apiVersion string
+	kind       string
+}
+
+var traceArgs = traceFlags{}
+
+func init() {
+	traceCmd.Flags().StringVar(&traceArgs.kind, "kind", "",
+		"the Kubernetes object kind, e.g. Deployment'")
+	traceCmd.Flags().StringVar(&traceArgs.apiVersion, "api-version", "",
+		"the Kubernetes object API version, e.g. 'apps/v1'")
+	rootCmd.AddCommand(traceCmd)
+}
+
+func traceCmdRun(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		return fmt.Errorf("object name is required")
+	}
+	name := args[0]
+
+	if traceArgs.kind == "" {
+		return fmt.Errorf("object kind is required (--kind)")
+	}
+
+	if traceArgs.apiVersion == "" {
+		return fmt.Errorf("object apiVersion is required (--api-version)")
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
+	defer cancel()
+
+	kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
+	if err != nil {
+		return err
+	}
+
+	gv, err := schema.ParseGroupVersion(traceArgs.apiVersion)
+	if err != nil {
+		return fmt.Errorf("invaild apiVersion: %w", err)
+	}
+
+	obj := &unstructured.Unstructured{}
+	obj.SetGroupVersionKind(schema.GroupVersionKind{
+		Group:   gv.Group,
+		Version: gv.Version,
+		Kind:    traceArgs.kind,
+	})
+
+	objName := types.NamespacedName{
+		Namespace: rootArgs.namespace,
+		Name:      name,
+	}
+
+	err = kubeClient.Get(ctx, objName, obj)
+	if err != nil {
+		return fmt.Errorf("failed to find object: %w", err)
+	}
+
+	if ks, ok := isManagedByFlux(obj, kustomizev1.GroupVersion.Group); ok {
+		report, err := traceKustomization(ctx, kubeClient, ks, obj)
+		if err != nil {
+			return err
+		}
+		fmt.Println(report)
+		return nil
+	}
+
+	if hr, ok := isManagedByFlux(obj, helmv2.GroupVersion.Group); ok {
+		report, err := traceHelm(ctx, kubeClient, hr, obj)
+		if err != nil {
+			return err
+		}
+		fmt.Println(report)
+		return nil
+	}
+
+	return fmt.Errorf("object not managed by Flux")
+}
+
+func traceKustomization(ctx context.Context, kubeClient client.Client, ksName types.NamespacedName, obj *unstructured.Unstructured) (string, error) {
+	ks := &kustomizev1.Kustomization{}
+	ksReady := &metav1.Condition{}
+	err := kubeClient.Get(ctx, ksName, ks)
+	if err != nil {
+		return "", fmt.Errorf("failed to find kustomization: %w", err)
+	}
+	ksReady = meta.FindStatusCondition(ks.Status.Conditions, fluxmeta.ReadyCondition)
+
+	var ksRepository *sourcev1.GitRepository
+	var ksRepositoryReady *metav1.Condition
+	if ks.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind {
+		ksRepository = &sourcev1.GitRepository{}
+		sourceNamespace := ks.Namespace
+		if ks.Spec.SourceRef.Namespace != "" {
+			sourceNamespace = ks.Spec.SourceRef.Namespace
+		}
+		err = kubeClient.Get(ctx, types.NamespacedName{
+			Namespace: sourceNamespace,
+			Name:      ks.Spec.SourceRef.Name,
+		}, ksRepository)
+		if err != nil {
+			return "", fmt.Errorf("failed to find GitRepository: %w", err)
+		}
+		ksRepositoryReady = meta.FindStatusCondition(ksRepository.Status.Conditions, fluxmeta.ReadyCondition)
+	}
+
+	var traceTmpl = `
+Object:        {{.ObjectName}}
+{{- if .ObjectNamespace }}
+Namespace:     {{.ObjectNamespace}}
+{{- end }}
+Status:        Managed by Flux
+{{- if .Kustomization }}
+---
+Kustomization: {{.Kustomization.Name}}
+Namespace:     {{.Kustomization.Namespace}}
+{{- if .Kustomization.Spec.TargetNamespace }}
+Target:        {{.Kustomization.Spec.TargetNamespace}}
+{{- end }}
+Path:          {{.Kustomization.Spec.Path}}
+Revision:      {{.Kustomization.Status.LastAppliedRevision}}
+{{- if .KustomizationReady }}
+Status:        Last reconciled at {{.KustomizationReady.LastTransitionTime}}
+Message:       {{.KustomizationReady.Message}}
+{{- else }}
+Status:        Unknown
+{{- end }}
+{{- end }}
+{{- if .GitRepository }}
+---
+GitRepository: {{.GitRepository.Name}}
+Namespace:     {{.GitRepository.Namespace}}
+URL:           {{.GitRepository.Spec.URL}}
+{{- if .GitRepository.Spec.Reference.Branch }}
+Branch:        {{.GitRepository.Spec.Reference.Branch}}
+{{- end }}
+{{- if .GitRepository.Spec.Reference.Tag }}
+Tag:           {{.GitRepository.Spec.Reference.Tag}}
+{{- else if .GitRepository.Spec.Reference.SemVer }}
+Tag:           {{.GitRepository.Spec.Reference.SemVer}}
+{{- else if .GitRepository.Status.Artifact }}
+Revision:      {{.GitRepository.Status.Artifact.Revision}}
+{{- end }}
+{{- if .GitRepositoryReady }}
+Status:        Last reconciled at {{.GitRepositoryReady.LastTransitionTime}}
+Message:       {{.GitRepositoryReady.Message}}
+{{- else }}
+Status:        Unknown
+{{- end }}
+{{- end }}
+`
+
+	traceResult := struct {
+		ObjectName         string
+		ObjectNamespace    string
+		Kustomization      *kustomizev1.Kustomization
+		KustomizationReady *metav1.Condition
+		GitRepository      *sourcev1.GitRepository
+		GitRepositoryReady *metav1.Condition
+	}{
+		ObjectName:         obj.GetKind() + "/" + obj.GetName(),
+		ObjectNamespace:    obj.GetNamespace(),
+		Kustomization:      ks,
+		KustomizationReady: ksReady,
+		GitRepository:      ksRepository,
+		GitRepositoryReady: ksRepositoryReady,
+	}
+
+	t, err := template.New("tmpl").Parse(traceTmpl)
+	if err != nil {
+		return "", err
+	}
+
+	var data bytes.Buffer
+	writer := bufio.NewWriter(&data)
+	if err := t.Execute(writer, traceResult); err != nil {
+		return "", err
+	}
+
+	if err := writer.Flush(); err != nil {
+		return "", err
+	}
+
+	return data.String(), nil
+}
+
+func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.NamespacedName, obj *unstructured.Unstructured) (string, error) {
+	hr := &helmv2.HelmRelease{}
+	hrReady := &metav1.Condition{}
+	err := kubeClient.Get(ctx, hrName, hr)
+	if err != nil {
+		return "", fmt.Errorf("failed to find HelmRelease: %w", err)
+	}
+	hrReady = meta.FindStatusCondition(hr.Status.Conditions, fluxmeta.ReadyCondition)
+
+	var hrChart *sourcev1.HelmChart
+	var hrChartReady *metav1.Condition
+	if chart := hr.Status.HelmChart; chart != "" {
+		hrChart = &sourcev1.HelmChart{}
+		err = kubeClient.Get(ctx, utils.ParseNamespacedName(chart), hrChart)
+		if err != nil {
+			return "", fmt.Errorf("failed to find HelmChart: %w", err)
+		}
+		hrChartReady = meta.FindStatusCondition(hrChart.Status.Conditions, fluxmeta.ReadyCondition)
+	}
+
+	var hrGitRepository *sourcev1.GitRepository
+	var hrGitRepositoryReady *metav1.Condition
+	if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind {
+		hrGitRepository = &sourcev1.GitRepository{}
+		sourceNamespace := hr.Namespace
+		if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
+			sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
+		}
+		err = kubeClient.Get(ctx, types.NamespacedName{
+			Namespace: sourceNamespace,
+			Name:      hr.Spec.Chart.Spec.SourceRef.Name,
+		}, hrGitRepository)
+		if err != nil {
+			return "", fmt.Errorf("failed to find GitRepository: %w", err)
+		}
+		hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition)
+	}
+
+	var hrHelmRepository *sourcev1.HelmRepository
+	var hrHelmRepositoryReady *metav1.Condition
+	if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind {
+		hrHelmRepository = &sourcev1.HelmRepository{}
+		sourceNamespace := hr.Namespace
+		if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
+			sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
+		}
+		err = kubeClient.Get(ctx, types.NamespacedName{
+			Namespace: sourceNamespace,
+			Name:      hr.Spec.Chart.Spec.SourceRef.Name,
+		}, hrHelmRepository)
+		if err != nil {
+			return "", fmt.Errorf("failed to find HelmRepository: %w", err)
+		}
+		hrHelmRepositoryReady = meta.FindStatusCondition(hrHelmRepository.Status.Conditions, fluxmeta.ReadyCondition)
+	}
+
+	var traceTmpl = `
+Object:         {{.ObjectName}}
+{{- if .ObjectNamespace }}
+Namespace:      {{.ObjectNamespace}}
+{{- end }}
+Status:         Managed by Flux
+{{- if .HelmRelease }}
+---
+HelmRelease:    {{.HelmRelease.Name}}
+Namespace:      {{.HelmRelease.Namespace}}
+{{- if .HelmRelease.Spec.TargetNamespace }}
+Target:         {{.HelmRelease.Spec.TargetNamespace}}
+{{- end }}
+Revision:       {{.HelmRelease.Status.LastAppliedRevision}}
+{{- if .HelmReleaseReady }}
+Status:         Last reconciled at {{.HelmReleaseReady.LastTransitionTime}}
+Message:        {{.HelmReleaseReady.Message}}
+{{- else }}
+Status:         Unknown
+{{- end }}
+{{- end }}
+{{- if .HelmChart }}
+---
+HelmChart:      {{.HelmChart.Name}}
+Namespace:      {{.HelmChart.Namespace}}
+Chart:          {{.HelmChart.Spec.Chart}}
+Version:        {{.HelmChart.Spec.Version}}
+{{- if .HelmChart.Status.Artifact }}
+Revision:       {{.HelmChart.Status.Artifact.Revision}}
+{{- end }}
+{{- if .HelmChartReady }}
+Status:         Last reconciled at {{.HelmChartReady.LastTransitionTime}}
+Message:        {{.HelmChartReady.Message}}
+{{- else }}
+Status:         Unknown
+{{- end }}
+{{- end }}
+{{- if .HelmRepository }}
+---
+HelmRepository: {{.HelmRepository.Name}}
+Namespace:      {{.HelmRepository.Namespace}}
+URL:            {{.HelmRepository.Spec.URL}}
+{{- if .HelmRepository.Status.Artifact }}
+Revision:       {{.HelmRepository.Status.Artifact.Revision}}
+{{- end }}
+{{- if .HelmRepositoryReady }}
+Status:         Last reconciled at {{.HelmRepositoryReady.LastTransitionTime}}
+Message:        {{.HelmRepositoryReady.Message}}
+{{- else }}
+Status:         Unknown
+{{- end }}
+{{- end }}
+{{- if .GitRepository }}
+---
+GitRepository:  {{.GitRepository.Name}}
+Namespace:      {{.GitRepository.Namespace}}
+URL:            {{.GitRepository.Spec.URL}}
+{{- if .GitRepository.Spec.Reference.Branch }}
+Branch:         {{.GitRepository.Spec.Reference.Branch}}
+{{- end }}
+{{- if .GitRepository.Spec.Reference.Tag }}
+Tag:            {{.GitRepository.Spec.Reference.Tag}}
+{{- end }}
+{{- if .GitRepository.Spec.Reference.Tag }}
+Tag:            {{.GitRepository.Spec.Reference.Tag}}
+{{- end }}
+{{- if .GitRepository.Spec.Reference.SemVer }}
+Tag:            {{.GitRepository.Spec.Reference.SemVer}}
+{{- end }}
+{{- if .GitRepository.Status.Artifact }}
+Revision:       {{.GitRepository.Status.Artifact.Revision}}
+{{- end }}
+{{- if .GitRepositoryReady }}
+Status:         Last reconciled at {{.GitRepositoryReady.LastTransitionTime}}
+Message:        {{.GitRepositoryReady.Message}}
+{{- else }}
+Status:         Unknown
+{{- end }}
+{{- end }}
+`
+
+	traceResult := struct {
+		ObjectName          string
+		ObjectNamespace     string
+		HelmRelease         *helmv2.HelmRelease
+		HelmReleaseReady    *metav1.Condition
+		HelmChart           *sourcev1.HelmChart
+		HelmChartReady      *metav1.Condition
+		GitRepository       *sourcev1.GitRepository
+		GitRepositoryReady  *metav1.Condition
+		HelmRepository      *sourcev1.HelmRepository
+		HelmRepositoryReady *metav1.Condition
+	}{
+		ObjectName:          obj.GetKind() + "/" + obj.GetName(),
+		ObjectNamespace:     obj.GetNamespace(),
+		HelmRelease:         hr,
+		HelmReleaseReady:    hrReady,
+		HelmChart:           hrChart,
+		HelmChartReady:      hrChartReady,
+		GitRepository:       hrGitRepository,
+		GitRepositoryReady:  hrGitRepositoryReady,
+		HelmRepository:      hrHelmRepository,
+		HelmRepositoryReady: hrHelmRepositoryReady,
+	}
+
+	t, err := template.New("tmpl").Parse(traceTmpl)
+	if err != nil {
+		return "", err
+	}
+
+	var data bytes.Buffer
+	writer := bufio.NewWriter(&data)
+	if err := t.Execute(writer, traceResult); err != nil {
+		return "", err
+	}
+
+	if err := writer.Flush(); err != nil {
+		return "", err
+	}
+
+	return data.String(), nil
+}
+
+func isManagedByFlux(obj *unstructured.Unstructured, group string) (types.NamespacedName, bool) {
+	nameKey := fmt.Sprintf("%s/name", group)
+	namespaceKey := fmt.Sprintf("%s/namespace", group)
+	namespacedName := types.NamespacedName{}
+
+	for k, v := range obj.GetLabels() {
+		if k == nameKey {
+			namespacedName.Name = v
+		}
+		if k == namespaceKey {
+			namespacedName.Namespace = v
+		}
+	}
+
+	if namespacedName.Name == "" {
+		return namespacedName, false
+	}
+	return namespacedName, true
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index f606f4eba339470d79a550651c7f4fa3bcb4fa88..958f861fc8d059bd91c863a3d22191dd6b86c2e0 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -17,20 +17,12 @@ limitations under the License.
 package utils
 
 import (
-	"bufio"
 	"bytes"
 	"context"
 	"fmt"
+	"github.com/olekukonko/tablewriter"
 	"io"
 	"io/ioutil"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"text/template"
-
-	"github.com/olekukonko/tablewriter"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
@@ -38,11 +30,17 @@ import (
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	apiruntime "k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	sigyaml "k8s.io/apimachinery/pkg/util/yaml"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/yaml"
+	"strings"
 
 	helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
 	imageautov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
@@ -109,36 +107,6 @@ func ExecKubectlCommand(ctx context.Context, mode ExecMode, kubeConfigPath strin
 	return "", nil
 }
 
-func ExecTemplate(obj interface{}, tmpl, filename string) error {
-	t, err := template.New("tmpl").Parse(tmpl)
-	if err != nil {
-		return err
-	}
-
-	var data bytes.Buffer
-	writer := bufio.NewWriter(&data)
-	if err := t.Execute(writer, obj); err != nil {
-		return err
-	}
-
-	if err := writer.Flush(); err != nil {
-		return err
-	}
-
-	file, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-
-	_, err = io.WriteString(file, data.String())
-	if err != nil {
-		return err
-	}
-
-	return file.Sync()
-}
-
 func KubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error) {
 	configFiles := SplitKubeConfigPath(kubeConfigPath)
 	configOverrides := clientcmd.ConfigOverrides{}
@@ -225,6 +193,21 @@ func ContainsEqualFoldItemString(s []string, e string) (string, bool) {
 	return "", false
 }
 
+// ParseNamespacedName extracts the NamespacedName of a resource
+// based on the '<namespace>/<name>' format
+func ParseNamespacedName(input string) types.NamespacedName {
+	parts := strings.Split(input, "/")
+	if len(parts) == 2 {
+		return types.NamespacedName{
+			Namespace: parts[0],
+			Name:      parts[1],
+		}
+	}
+	return types.NamespacedName{
+		Name: input,
+	}
+}
+
 // ParseObjectKindName extracts the kind and name of a resource
 // based on the '<kind>/<name>' format
 func ParseObjectKindName(input string) (kind, name string) {