Repository: GoogleContainerTools/kpt
Pull request #3473 (2022-08-19 01:23:16)

Add flag to repo register command for creating package branch


Patch:
diff --git a/go.mod b/go.mod
index c47c5136..3d642ce8 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/GoogleContainerTools/kpt
 go 1.18
 
 require (
-	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220707151439-cb4c4e496c92
+	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee
 	github.com/bytecodealliance/wasmtime-go v0.39.0
 	github.com/cpuguy83/go-md2man/v2 v2.0.2
 	github.com/go-errors/errors v1.4.2
diff --git a/go.sum b/go.sum
index 00189ddf..8a4c4756 100644
--- a/go.sum
+++ b/go.sum
@@ -85,8 +85,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
-github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220707151439-cb4c4e496c92 h1:0h6lonywmlgUWEc62Nvjt3IqkzudXDbplILJc7wlayI=
-github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220707151439-cb4c4e496c92/go.mod h1:51Vk7QZ+XUzHCvQBi7t9tiWqTXvy6T13cv/inUXJJ0s=
+github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee h1:FSW33xqr8SeoZWQ0MgKtjosMmaUBoHa/z0Tlp493528=
+github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee/go.mod h1:VUrDv94BkVOEE/MEwwqVr7C1K0sWxF/QsoIZvpQacnA=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
diff --git a/internal/alpha/printers/table/printer.go b/internal/alpha/printers/table/printer.go
index 29ee4c52..5798bdd3 100644
--- a/internal/alpha/printers/table/printer.go
+++ b/internal/alpha/printers/table/printer.go
@@ -6,11 +6,14 @@ package table
 import (
 	"fmt"
 	"io"
+	"strings"
 	"time"
 
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"sigs.k8s.io/cli-utils/pkg/apply/event"
 	"sigs.k8s.io/cli-utils/pkg/common"
+	pollingevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
 	printcommon "sigs.k8s.io/cli-utils/pkg/print/common"
 	"sigs.k8s.io/cli-utils/pkg/print/table"
 )
@@ -72,87 +75,23 @@ func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy, _ bool)
 }
 
 // columns defines the columns we want to print
-//TODO: We should have the number of columns and their widths be
+// TODO: We should have the number of columns and their widths be
 // dependent on the space available.
 var (
-	actionColumnDef = table.ColumnDef{
-		// Column containing the resource type and name. Currently it does not
-		// print group or version since those are rarely needed to uniquely
-		// distinguish two resources from each other. Just name and kind should
-		// be enough in almost all cases and saves space in the output.
-		ColumnName:   "action",
-		ColumnHeader: "ACTION",
-		ColumnWidth:  12,
-		PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int,
-			error) {
-			var resInfo *resourceInfo
-			switch res := r.(type) {
-			case *resourceInfo:
-				resInfo = res
-			default:
-				return 0, nil
-			}
-
-			var text string
-			switch resInfo.ResourceAction {
-			case event.ApplyAction:
-				if resInfo.ApplyStatus != event.ApplyFailed {
-					text = resInfo.ApplyStatus.String()
-				}
-			case event.PruneAction:
-				if resInfo.PruneStatus != event.PruneFailed {
-					text = resInfo.PruneStatus.String()
-				}
-			}
-
-			if len(text) > width {
-				text = text[:width]
-			}
-			_, err := fmt.Fprint(w, text)
-			return len(text), err
-		},
-	}
-
-	reconciledColumnDef = table.ColumnDef{
-		// Column containing the reconciliation status.
-		ColumnName:   "reconciled",
-		ColumnHeader: "RECONCILED",
-		ColumnWidth:  10,
-		PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (
-			int,
-			error,
-		) {
-			var resInfo *resourceInfo
-			switch res := r.(type) {
-			case *resourceInfo:
-				resInfo = res
-			default:
-				return 0, nil
-			}
-
-			var text string
-			switch resInfo.ResourceAction {
-			case event.WaitAction:
-				text = resInfo.WaitStatus.String()
-			}
-
-			if len(text) > width {
-				text = text[:width]
-			}
-			_, err := fmt.Fprint(w, text)
-			return len(text), err
-		},
+	unifiedStatusColumnDef = table.ColumnDef{
+		// Column containing the overall progress.
+		ColumnName:        "progress",
+		ColumnHeader:      "PROGRESS",
+		ColumnWidth:       80,
+		PrintResourceFunc: printProgress,
 	}
 
-	columns = []table.ColumnDefinition{
+	alphaColumns = []table.ColumnDefinition{
 		table.MustColumn("namespace"),
 		table.MustColumn("resource"),
-		actionColumnDef,
-		table.MustColumn("status"),
-		reconciledColumnDef,
-		table.MustColumn("conditions"),
-		table.MustColumn("age"),
-		table.MustColumn("message"),
+
+		// We are trying out a "single column" model here
+		unifiedStatusColumnDef,
 	}
 )
 
@@ -163,7 +102,7 @@ func (t *Printer) runPrintLoop(coll *resourceStateCollector, stop chan struct{})
 
 	baseTablePrinter := table.BaseTablePrinter{
 		IOStreams: t.IOStreams,
-		Columns:   columns,
+		Columns:   alphaColumns,
 	}
 
 	linesPrinted := baseTablePrinter.PrintTable(coll.LatestState(), 0)
@@ -187,3 +126,147 @@ func (t *Printer) runPrintLoop(coll *resourceStateCollector, stop chan struct{})
 	}()
 	return finished
 }
+
+func printProgress(w io.Writer, width int, r table.Resource) (int, error) {
+	var resInfo *resourceInfo
+	switch res := r.(type) {
+	case *resourceInfo:
+		resInfo = res
+	default:
+		return 0, fmt.Errorf("unexpected type %T", r)
+	}
+
+	text, details, err := getProgress(resInfo)
+	if err != nil {
+		return 0, err
+	}
+
+	if details != "" {
+		text += " " + details
+	}
+
+	if len(text) > width {
+		text = text[:width]
+	}
+	n, err := fmt.Fprint(w, text)
+	if err != nil {
+		return n, err
+	}
+	return len(text), err
+}
+
+func getProgress(resInfo *resourceInfo) (string, string, error) {
+	printStatus := false
+	var text string
+	var details string
+	switch resInfo.ResourceAction {
+	case event.ApplyAction:
+		switch resInfo.ApplyStatus {
+		case event.ApplyPending:
+			text = "PendingApply"
+		case event.ApplySuccessful:
+			text = "Applied"
+			printStatus = true
+		case event.ApplySkipped:
+			text = "SkippedApply"
+		case event.ApplyFailed:
+			text = "ApplyFailed"
+
+		default:
+			return "", "", fmt.Errorf("unknown ApplyStatus: %v", resInfo.ApplyStatus)
+		}
+
+	case event.PruneAction:
+		switch resInfo.PruneStatus {
+		case event.PrunePending:
+			text = "PendingDeletion"
+		case event.PruneSuccessful:
+			text = "Deleted"
+		case event.PruneSkipped:
+			text = "DeletionSkipped"
+		case event.PruneFailed:
+			text = "DeletionFailed"
+
+		default:
+			return "", "", fmt.Errorf("unknown PruneStatus: %v", resInfo.PruneStatus)
+		}
+
+	default:
+		return "", "", fmt.Errorf("unknown ResourceAction %v", resInfo.ResourceAction)
+	}
+
+	rs := resInfo.ResourceStatus()
+	if printStatus && rs != nil {
+		s := rs.Status.String()
+
+		color, setColor := printcommon.ColorForStatus(rs.Status)
+		if setColor {
+			s = printcommon.SprintfWithColor(color, s)
+		}
+
+		text = s
+
+		if resInfo.ResourceAction == event.WaitAction {
+			text += " WaitStatus:" + resInfo.WaitStatus.String()
+		}
+
+		conditionStrings := getConditions(rs)
+		text += " Conditions:" + strings.Join(conditionStrings, ",")
+
+		var message string
+		if rs.Error != nil {
+			message = rs.Error.Error()
+		} else {
+			message = rs.Message
+		}
+		// TODO: Map from status?
+		if message == "Resource is Ready" || message == "Resource is current" {
+			message = ""
+		}
+		if message != "" {
+			details += "Message:" + message
+		}
+	}
+
+	return text, details, nil
+}
+
+func getConditions(rs *pollingevent.ResourceStatus) []string {
+	u := rs.Resource
+	if u == nil {
+		return nil
+	}
+
+	// TODO: Should we be using kstatus here?
+
+	conditions, found, err := unstructured.NestedSlice(u.Object,
+		"status", "conditions")
+	if !found || err != nil || len(conditions) == 0 {
+		return nil
+	}
+
+	var conditionStrings []string
+	for _, cond := range conditions {
+		condition := cond.(map[string]interface{})
+		conditionType := condition["type"].(string)
+		conditionStatus := condition["status"].(string)
+		conditionReason := condition["reason"].(string)
+		var color printcommon.Color
+		switch conditionStatus {
+		case "True":
+			color = printcommon.GREEN
+		case "False":
+			color = printcommon.RED
+		default:
+			color = printcommon.YELLOW
+		}
+
+		text := conditionReason
+		if text == "" {
+			text = conditionType
+		}
+		s := printcommon.SprintfWithColor(color, text)
+		conditionStrings = append(conditionStrings, s)
+	}
+	return conditionStrings
+}
diff --git a/internal/cmdreporeg/command.go b/internal/cmdreporeg/command.go
index c523089a..2d908007 100644
--- a/internal/cmdreporeg/command.go
+++ b/internal/cmdreporeg/command.go
@@ -53,6 +53,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 
 	c.Flags().StringVar(&r.directory, "directory", "/", "Directory within the repository where to look for packages.")
 	c.Flags().StringVar(&r.branch, "branch", "main", "Branch in the repository where finalized packages are committed.")
+	c.Flags().BoolVar(&r.createBranch, "create-branch", false, "Create the package branch if it doesn't already exist.")
 	c.Flags().StringVar(&r.name, "name", "", "Name of the package repository. If unspecified, will use the name portion (last segment) of the repository URL.")
 	c.Flags().StringVar(&r.description, "description", "", "Brief description of the package repository.")
 	c.Flags().BoolVar(&r.deployment, "deployment", false, "Repository is a deployment repository; packages in a deployment repository are considered deployment-ready.")
@@ -76,6 +77,7 @@ type runner struct {
 	// Flags
 	directory        string
 	branch           string
+	createBranch     bool
 	name             string
 	description      string
 	deployment       bool
@@ -123,9 +125,10 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		// 	return errors.E(op, err)
 		// }
 		git = &configapi.GitRepository{
-			Repo:      repository,
-			Branch:    r.branch,
-			Directory: r.directory,
+			Repo:         repository,
+			Branch:       r.branch,
+			CreateBranch: r.createBranch,
+			Directory:    r.directory,
 		}
 
 		if r.name == "" {
diff --git a/pkg/status/poller.go b/pkg/status/poller.go
index 529d805b..2d5ae290 100644
--- a/pkg/status/poller.go
+++ b/pkg/status/poller.go
@@ -36,6 +36,9 @@ func NewStatusPoller(f util.Factory) (*polling.StatusPoller, error) {
 			&ConfigConnectorStatusReader{
 				Mapper: mapper,
 			},
+			&RolloutStatusReader{
+				Mapper: mapper,
+			},
 		},
 	})
 }
@@ -55,7 +58,10 @@ func NewStatusWatcher(f util.Factory) (watcher.StatusWatcher, error) {
 		DynamicClient: dynamicClient,
 		Mapper:        mapper,
 		ResyncPeriod:  1 * time.Hour,
-		StatusReader:  statusreaders.NewStatusReader(mapper, NewConfigConnectorStatusReader(mapper)),
+		StatusReader: statusreaders.NewStatusReader(
+			mapper,
+			NewConfigConnectorStatusReader(mapper),
+			NewRolloutStatusReader(mapper)),
 		ClusterReader: &clusterreader.DynamicClusterReader{
 			DynamicClient: dynamicClient,
 			Mapper:        mapper,
diff --git a/pkg/status/rollout.go b/pkg/status/rollout.go
new file mode 100644
index 00000000..a8952f0a
--- /dev/null
+++ b/pkg/status/rollout.go
@@ -0,0 +1,230 @@
+// Copyright 2022 Google LLC
+//
+// 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 status
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	"sigs.k8s.io/yaml"
+)
+
+const (
+	ArgoGroup   = "argoproj.io"
+	Rollout     = "Rollout"
+	Degraded    = "Degraded"
+	Failed      = "Failed"
+	Healthy     = "Healthy"
+	Paused      = "Paused"
+	Progressing = "Progressing"
+)
+
+type RolloutStatusReader struct {
+	Mapper meta.RESTMapper
+}
+
+func NewRolloutStatusReader(mapper meta.RESTMapper) engine.StatusReader {
+	return &RolloutStatusReader{
+		Mapper: mapper,
+	}
+}
+
+var _ engine.StatusReader = &RolloutStatusReader{}
+
+// Supports returns true for all rollout resources.
+func (r *RolloutStatusReader) Supports(gk schema.GroupKind) bool {
+	return gk.Group == ArgoGroup && gk.Kind == Rollout
+}
+
+func (r *RolloutStatusReader) Compute(u *unstructured.Unstructured) (*status.Result, error) {
+	result := status.Result{
+		Status:     status.UnknownStatus,
+		Message:    status.GetStringField(u.Object, ".status.message", ""),
+		Conditions: make([]status.Condition, 0),
+	}
+	// ensure that the meta generation is observed
+	generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
+	if err != nil {
+		return &result, fmt.Errorf("looking up metadata.generation from resource: %w", err)
+	}
+	if !found {
+		return &result, fmt.Errorf("metadata.generation not found")
+	}
+
+	// Argo Rollouts defines the observedGeneration field in the Rollout object as a string
+	// so read it as a string here
+	observedGenerationString, found, err := unstructured.NestedString(u.Object, "status", "observedGeneration")
+	if err != nil {
+		return &result, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
+	}
+	if !found {
+		// We know that Rollout resources uses the ObservedGeneration pattern, so consider it
+		// an error if it is not found.
+		return &result, fmt.Errorf("status.ObservedGeneration not found")
+	}
+	// If no errors detected and the field is found
+	// Parse it to become an integer
+	observedGeneration, err := strconv.ParseInt(observedGenerationString, 10, 64)
+	if err != nil {
+		return &result, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
+	}
+
+	if generation != observedGeneration {
+		msg := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
+		result.Status = status.InProgressStatus
+		result.Message = msg
+		return &result, nil
+	}
+
+	phase, phaseFound, err := unstructured.NestedString(u.Object, "status", "phase")
+	if err != nil {
+		return &result, fmt.Errorf("looking up status.phase from resource: %w", err)
+	}
+	if !phaseFound {
+		// We know that Rollout resources uses the phase pattern, so consider it
+		// an error if it is not found.
+		return &result, fmt.Errorf("status.phase not found")
+	}
+
+	conditions, condFound, err := unstructured.NestedSlice(u.Object, "status", "conditions")
+	if err != nil {
+		return &result, fmt.Errorf("looking up status.conditions from resource: %w", err)
+	}
+	if condFound {
+		data, err := yaml.Marshal(conditions)
+		if err != nil {
+			return &result, fmt.Errorf("failed to marshal conditions for %s/%s", u.GetNamespace(), u.GetName())
+		}
+		err = yaml.Unmarshal(data, &result.Conditions)
+		if err != nil {
+			return &result, fmt.Errorf("failed to unmarshal conditions for %s/%s", u.GetNamespace(), u.GetName())
+		}
+	}
+
+	specReplicas := status.GetIntField(u.Object, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
+	statusReplicas := status.GetIntField(u.Object, ".status.replicas", 0)
+	updatedReplicas := status.GetIntField(u.Object, ".status.updatedReplicas", 0)
+	readyReplicas := status.GetIntField(u.Object, ".status.readyReplicas", 0)
+	availableReplicas := status.GetIntField(u.Object, ".status.availableReplicas", 0)
+
+	if specReplicas > statusReplicas {
+		message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas)
+		result.Status = status.InProgressStatus
+		result.Message = message
+
+		return &result, nil
+	}
+
+	if statusReplicas > specReplicas {
+		message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
+		result.Status = status.InProgressStatus
+		result.Message = message
+		return &result, nil
+	}
+
+	if updatedReplicas > availableReplicas {
+		message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas)
+		result.Status = status.InProgressStatus
+		result.Message = message
+		return &result, nil
+	}
+
+	if specReplicas > readyReplicas {
+		message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
+		result.Status = status.InProgressStatus
+		result.Message = message
+		return &result, nil
+	}
+
+	message := status.GetStringField(u.Object, ".status.message", "")
+	if message != "" {
+		message += " "
+	}
+	message += fmt.Sprintf("Ready Replicas: %d, Updated Replicas: %d", readyReplicas, updatedReplicas)
+	result.Message = message
+
+	switch phase {
+	case Degraded, Failed:
+		result.Status = status.FailedStatus
+	case Healthy:
+		result.Status = status.CurrentStatus
+	case Paused, Progressing:
+		result.Status = status.InProgressStatus
+	default:
+		// Undefined status
+		result.Status = status.UnknownStatus
+	}
+	return &result, nil
+}
+
+func (r *RolloutStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, id object.ObjMetadata) (
+	*event.ResourceStatus, error) {
+	gvk, err := toGVK(id.GroupKind, r.Mapper)
+	if err != nil {
+		return newUnknownResourceStatus(id, nil, err), nil
+	}
+
+	key := types.NamespacedName{
+		Name:      id.Name,
+		Namespace: id.Namespace,
+	}
+
+	var u unstructured.Unstructured
+	u.SetGroupVersionKind(gvk)
+	err = reader.Get(ctx, key, &u)
+	if err != nil {
+		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+			return nil, err
+		}
+		if apierrors.IsNotFound(err) {
+			return newResourceStatus(id, status.NotFoundStatus, &u, "Resource not found"), nil
+		}
+		return newUnknownResourceStatus(id, nil, err), nil
+	}
+
+	return r.ReadStatusForObject(ctx, reader, &u)
+}
+
+func (r *RolloutStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, u *unstructured.Unstructured) (
+	*event.ResourceStatus, error) {
+	id := object.UnstructuredToObjMetadata(u)
+
+	// First check if the resource is in the process of being deleted.
+	deletionTimestamp, found, err := unstructured.NestedString(u.Object, "metadata", "deletionTimestamp")
+	if err != nil {
+		return newUnknownResourceStatus(id, u, err), nil
+	}
+	if found && deletionTimestamp != "" {
+		return newResourceStatus(id, status.TerminatingStatus, u, "Resource scheduled for deletion"), nil
+	}
+
+	res, err := r.Compute(u)
+	if err != nil {
+		return newUnknownResourceStatus(id, u, err), nil
+	}
+
+	return newResourceStatus(id, res.Status, u, res.Message), nil
+}
diff --git a/porch/Makefile b/porch/Makefile
index 3ad8ea7f..5683477c 100644
--- a/porch/Makefile
+++ b/porch/Makefile
@@ -46,6 +46,8 @@ PORCH_CONTROLLERS_IMAGE ?= porch-controllers
 PORCH_WRAPPER_SERVER_IMAGE ?= porch-wrapper-server
 TEST_GIT_SERVER_IMAGE ?= test-git-server
 
+.DEFAULT_GOAL := all
+
 "$(MYGOBIN)/stringer":
 	go install golang.org/x/tools/cmd/stringer@v0.1.12
 
diff --git a/porch/go.mod b/porch/go.mod
index 05edbda1..bcf6c4fe 100644
--- a/porch/go.mod
+++ b/porch/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/GoogleContainerTools/kpt-functions-catalog/functions/go/set-namespace v0.4.1
 	github.com/GoogleContainerTools/kpt-functions-catalog/functions/go/starlark v0.4.3
 	github.com/GoogleContainerTools/kpt-functions-sdk/go/fn v0.0.0-20220506190241-f85503febd54
-	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220707151439-cb4c4e496c92
+	github.com/GoogleContainerTools/kpt/porch/api v0.0.0-20220821193112-4792e5fa18ee
 	github.com/bluekeyes/go-gitdiff v0.6.1
 	github.com/go-git/go-billy/v5 v5.3.1
 	github.com/go-git/go-git/v5 v5.4.3-0.20220408232334-4f916225cb2f


Test patch:
diff --git a/internal/cmdreporeg/command_test.go b/internal/cmdreporeg/command_test.go
index 7d077ff5..67e28c82 100644
--- a/internal/cmdreporeg/command_test.go
+++ b/internal/cmdreporeg/command_test.go
@@ -100,6 +100,7 @@ func TestRepoReg(t *testing.T) {
 				"--deployment",
 				"--directory=/catalog",
 				"--branch=main-branch",
+				"--create-branch",
 				"--namespace=repository-namespace",
 			},
 			actions: []httpAction{
diff --git a/internal/cmdreporeg/testdata/full-repository.yaml b/internal/cmdreporeg/testdata/full-repository.yaml
index 486034f4..a3650ae6 100644
--- a/internal/cmdreporeg/testdata/full-repository.yaml
+++ b/internal/cmdreporeg/testdata/full-repository.yaml
@@ -10,6 +10,7 @@ spec:
     description: '"Test Repository Description"'
     git:
         branch: main-branch
+        createBranch: true
         directory: /catalog
         repo: https://github.com/platkrm/test-blueprints.git
         secretRef:
diff --git a/pkg/status/rollout_test.go b/pkg/status/rollout_test.go
new file mode 100644
index 00000000..de66b959
--- /dev/null
+++ b/pkg/status/rollout_test.go
@@ -0,0 +1,339 @@
+// Copyright 2021 Google LLC
+//
+// 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 status
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	fakemapper "sigs.k8s.io/cli-utils/pkg/testutil"
+)
+
+func TestRolloutSupports(t *testing.T) {
+	testCases := map[string]struct {
+		gk       schema.GroupKind
+		supports bool
+	}{
+		"matches rollout group": {
+			gk: schema.GroupKind{
+				Group: "argoproj.io",
+				Kind:  "Rollout",
+			},
+			supports: true,
+		},
+		"doesn't match other resources": {
+			gk: schema.GroupKind{
+				Group: "apps",
+				Kind:  "StatefulSet",
+			},
+			supports: false,
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			fakeMapper := fakemapper.NewFakeRESTMapper()
+
+			statusReader := &RolloutStatusReader{
+				Mapper: fakeMapper,
+			}
+
+			supports := statusReader.Supports(tc.gk)
+
+			assert.Equal(t, tc.supports, supports)
+		})
+	}
+}
+
+func TestRolloutReadStatus(t *testing.T) {
+	testCases := map[string]struct {
+		resource       string
+		gvk            schema.GroupVersionKind
+		expectedStatus status.Status
+		expectedMsg    string
+	}{
+		"Resource where observedGeneration doesn't match generation is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+status:
+  observedGeneration: "41"
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Version: "v1alpha1",
+				Kind:    "Rollout",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Rollout generation is 42, but latest observed generation is 41",
+		},
+		"Resource when spec.replicas more than status.replicas is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 1
+  phase: Progressing
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "replicas: 1/2",
+		},
+		"Resource when status.replicas more than spec.replicas is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 1
+status:
+  observedGeneration: "42"
+  replicas: 2
+  phase: Progressing
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Pending termination: 1",
+		},
+		"Resource when status.updatedReplicas more than spec.availableReplicas is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 1
+  phase: Progressing
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Available: 1/2",
+		},
+		"Resource when spec.replicas more than spec.readyReplicas is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 1
+  phase: Progressing
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Ready: 1/2",
+		},
+		"Resource when status.phase is Degraded is Failed": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 2
+  phase: Degraded
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.FailedStatus,
+			expectedMsg:    "Ready Replicas: 2, Updated Replicas: 2",
+		},
+		"Resource when status.phase is Failed is Failed": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 2
+  phase: Failed
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.FailedStatus,
+			expectedMsg:    "Ready Replicas: 2, Updated Replicas: 2",
+		},
+		"Resource when status.phase is Healthy is Current": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 2
+  phase: Healthy
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.CurrentStatus,
+			expectedMsg:    "Ready Replicas: 2, Updated Replicas: 2",
+		},
+		"Resource when status.phase is Paused is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 2
+  phase: Paused
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Ready Replicas: 2, Updated Replicas: 2",
+		},
+		"Resource when status.phase is Progressing is InProgress": {
+			resource: `
+apiVersion: argoproj.io/v1alpha1
+kind: Rollout
+metadata:
+  name: pubsub.googleapis.com
+  namespace: cnrm
+  generation: 42
+spec:
+  replicas: 2
+status:
+  observedGeneration: "42"
+  replicas: 2
+  updatedReplicas: 2
+  availableReplicas: 2
+  readyReplicas: 2
+  phase: Progressing
+`,
+			gvk: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Kind:    "Rollout",
+				Version: "v1alpha1",
+			},
+			expectedStatus: status.InProgressStatus,
+			expectedMsg:    "Ready Replicas: 2, Updated Replicas: 2",
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			obj := testutil.YamlToUnstructured(t, tc.resource)
+
+			fakeClusterReader := &fakeClusterReader{
+				getResource: obj,
+			}
+
+			fakeMapper := fakemapper.NewFakeRESTMapper(tc.gvk)
+			statusReader := &RolloutStatusReader{
+				Mapper: fakeMapper,
+			}
+
+			res, err := statusReader.ReadStatus(context.Background(), fakeClusterReader, object.UnstructuredToObjMetadata(obj))
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expectedStatus, res.Status)
+			assert.Equal(t, tc.expectedMsg, res.Message)
+		})
+	}
+}