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

Add ttr, the time-to-readiness reporting tool for managed resources #71

Merged
merged 1 commit into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PLATFORMS ?= linux_amd64 linux_arm64 darwin_amd64 darwin_arm64
# Setup Go
GO_REQUIRED_VERSION = 1.19
GOLANGCILINT_VERSION ?= 1.50.0
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/uptest $(GO_PROJECT)/cmd/updoc
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/uptest $(GO_PROJECT)/cmd/updoc $(GO_PROJECT)/cmd/ttr
GO_LDFLAGS += -X $(GO_PROJECT)/internal/version.Version=$(VERSION)
GO_SUBDIRS += cmd internal
GO111MODULE = on
Expand Down
86 changes: 86 additions & 0 deletions cmd/ttr/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2023 Upbound Inc.
//
// 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.

// main package for the ttr tool, which reports the time-to-readiness
// measurements for all managed resources in a cluster.

package main

import (
"regexp"
"strings"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
)

type filter struct {
gvk schema.GroupVersionKind
name *regexp.Regexp
}

func (f filter) matchGVK(gvk schema.GroupVersionKind) bool {
return (len(f.gvk.Group) == 0 || f.gvk.Group == gvk.Group) &&
(len(f.gvk.Version) == 0 || f.gvk.Version == gvk.Version) &&
(len(f.gvk.Kind) == 0 || f.gvk.Kind == gvk.Kind)
}

type filters []*filter

func (f filters) match(gvk schema.GroupVersionKind, name string) bool {
if len(f) == 0 {
return true
}
for _, e := range f {
if e.matchGVK(gvk) && (len(name) == 0 || e.name == nil || e.name.MatchString(name)) {
return true
}
}
return false
}

func parseFilter(f string) (*filter, error) {
tokens := strings.Split(f, "/")
if len(tokens) != 4 {
return nil, errors.Errorf("invalid filter string: %s", f)
}
var re *regexp.Regexp
if len(tokens[3]) != 0 {
r, err := regexp.Compile(tokens[3])
if err != nil {
return nil, errors.Wrapf(err, "invalid name regex expression: %s", tokens[3])
}
re = r
}
return &filter{
gvk: schema.GroupVersionKind{
Group: tokens[0],
Version: tokens[1],
Kind: tokens[2],
},
name: re,
}, nil
}

func getFilters(f ...string) (filters, error) {
result := make(filters, 0, len(f))
for _, s := range f {
f, err := parseFilter(s)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare filters")
}
result = append(result, f)
}
return result, nil
}
157 changes: 157 additions & 0 deletions cmd/ttr/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2023 Upbound Inc.
//
// 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.

// main package for the ttr tool, which reports the time-to-readiness
// measurements for all managed resources in a cluster.
package main

import (
"context"
"fmt"
"os"

"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/dynamic"

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

// example invocations:
// - ttr -> Report on all managed resources
// - ttr -f cognitoidp.aws.upbound.io/v1beta1/UserPool/example
// - ttr -f //UserPool/ -> Report all UserPool resources
// - ttr -f //UserPool/ -f //VPC/ -> Report all UserPool and VPC resources
// - ttr -f cognitoidp.aws.upbound.io/// -> Report all resources in the group
// - ttr -f ///example-.* -> Report all resources with names prefixed by example-
func main() {
cf := genericclioptions.NewConfigFlags(true)
var filters []string
cmd := &cobra.Command{
Use: "ttr",
Short: "Reports the time-to-readiness measurements for a subset of the managed resources in a Kubernetes cluster",
Example: "ttr --kubeconfig=./kubeconfig",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return report(cf, filters)
},
}
cmd.Flags().StringArrayVarP(&filters, "filters", "f", nil,
"Zero or more filter expressions each with the following syntax: [group]/[version]/[kind]/[name regex]. Can be repeated. "+
"Filters managed resources with the specified APIs and names. Missing entries should be specified as empty strings.")
// add common Kubernetes client configuration flags
cf.AddFlags(cmd.Flags())
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

func report(cf *genericclioptions.ConfigFlags, filters []string) error {
dc, err := cf.ToDiscoveryClient()
if err != nil {
return errors.Wrap(err, "failed to initialize the Kubernetes discovery client")
}
c, err := cf.ToRESTConfig()
if err != nil {
return errors.Wrap(err, "failed to get REST config for the cluster")
}
dyn, err := dynamic.NewForConfig(c)
if err != nil {
return errors.Wrap(err, "failed to initialize a dynamic Kubernetes client")
}
_, rlList, err := dc.ServerGroupsAndResources()
if err != nil {
return errors.Wrap(err, "failed to discover the API resource list")
}
f, err := getFilters(filters...)
if err != nil {
return errors.Wrap(err, "failed to convert filter expression")
}
return errors.Wrap(reportOnAPIs(rlList, f, dyn), "failed to report on the available APIs")
}

func reportOnAPIs(rlList []*metav1.APIResourceList, f filters, dyn dynamic.Interface) error { //nolint:gocyclo // should we break this?
for _, rl := range rlList {
for _, r := range rl.APIResources {
if r.Namespaced {
continue
}
managed := false
for _, c := range r.Categories {
if c == "managed" {
managed = true
break
}
}
if !managed {
continue
}

gv, err := schema.ParseGroupVersion(rl.GroupVersion)
if err != nil {
return errors.Wrapf(err, "failed to parse GroupVersion string: %s", rl.GroupVersion)
}
gvr := schema.GroupVersionResource{
Group: gv.Group,
Version: gv.Version,
Resource: r.Name,
}
gvk := schema.GroupVersionKind{
Group: gv.Group,
Version: gv.Version,
Kind: r.Kind,
}
if !f.match(gvk, "") {
continue
}

ri := dyn.Resource(gvr)
ul, err := ri.List(context.TODO(), metav1.ListOptions{})
if err != nil {
return errors.Wrapf(err, "failed to list resources with GVR: %s", gvr.String())
}
for _, u := range ul.Items {
if !f.match(gvk, u.GetName()) {
continue
}
reportTTR(gvk, u)
}
}
}
return nil
}

func reportTTR(gvk schema.GroupVersionKind, u unstructured.Unstructured) {
rc := getReadyCondition(u)
// resource not ready yet
if rc.Status != corev1.ConditionTrue {
return
}
fmt.Printf("%s/%s/%s/%s:%.0f\n", gvk.Group, gvk.Version, gvk.Kind, u.GetName(), rc.LastTransitionTime.Sub(u.GetCreationTimestamp().Time).Seconds())
}

func getReadyCondition(u unstructured.Unstructured) xpv1.Condition {
conditioned := xpv1.ConditionedStatus{}
// The path is directly `status` because conditions are inline.
if err := fieldpath.Pave(u.Object).GetValueInto("status", &conditioned); err != nil {
return xpv1.Condition{}
}
return conditioned.GetCondition(xpv1.TypeReady)
}
48 changes: 33 additions & 15 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ require (
github.com/google/go-cmp v0.5.9
github.com/pkg/errors v0.9.1
github.com/spf13/afero v1.8.0
github.com/spf13/cobra v1.6.0
github.com/tufin/oasdiff v1.2.6
golang.org/x/mod v0.7.0
google.golang.org/api v0.102.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
k8s.io/api v0.26.1
k8s.io/apiextensions-apiserver v0.23.0
k8s.io/apimachinery v0.23.0
k8s.io/apimachinery v0.26.1
k8s.io/cli-runtime v0.26.1
k8s.io/client-go v0.26.1
sigs.k8s.io/yaml v1.3.0
)

Expand All @@ -29,34 +33,48 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
github.com/yuin/goldmark v1.5.3 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
Expand All @@ -65,12 +83,12 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.23.0 // indirect
k8s.io/client-go v0.23.0 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
sigs.k8s.io/controller-runtime v0.11.0 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
Loading