Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for tracing package types #5252

Merged
merged 14 commits into from
Jan 26, 2024
188 changes: 171 additions & 17 deletions cmd/crank/beta/trace/internal/printer/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ import (
"io"
"strings"

gcrname "github.com/google/go-containerregistry/pkg/name"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/printers"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"

pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
"github.com/crossplane/crossplane/cmd/crank/beta/trace/internal/resource"
"github.com/crossplane/crossplane/cmd/crank/beta/trace/internal/resource/xpkg"
)

const (
Expand Down Expand Up @@ -56,20 +61,70 @@ func (r *defaultPrinterRow) String() string {
r.synced,
r.ready,
r.status,
}, "\t") + "\t"
}, "\t")
}

// Print implements the Printer interface by prints the resource tree in a
// human-readable format.
func (p *DefaultPrinter) Print(w io.Writer, root *resource.Resource) error {
tw := printers.GetNewTabWriter(w)
type defaultPkgPrinterRow struct {
wide bool
// wide only fields
// NOTE(phisco): just package is a reserved word
packageImg string

name string
version string
installed string
healthy string
state string
status string
}

headers := defaultPrinterRow{
func (r *defaultPkgPrinterRow) String() string {
cols := []string{
r.name,
}
if r.wide {
cols = append(cols, r.packageImg)
}
cols = append(cols,
r.version,
r.installed,
r.healthy,
r.state,
r.status,
)
return strings.Join(cols, "\t") + "\t"
}

func getHeaders(gk schema.GroupKind, wide bool) (headers fmt.Stringer, isPackageOrPackageRevision bool) {
if xpkg.IsPackageType(gk) || xpkg.IsPackageRevisionType(gk) {
return &defaultPkgPrinterRow{
wide: wide,

name: "NAME",
packageImg: "PACKAGE",
version: "VERSION",
installed: "INSTALLED",
healthy: "HEALTHY",
state: "STATE",
status: "STATUS",
}, true
}
return &defaultPrinterRow{
name: "NAME",
synced: "SYNCED",
ready: "READY",
status: "STATUS",
}
}, false

}

// Print implements the Printer interface by prints the resource tree in a
// human-readable format.
func (p *DefaultPrinter) Print(w io.Writer, root *resource.Resource) error {
tw := printers.GetNewTabWriter(w)

headers, isPackageOrRevision := getHeaders(root.Unstructured.GroupVersionKind().GroupKind(), p.wide)

if _, err := fmt.Fprintln(tw, headers.String()); err != nil {
return errors.Wrap(err, errWriteHeader)
}
Expand Down Expand Up @@ -113,14 +168,14 @@ func (p *DefaultPrinter) Print(w io.Writer, root *resource.Resource) error {
if item.resource.Unstructured.GetNamespace() != "" {
name.WriteString(fmt.Sprintf(" (%s)", item.resource.Unstructured.GetNamespace()))
}
ready, synced, status := getResourceStatus(item.resource, p.wide)

row := defaultPrinterRow{
name: name.String(),
ready: ready,
synced: synced,
status: status,
var row fmt.Stringer
if isPackageOrRevision {
row = getPkgResourceStatus(item.resource, name.String(), p.wide)
} else {
row = getResourceStatus(item.resource, name.String(), p.wide)
}

if _, err := fmt.Fprintln(tw, row.String()); err != nil {
return errors.Wrap(err, errWriteRow)
}
Expand All @@ -141,11 +196,12 @@ func (p *DefaultPrinter) Print(w io.Writer, root *resource.Resource) error {
return nil
}

// getResourceStatus returns the status of the resource.
func getResourceStatus(r *resource.Resource, wide bool) (ready string, synced string, status string) {
// getResourceStatus returns a string that represents an entire row of status
// information for the resource.
func getResourceStatus(r *resource.Resource, name string, wide bool) fmt.Stringer {
readyCond := r.GetCondition(xpv1.TypeReady)
syncedCond := r.GetCondition(xpv1.TypeSynced)
var m string
var status, m string
switch {
case r.Error != nil:
// if there is an error we want to show it
Expand Down Expand Up @@ -181,7 +237,105 @@ func getResourceStatus(r *resource.Resource, wide bool) (ready string, synced st
status = fmt.Sprintf("%s: %s", status, m)
}

return mapEmptyStatusToDash(readyCond.Status), mapEmptyStatusToDash(syncedCond.Status), status
return &defaultPrinterRow{
name: name,
ready: mapEmptyStatusToDash(readyCond.Status),
synced: mapEmptyStatusToDash(syncedCond.Status),
status: status,
}
}

func getPkgResourceStatus(r *resource.Resource, name string, wide bool) fmt.Stringer { //nolint:gocyclo // TODO: just a few switches, not much to do here
var err error
var packageImg, state, status, m string

healthyCond := r.GetCondition(pkgv1.TypeHealthy)
installedCond := r.GetCondition(pkgv1.TypeInstalled)

gk := r.Unstructured.GroupVersionKind().GroupKind()
switch {
case r.Error != nil:
// if there is an error we want to show it, regardless of what type this
// resource is and what conditions it has
status = "Error"
m = r.Error.Error()
case xpkg.IsPackageType(gk):
switch {
case healthyCond.Status == corev1.ConditionTrue && installedCond.Status == corev1.ConditionTrue:
// if both are true we want to show the healthy reason only
status = string(healthyCond.Reason)

// The following cases are for when one of the conditions is false,
// prioritizing installed over healthy in case of issues.
case installedCond.Status == corev1.ConditionFalse:
status = string(installedCond.Reason)
m = installedCond.Message
case healthyCond.Status == corev1.ConditionFalse:
status = string(healthyCond.Reason)
m = healthyCond.Message
default:
// both are unknown or unset, let's try showing the installed reason
status = string(installedCond.Reason)
m = installedCond.Message
}

if packageImg, err = fieldpath.Pave(r.Unstructured.Object).GetString("spec.package"); err != nil {
state = err.Error()
}
case xpkg.IsPackageRevisionType(gk):
// package revisions only have the healthy condition, so use that
status = string(healthyCond.Reason)
m = healthyCond.Message

// Get the state (active vs. inactive) of this package revision.
var err error
state, err = fieldpath.Pave(r.Unstructured.Object).GetString("spec.desiredState")
if err != nil {
state = err.Error()
}
// Get the image used.
if packageImg, err = fieldpath.Pave(r.Unstructured.Object).GetString("spec.image"); err != nil {
state = err.Error()
}
case xpkg.IsPackageRuntimeConfigType(gk):
// nothing to do here
default:
status = "Unknown package type"
}

// Crop the message to the last 64 characters if it's too long and we are
// not in wide mode
if !wide && len(m) > 64 {
m = "..." + m[len(m)-64:]
}
lsviben marked this conversation as resolved.
Show resolved Hide resolved

// Append the message to the status if it's not empty
if m != "" {
status = fmt.Sprintf("%s: %s", status, m)
}

// Parse the image reference extracting the tag, we'll leave it empty if we
// couldn't parse it and leave the whole thing as package instead.
var packageImgTag string
if tag, err := gcrname.NewTag(packageImg); err == nil {
packageImgTag = tag.TagStr()
packageImg = tag.RepositoryStr()
if tag.RegistryStr() != "" {
packageImg = fmt.Sprintf("%s/%s", tag.RegistryStr(), packageImg)
}
}

return &defaultPkgPrinterRow{
wide: wide,

name: name,
packageImg: packageImg,
version: packageImgTag,
installed: mapEmptyStatusToDash(installedCond.Status),
healthy: mapEmptyStatusToDash(healthyCond.Status),
state: mapEmptyStatusToDash(corev1.ConditionStatus(state)),
status: status,
}
}

func mapEmptyStatusToDash(s corev1.ConditionStatus) string {
Expand Down
39 changes: 29 additions & 10 deletions cmd/crank/beta/trace/internal/printer/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,36 @@ func TestDefaultPrinter(t *testing.T) {
resource: GetComplexResource(),
},
want: want{
// Note: Use spaces instead of tabs for intendation
// Note: Use spaces instead of tabs for indentation
output: `
NAME SYNCED READY STATUS
ObjectStorage/test-resource (default) True True
└─ XObjectStorage/test-resource-hash True True
├─ Bucket/test-resource-bucket-hash True True
│ ├─ User/test-resource-child-1-bucket-hash True False SomethingWrongHappened: Error with bucket child 1
│ ├─ User/test-resource-child-mid-bucket-hash False True CantSync: Sync error with bucket child mid
│ └─ User/test-resource-child-2-bucket-hash True False SomethingWrongHappened: Error with bucket child 2
│ └─ User/test-resource-child-2-1-bucket-hash True -
└─ User/test-resource-user-hash Unknown True
NAME SYNCED READY STATUS
ObjectStorage/test-resource (default) True True
└─ XObjectStorage/test-resource-hash True True
├─ Bucket/test-resource-bucket-hash True True
│ ├─ User/test-resource-child-1-bucket-hash True False SomethingWrongHappened: Error with bucket child 1
│ ├─ User/test-resource-child-mid-bucket-hash False True CantSync: Sync error with bucket child mid
│ └─ User/test-resource-child-2-bucket-hash True False SomethingWrongHappened: Error with bucket child 2
│ └─ User/test-resource-child-2-1-bucket-hash True -
└─ User/test-resource-user-hash Unknown True
`,
err: nil,
},
},
"PackageWithChildren": {
reason: "Should print a complex Package with children.",
args: args{
resource: GetComplexPackage(),
},
want: want{
// Note: Use spaces instead of tabs for indentation
output: `
NAME VERSION INSTALLED HEALTHY STATE STATUS
Configuration/platform-ref-aws v0.9.0 True True - HealthyPackageRevision
├─ ConfigurationRevision/platform-ref-aws-9ad7b5db2899 v0.9.0 True True Active HealthyPackageRevision
└─ Configuration/upbound-configuration-aws-network upbound-configuration-aws-network v0.7.0 True True - HealthyPackageRevision
├─ ConfigurationRevision/upbound-configuration-aws-network-97be9100cfe1 v0.7.0 True True Active HealthyPackageRevision
└─ Provider/upbound-provider-aws-ec2 v0.47.0 True False - UnhealthyPackageRevision: ...ider package deployment has no condition of type "Available" yet
└─ ProviderRevision/upbound-provider-aws-ec2-9ad7b5db2899 v0.47.0 True False Active UnhealthyPackageRevision: ...ider package deployment has no condition of type "Available" yet
`,
err: nil,
},
Expand Down
85 changes: 77 additions & 8 deletions cmd/crank/beta/trace/internal/printer/dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import (
"github.com/pkg/errors"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"

v1 "github.com/crossplane/crossplane/apis/pkg/v1"
"github.com/crossplane/crossplane/cmd/crank/beta/trace/internal/resource"
"github.com/crossplane/crossplane/cmd/crank/beta/trace/internal/resource/xpkg"
)

// DotPrinter defines the DotPrinter configuration
Expand Down Expand Up @@ -50,6 +53,43 @@ func (r *dotLabel) String() string {
return strings.Join(out, "\n") + "\n"
}

type dotPackageLabel struct {
apiVersion string
name string
pkg string
installed string
healthy string
state string
error string
}

func (r *dotPackageLabel) String() string {
out := []string{
"Name: " + r.name,
"ApiVersion: " + r.apiVersion,
"Package: " + r.pkg,
}
if r.installed != "" {
out = append(out,
"Installed: "+r.installed)
}
out = append(out,
"Healthy: "+r.healthy,
)
if r.state != "" {
out = append(out,
"State: "+r.state,
)
}

if r.error != "" {
out = append(out,
"Error: "+r.error,
)
}
return strings.Join(out, "\n") + "\n"
}

// Print gets all the nodes and then return the graph as a dot format string to the Writer.
func (p *DotPrinter) Print(w io.Writer, root *resource.Resource) error {
g := dot.NewGraph(dot.Undirected)
Expand All @@ -73,15 +113,44 @@ func (p *DotPrinter) Print(w io.Writer, root *resource.Resource) error {
if item.parent != nil {
g.Edge(*item.parent, node)
}

label := &dotLabel{
namespace: item.resource.Unstructured.GetNamespace(),
apiVersion: item.resource.Unstructured.GetObjectKind().GroupVersionKind().GroupVersion().String(),
name: fmt.Sprintf("%s/%s", item.resource.Unstructured.GetKind(), item.resource.Unstructured.GetName()),
ready: string(item.resource.GetCondition(xpv1.TypeReady).Status),
synced: string(item.resource.GetCondition(xpv1.TypeSynced).Status),
var label fmt.Stringer
gk := item.resource.Unstructured.GroupVersionKind().GroupKind()
switch {
case xpkg.IsPackageType(gk):
pkg, err := fieldpath.Pave(item.resource.Unstructured.Object).GetString("spec.package")
l := &dotPackageLabel{
apiVersion: item.resource.Unstructured.GroupVersionKind().GroupVersion().String(),
name: item.resource.Unstructured.GetName(),
pkg: pkg,
installed: string(item.resource.GetCondition(v1.TypeInstalled).Status),
healthy: string(item.resource.GetCondition(v1.TypeHealthy).Status),
}
if err != nil {
l.error = err.Error()
}
label = l
case xpkg.IsPackageRevisionType(gk):
pkg, err := fieldpath.Pave(item.resource.Unstructured.Object).GetString("spec.image")
l := &dotPackageLabel{
apiVersion: item.resource.Unstructured.GroupVersionKind().GroupVersion().String(),
name: item.resource.Unstructured.GetName(),
pkg: pkg,
healthy: string(item.resource.GetCondition(v1.TypeHealthy).Status),
state: string(item.resource.GetCondition(v1.TypeHealthy).Reason),
}
if err != nil {
l.error = err.Error()
}
label = l
default:
label = &dotLabel{
namespace: item.resource.Unstructured.GetNamespace(),
apiVersion: item.resource.Unstructured.GetObjectKind().GroupVersionKind().GroupVersion().String(),
name: fmt.Sprintf("%s/%s", item.resource.Unstructured.GetKind(), item.resource.Unstructured.GetName()),
ready: string(item.resource.GetCondition(xpv1.TypeReady).Status),
synced: string(item.resource.GetCondition(xpv1.TypeSynced).Status),
}
}

node.Label(label.String())
node.Attr("penwidth", "2")

Expand Down