Skip to content

Commit

Permalink
Merge pull request #71 from ulucinar/fix-69
Browse files Browse the repository at this point in the history
Add ttr, the time-to-readiness reporting tool for managed resources
  • Loading branch information
ulucinar authored Apr 17, 2023
2 parents 0224e96 + b470b50 commit 3eecf2d
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 37 deletions.
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

0 comments on commit 3eecf2d

Please sign in to comment.