diff --git a/cmd/tk/create_kustomization.go b/cmd/tk/create_kustomization.go
new file mode 100644
index 0000000000000000000000000000000000000000..c296b3c35c2439e921b292ac84ea3215f60643b0
--- /dev/null
+++ b/cmd/tk/create_kustomization.go
@@ -0,0 +1,240 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
+	"strings"
+	"time"
+
+	sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var createKsCmd = &cobra.Command{
+	Use:   "kustomization [name]",
+	Short: "Create or update a kustomization resource",
+	Long: `
+The kustomization source command generates a kustomization.kustomize.fluxcd.io resource for a given GitRepository source.
+API spec: https://github.com/fluxcd/kustomize-controller/tree/master/docs/spec/v1alpha1`,
+	Example: `  # Create a kustomization from a source at a given path
+  create kustomization backend \
+    --source=webapp \
+    --path="./overlays/backend/" \
+    --prune="app=backend" \
+    --interval=10m \
+    --validate=client \
+    --health-check="StatefulSet/backend.test" \
+    --health-check-timeout=3m
+
+  # Create a kustomization that depends on another
+  create kustomization frontend \
+    --depends-on=backend \
+    --source=webapp \
+    --path="./overlays/frontend/" \
+    --prune="app=frontend" \
+    --interval=5m \
+    --validate=client \
+    --health-check="Deployment/frontend.test" \
+    --health-check-timeout=2m
+`,
+	RunE: createKsCmdRun,
+}
+
+var (
+	ksSource        string
+	ksPath          string
+	ksPrune         string
+	ksDependsOn     []string
+	ksValidate      string
+	ksHealthCheck   []string
+	ksHealthTimeout time.Duration
+	ksGenerate      bool
+)
+
+func init() {
+	createKsCmd.Flags().StringVar(&ksSource, "source", "", "GitRepository name")
+	createKsCmd.Flags().StringVar(&ksPath, "path", "./", "path to the directory containing the kustomization file")
+	createKsCmd.Flags().StringVar(&ksPrune, "prune", "", "label selector used for garbage collection")
+	createKsCmd.Flags().StringArrayVar(&ksHealthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '<kind>/<name>.<namespace>'")
+	createKsCmd.Flags().DurationVar(&ksHealthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations")
+	createKsCmd.Flags().StringVar(&ksValidate, "validate", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'")
+	createKsCmd.Flags().BoolVar(&ksGenerate, "generate", false, "generate the kustomization.yaml for all the Kubernetes manifests in the specified path and sub-directories")
+	createKsCmd.Flags().StringArrayVar(&ksDependsOn, "depends-on", nil, "kustomization that must be ready before this kustomization can be applied")
+
+	createCmd.AddCommand(createKsCmd)
+}
+
+func createKsCmdRun(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		return fmt.Errorf("kustomization name is required")
+	}
+	name := args[0]
+
+	if ksSource == "" {
+		return fmt.Errorf("source is required")
+	}
+	if ksPath == "" {
+		return fmt.Errorf("path is required")
+	}
+	if !strings.HasPrefix(ksPath, "./") {
+		return fmt.Errorf("path must begin with ./")
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+
+	kubeClient, err := utils.kubeClient(kubeconfig)
+	if err != nil {
+		return err
+	}
+
+	logAction("generating %s kustomization", name)
+
+	emptyAPIGroup := ""
+	kustomization := kustomizev1.Kustomization{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: kustomizev1.KustomizationSpec{
+			DependsOn: ksDependsOn,
+			Generate:  ksGenerate,
+			Interval: metav1.Duration{
+				Duration: interval,
+			},
+			Path:  ksPath,
+			Prune: ksPrune,
+			SourceRef: corev1.TypedLocalObjectReference{
+				APIGroup: &emptyAPIGroup,
+				Kind:     "GitRepository",
+				Name:     ksSource,
+			},
+			Suspend:    false,
+			Validation: ksValidate,
+		},
+	}
+
+	if len(ksHealthCheck) > 0 {
+		healthChecks := make([]kustomizev1.WorkloadReference, 0)
+		for _, w := range ksHealthCheck {
+			kindObj := strings.Split(w, "/")
+			if len(kindObj) != 2 {
+				return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj)
+			}
+			kind := kindObj[0]
+			kinds := map[string]bool{
+				"Deployment":  true,
+				"DaemonSet":   true,
+				"StatefulSet": true,
+			}
+			if !kinds[kind] {
+				return fmt.Errorf("invalid health check kind '%s' can be Deployment, DaemonSet or StatefulSet", kind)
+			}
+			nameNs := strings.Split(kindObj[1], ".")
+			if len(nameNs) != 2 {
+				return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w)
+			}
+
+			healthChecks = append(healthChecks, kustomizev1.WorkloadReference{
+				Kind:      kind,
+				Name:      nameNs[0],
+				Namespace: nameNs[1],
+			})
+		}
+		kustomization.Spec.HealthChecks = healthChecks
+		kustomization.Spec.Timeout = &metav1.Duration{
+			Duration: ksHealthTimeout,
+		}
+	}
+
+	if err := upsertKustomization(ctx, kubeClient, kustomization); err != nil {
+		return err
+	}
+
+	logAction("waiting for kustomization sync")
+	if err := wait.PollImmediate(2*time.Second, timeout,
+		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
+		return err
+	}
+
+	logSuccess("kustomization %s is ready", name)
+
+	namespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}
+	err = kubeClient.Get(ctx, namespacedName, &kustomization)
+	if err != nil {
+		return fmt.Errorf("kustomization sync failed: %w", err)
+	}
+
+	if kustomization.Status.LastAppliedRevision != "" {
+		logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision)
+	} else {
+		return fmt.Errorf("kustomization sync failed")
+	}
+
+	return nil
+}
+
+func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization) error {
+	namespacedName := types.NamespacedName{
+		Namespace: kustomization.GetNamespace(),
+		Name:      kustomization.GetName(),
+	}
+
+	var existing kustomizev1.Kustomization
+	err := kubeClient.Get(ctx, namespacedName, &existing)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			if err := kubeClient.Create(ctx, &kustomization); err != nil {
+				return err
+			} else {
+				logSuccess("kustomization created")
+				return nil
+			}
+		}
+		return err
+	}
+
+	existing.Spec = kustomization.Spec
+	if err := kubeClient.Update(ctx, &existing); err != nil {
+		return err
+	}
+
+	logSuccess("kustomization updated")
+	return nil
+}
+
+func isKustomizationReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc {
+	return func() (bool, error) {
+		var kustomization kustomizev1.Kustomization
+		namespacedName := types.NamespacedName{
+			Namespace: namespace,
+			Name:      name,
+		}
+
+		err := kubeClient.Get(ctx, namespacedName, &kustomization)
+		if err != nil {
+			return false, err
+		}
+
+		for _, condition := range kustomization.Status.Conditions {
+			if condition.Type == sourcev1.ReadyCondition {
+				if condition.Status == corev1.ConditionTrue {
+					return true, nil
+				} else if condition.Status == corev1.ConditionFalse {
+					return false, fmt.Errorf(condition.Message)
+				}
+			}
+		}
+		return false, nil
+	}
+}
diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go
index 1f4ae8dd60374841cd6555dc716da5b2601204e3..2fe5e1943c31ce898395a49cb53bd245ec167c27 100644
--- a/cmd/tk/create_source.go
+++ b/cmd/tk/create_source.go
@@ -22,10 +22,11 @@ import (
 
 var createSourceCmd = &cobra.Command{
 	Use:   "source [name]",
-	Short: "Create source resource",
+	Short: "Create or update a source resource",
 	Long: `
 The create source command generates a source.fluxcd.io resource and waits for it to sync.
-For Git over SSH, host and SSH keys are automatically generated.`,
+For Git over SSH, host and SSH keys are automatically generated and stored in a Kubernetes secret.
+For private Git repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
 	Example: `  # Create a source from a public Git repository master branch
   create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy --git-branch master
 
@@ -131,20 +132,8 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	namespacedName := types.NamespacedName{
-		Namespace: namespace,
-		Name:      name,
-	}
-
-	err = kubeClient.Get(ctx, namespacedName, &gitRepository)
-	if errors.IsNotFound(err) {
-		if err := kubeClient.Create(ctx, &gitRepository); err != nil {
-			return err
-		}
-	} else {
-		if err := kubeClient.Update(ctx, &gitRepository); err != nil {
-			return err
-		}
+	if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil {
+		return err
 	}
 
 	logAction("waiting for source sync")
@@ -155,13 +144,17 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error {
 
 	logSuccess("source %s is ready", name)
 
+	namespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}
 	err = kubeClient.Get(ctx, namespacedName, &gitRepository)
 	if err != nil {
 		return fmt.Errorf("source sync failed: %w", err)
 	}
 
 	if gitRepository.Status.Artifact != nil {
-		logSuccess("revision %s", gitRepository.Status.Artifact.Revision)
+		logSuccess("fetched revision %s", gitRepository.Status.Artifact.Revision)
 	} else {
 		return fmt.Errorf("source sync failed, artifact not found")
 	}
@@ -222,6 +215,35 @@ func generateSSH(ctx context.Context, name, host, tmpDir string) error {
 	return nil
 }
 
+func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepository sourcev1.GitRepository) error {
+	namespacedName := types.NamespacedName{
+		Namespace: gitRepository.GetNamespace(),
+		Name:      gitRepository.GetName(),
+	}
+
+	var existing sourcev1.GitRepository
+	err := kubeClient.Get(ctx, namespacedName, &existing)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			if err := kubeClient.Create(ctx, &gitRepository); err != nil {
+				return err
+			} else {
+				logSuccess("source created")
+				return nil
+			}
+		}
+		return err
+	}
+
+	existing.Spec = gitRepository.Spec
+	if err := kubeClient.Update(ctx, &existing); err != nil {
+		return err
+	}
+
+	logSuccess("source updated")
+	return nil
+}
+
 func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc {
 	return func() (bool, error) {
 		var gitRepository sourcev1.GitRepository