From 5492d4066d6ace8b3a7d2c5d8fd2f82c700da7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 23 Aug 2024 16:17:54 +0300 Subject: [PATCH 1/5] Sort text output by API group for easier overview --- internal/client/result/resource.go | 46 +++++++++++++++++++++++------- internal/printer/printer.go | 4 ++- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/internal/client/result/resource.go b/internal/client/result/resource.go index 29d017e..8cd9824 100644 --- a/internal/client/result/resource.go +++ b/internal/client/result/resource.go @@ -17,10 +17,12 @@ limitations under the License. package result import ( + "cmp" "sort" "strings" "github.com/corneliusweig/rakkess/internal/printer" + "k8s.io/apimachinery/pkg/runtime/schema" ) // ResourceAccess holds the access result for all resources. @@ -28,25 +30,49 @@ type ResourceAccess map[string]map[string]Access // Print implements MatrixPrinter.Print. It prints a tab-separated table with a header. func (ra ResourceAccess) Table(verbs []string) *printer.Table { - var names []string + var groupResources []schema.GroupResource for name := range ra { - names = append(names, name) + groupResources = append(groupResources, schema.ParseGroupResource(name)) } - sort.Strings(names) + sort.Slice(groupResources, func(i, j int) bool { + x := groupResources[i] + y := groupResources[j] + // first sort by group, then resource + if x.Group != y.Group { + return cmp.Less(x.Group, y.Group) + } + return cmp.Less(x.Resource, y.Resource) + }) - // table header - headers := []string{"NAME"} + upperVerbs := make([]string, 0, len(verbs)) for _, v := range verbs { - headers = append(headers, strings.ToUpper(v)) + upperVerbs = append(upperVerbs, strings.ToUpper(v)) } - p := printer.TableWithHeaders(headers) + p := printer.TableWithHeaders(nil) // table body - for _, name := range names { + lastGroup := "" + for i, gr := range groupResources { + // print the API group and verbs when the API group changes, or for the first API group (which often is "") + if gr.Group != lastGroup || i == 0 { + + if i != 0 { + p.AddRow([]string{" "}, printer.None) // at least one "none" outcome needed to get the tabprinter aligning all columns + } + + displayGroup := gr.Group + if displayGroup == "" { + displayGroup = "core" + } + + p.AddRow(append([]string{displayGroup + ":"}, upperVerbs...), printer.None) + lastGroup = gr.Group + } + var outcomes []printer.Outcome - res := ra[name] + res := ra[gr.String()] for _, v := range verbs { var o printer.Outcome switch res[v] { @@ -61,7 +87,7 @@ func (ra ResourceAccess) Table(verbs []string) *printer.Table { } outcomes = append(outcomes, o) } - p.AddRow([]string{name}, outcomes...) + p.AddRow([]string{gr.Resource}, outcomes...) } return p } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index f47b254..96ba53f 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -93,7 +93,9 @@ func (p *Table) Render(out io.Writer, outputFormat string) { fmt.Fprintf(w, "\t%s", h) } } - fmt.Fprint(w, "\n") + if len(p.Headers) != 0 { + fmt.Fprint(w, "\n") + } // table body for _, row := range p.Rows { From 93729b6bc652a80b4fe0af02506e07cb0e4e94d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 23 Aug 2024 16:21:42 +0300 Subject: [PATCH 2/5] Fetch all (sub)resources for a group version as well --- internal/client/fetch_resources.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/client/fetch_resources.go b/internal/client/fetch_resources.go index a78f4f3..f1f243c 100644 --- a/internal/client/fetch_resources.go +++ b/internal/client/fetch_resources.go @@ -80,7 +80,12 @@ func FetchAvailableGroupResources(opts *options.RakkessOptions) ([]GroupResource klog.Warningf("Cannot parse groupVersion: %s", err) continue } - for _, r := range list.APIResources { + resourceListWithSubresources, err := client.ServerResourcesForGroupVersion(list.GroupVersion) + if err != nil { + klog.Warningf("Cannot parse get all resources for gv: %s %s", list.GroupVersion, err) + continue + } + for _, r := range resourceListWithSubresources.APIResources { if len(r.Verbs) == 0 { continue } From 729a0ad0f97f45d16ed3e6f8ca7c91bfb2f33165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 23 Aug 2024 16:49:33 +0300 Subject: [PATCH 3/5] Implement fake method in test --- internal/client/fetch_resources_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/client/fetch_resources_test.go b/internal/client/fetch_resources_test.go index 881743a..881aabf 100644 --- a/internal/client/fetch_resources_test.go +++ b/internal/client/fetch_resources_test.go @@ -61,7 +61,7 @@ func (c *fakeCachedDiscoveryInterface) ServerGroupsAndResources() ([]*metav1.API } func (c *fakeCachedDiscoveryInterface) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { - panic("not implemented") + return &c.next, nil } func (c *fakeCachedDiscoveryInterface) ServerResources() ([]*metav1.APIResourceList, error) { From 746bb1b7cf3f037c618db3e4e9222b59da0c05b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 23 Aug 2024 17:21:27 +0300 Subject: [PATCH 4/5] Also consider API group in subjectaccess --- internal/client/result/subject.go | 26 ++++++++++++--- internal/client/result/subject_test.go | 30 ++++++++++++++++- internal/client/subject_access.go | 5 +-- internal/client/subject_access_test.go | 45 +++++++++++++++++--------- internal/rakkess.go | 9 ++++-- 5 files changed, 88 insertions(+), 27 deletions(-) diff --git a/internal/client/result/subject.go b/internal/client/result/subject.go index 78a3a1c..36a0885 100644 --- a/internal/client/result/subject.go +++ b/internal/client/result/subject.go @@ -23,6 +23,7 @@ import ( "github.com/corneliusweig/rakkess/internal/constants" "github.com/corneliusweig/rakkess/internal/printer" v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" ) @@ -39,8 +40,8 @@ type SubjectRef struct { // SubjectAccess holds the access information of all subjects for the given resource. type SubjectAccess struct { - // Resource is the kubernetes resource of this query. - Resource string + // GroupResource is the kubernetes GroupResource of this query. + GroupResource schema.GroupResource // ResourceName is the name of the kubernetes resource instance of this query. ResourceName string // roleToVerbs holds all rule data concerning this resource and is extracted from Roles and ClusterRoles. @@ -50,9 +51,9 @@ type SubjectAccess struct { } // NewSubjectAccess creates a new SubjectAccess with initialized fields. -func NewSubjectAccess(resource, resourceName string) *SubjectAccess { +func NewSubjectAccess(gr schema.GroupResource, resourceName string) *SubjectAccess { return &SubjectAccess{ - Resource: resource, + GroupResource: gr, ResourceName: resourceName, roleToVerbs: make(map[RoleRef]sets.String), subjectToVerbs: make(map[SubjectRef]sets.String), @@ -99,8 +100,14 @@ func (sa *SubjectAccess) MatchRules(ref RoleRef, rule v1.PolicyRule) { return } + // if the query is for "deployments.apps" GroupResource, ignore a PolicyRule which applies to resource + // "deployment", but API group "foo". + if !apiGroupMatches(rule.APIGroups, sa.GroupResource.Group) { + return + } + for _, r := range rule.Resources { - if r == v1.ResourceAll || r == sa.Resource { + if r == v1.ResourceAll || r == sa.GroupResource.Resource { expandedVerbs := expand(rule.Verbs) if verbs, ok := sa.roleToVerbs[ref]; ok { sa.roleToVerbs[ref] = sets.NewString(expandedVerbs...).Union(verbs) @@ -111,6 +118,15 @@ func (sa *SubjectAccess) MatchRules(ref RoleRef, rule v1.PolicyRule) { } } +func apiGroupMatches(entries []string, target string) bool { + for _, entry := range entries { + if entry == "*" || entry == target { + return true + } + } + return false +} + func includes(coll []string, x string) bool { if x == "" { return false diff --git a/internal/client/result/subject_test.go b/internal/client/result/subject_test.go index de1892c..67e4424 100644 --- a/internal/client/result/subject_test.go +++ b/internal/client/result/subject_test.go @@ -22,6 +22,7 @@ import ( "github.com/corneliusweig/rakkess/internal/constants" "github.com/stretchr/testify/assert" v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" ) @@ -30,6 +31,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { Name: "some-role", Kind: "some-kind", } + apiGroup := "apps" resource := "deployments" tests := []struct { name string @@ -41,6 +43,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { { name: "simple rule", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, Verbs: []string{"create", "get"}, }, @@ -50,6 +53,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { name: "simple rule with initial verbs", initialVerbs: []string{"initial", "other"}, rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, Verbs: []string{"create", "get"}, }, @@ -58,6 +62,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { { name: "rule for multiple resources", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{"resource-other", resource, "resource-yet-another"}, Verbs: []string{"create", "get"}, }, @@ -66,6 +71,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { { name: "no matching resource", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{"resource-other", "resource-yet-another"}, Verbs: []string{"create", "get"}, }, @@ -73,6 +79,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { { name: "VerbAll", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, Verbs: []string{v1.VerbAll}, }, @@ -81,6 +88,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { { name: "simple rule with resourceNames does not match", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, ResourceNames: []string{"no-match"}, Verbs: []string{"create", "get"}, @@ -90,6 +98,7 @@ func TestSubjectAccess_MatchRules(t *testing.T) { name: "simple rule with matching resourceName", resourceName: "my-resource-name", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, ResourceNames: []string{"my-resource-name"}, Verbs: []string{"create", "get"}, @@ -100,16 +109,35 @@ func TestSubjectAccess_MatchRules(t *testing.T) { name: "simple rule with wrong resourceName", resourceName: "my-resource-name", rule: v1.PolicyRule{ + APIGroups: []string{apiGroup}, Resources: []string{resource}, ResourceNames: []string{"wrong-resource-name"}, Verbs: []string{"create", "get"}, }, }, + { + name: "non-matching API group", + rule: v1.PolicyRule{ + APIGroups: []string{"nonmatchingapigroup"}, + Resources: []string{resource}, + Verbs: []string{"create", "get"}, + }, + }, + { + name: "match star API group", + rule: v1.PolicyRule{ + APIGroups: []string{"*"}, + Resources: []string{resource}, + Verbs: []string{"create", "get"}, + }, + expectedVerbs: []string{"create", "get"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - sa := NewSubjectAccess(resource, test.resourceName) + gr := schema.GroupResource{Group: apiGroup, Resource: resource} + sa := NewSubjectAccess(gr, test.resourceName) if test.initialVerbs != nil { sa.roleToVerbs[r] = sets.NewString(test.initialVerbs...) } diff --git a/internal/client/subject_access.go b/internal/client/subject_access.go index 611e086..811ad92 100644 --- a/internal/client/subject_access.go +++ b/internal/client/subject_access.go @@ -22,6 +22,7 @@ import ( "github.com/corneliusweig/rakkess/internal/client/result" "github.com/corneliusweig/rakkess/internal/options" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" clientv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" "k8s.io/klog/v2" ) @@ -37,7 +38,7 @@ const ( ) // GetSubjectAccess determines subjects with access to the given resource. -func GetSubjectAccess(ctx context.Context, opts *options.RakkessOptions, resource, resourceName string) (*result.SubjectAccess, error) { +func GetSubjectAccess(ctx context.Context, opts *options.RakkessOptions, gr schema.GroupResource, resourceName string) (*result.SubjectAccess, error) { rbacClient, err := getRbacClient(opts) if err != nil { return nil, err @@ -46,7 +47,7 @@ func GetSubjectAccess(ctx context.Context, opts *options.RakkessOptions, resourc namespace := opts.ConfigFlags.Namespace isNamespace := namespace != nil && *namespace != "" - sa := result.NewSubjectAccess(resource, resourceName) + sa := result.NewSubjectAccess(gr, resourceName) if err := fetchMatchingClusterRoles(ctx, rbacClient, sa); err != nil { if !isNamespace { diff --git a/internal/client/subject_access_test.go b/internal/client/subject_access_test.go index e6a0676..c6e9934 100644 --- a/internal/client/subject_access_test.go +++ b/internal/client/subject_access_test.go @@ -27,6 +27,7 @@ import ( v1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" clientv1 "k8s.io/client-go/kubernetes/typed/rbac/v1" @@ -46,6 +47,7 @@ func TestGetSubjectAccess(t *testing.T) { name string namespace string resource string + apiGroup string clusterRoles []v1.ClusterRole clusterRoleBindings []v1.ClusterRoleBinding roles []v1.Role @@ -55,10 +57,11 @@ func TestGetSubjectAccess(t *testing.T) { { name: "cluster-role and role matches", namespace: roleNamespace, + apiGroup: "apps", resource: "deployments", - clusterRoles: clusterRoles("deployments", "create"), + clusterRoles: clusterRoles("apps", "deployments", "create"), clusterRoleBindings: clusterRoleBindings("test-user"), - roles: roles("deployments", "list"), + roles: roles("apps", "deployments", "list"), roleBindings: roleBindings(testRoleName, roleName, "test-user"), expected: map[result.SubjectRef]sets.String{ {Name: "test-user", Kind: subjectKind}: sets.NewString("create", "list"), @@ -67,10 +70,11 @@ func TestGetSubjectAccess(t *testing.T) { { name: "cluster-role and role matches, multiple subjects", namespace: roleNamespace, + apiGroup: "apps", resource: "deployments", - clusterRoles: clusterRoles("deployments", "create"), + clusterRoles: clusterRoles("apps", "deployments", "create"), clusterRoleBindings: clusterRoleBindings("user1", "user2"), - roles: roles("deployments", "list"), + roles: roles("apps", "deployments", "list"), roleBindings: roleBindings(testRoleName, roleName, "user2", "user3"), expected: map[result.SubjectRef]sets.String{ {Name: "user1", Kind: subjectKind}: sets.NewString("create"), @@ -81,10 +85,11 @@ func TestGetSubjectAccess(t *testing.T) { { name: "cluster-role and role matches, global scope", namespace: "", // empty namespace means global scope + apiGroup: "apps", resource: "deployments", - clusterRoles: clusterRoles("deployments", "create"), + clusterRoles: clusterRoles("apps", "deployments", "create"), clusterRoleBindings: clusterRoleBindings("test-user"), - roles: roles("deployments", "list"), + roles: roles("apps", "deployments", "list"), roleBindings: roleBindings(testRoleName, roleName, "test-user"), expected: map[result.SubjectRef]sets.String{ {Name: "test-user", Kind: subjectKind}: sets.NewString("create"), @@ -93,8 +98,9 @@ func TestGetSubjectAccess(t *testing.T) { { name: "rolebinding to clusterrole", namespace: roleNamespace, + apiGroup: "apps", resource: "deployments", - clusterRoles: clusterRoles("deployments", "create"), + clusterRoles: clusterRoles("apps", "deployments", "create"), roleBindings: roleBindings(testClusterRoleName, clusterRoleName, "test-user"), expected: map[result.SubjectRef]sets.String{ {Name: "test-user", Kind: subjectKind}: sets.NewString("create"), @@ -103,20 +109,22 @@ func TestGetSubjectAccess(t *testing.T) { { name: "bindings for wrong resource", namespace: roleNamespace, + apiGroup: "apps", resource: "deployments", - clusterRoles: clusterRoles("configmaps", "create"), + clusterRoles: clusterRoles("", "configmaps", "create"), clusterRoleBindings: clusterRoleBindings("test-user"), - roles: roles("configmaps", "list"), + roles: roles("", "configmaps", "list"), roleBindings: roleBindings(testRoleName, roleName, "test-user"), expected: map[result.SubjectRef]sets.String{}, }, { name: "VerbAll role binding", namespace: roleNamespace, + apiGroup: "", resource: "configmaps", - clusterRoles: clusterRoles("configmaps", "create"), + clusterRoles: clusterRoles("", "configmaps", "create"), clusterRoleBindings: clusterRoleBindings("test-user"), - roles: roles("configmaps", v1.VerbAll), + roles: roles("", "configmaps", v1.VerbAll), roleBindings: roleBindings(testRoleName, roleName, "test-user"), expected: map[result.SubjectRef]sets.String{ {Name: "test-user", Kind: subjectKind}: sets.NewString(constants.ValidVerbs...), @@ -125,8 +133,9 @@ func TestGetSubjectAccess(t *testing.T) { { name: "VerbAll clusterrole binding", namespace: roleNamespace, + apiGroup: "", resource: "configmaps", - clusterRoles: clusterRoles("configmaps", v1.VerbAll), + clusterRoles: clusterRoles("", "configmaps", v1.VerbAll), clusterRoleBindings: clusterRoleBindings("test-user"), expected: map[result.SubjectRef]sets.String{ {Name: "test-user", Kind: subjectKind}: sets.NewString(constants.ValidVerbs...), @@ -166,15 +175,17 @@ func TestGetSubjectAccess(t *testing.T) { Namespace: &test.namespace, }, } - sa, err := GetSubjectAccess(ctx, opts, test.resource, "") + gr := schema.GroupResource{Group: test.apiGroup, Resource: test.resource} + sa, err := GetSubjectAccess(ctx, opts, gr, "") assert.NoError(t, err) - assert.Equal(t, test.resource, sa.Resource) + assert.Equal(t, test.resource, sa.GroupResource.Resource) + assert.Equal(t, test.apiGroup, sa.GroupResource.Group) assert.Equal(t, test.expected, sa.Get()) }) } } -func clusterRoles(resource string, verbs ...string) []v1.ClusterRole { +func clusterRoles(apiGroup, resource string, verbs ...string) []v1.ClusterRole { return []v1.ClusterRole{ { ObjectMeta: metav1.ObjectMeta{ @@ -182,6 +193,7 @@ func clusterRoles(resource string, verbs ...string) []v1.ClusterRole { }, Rules: []v1.PolicyRule{ { + APIGroups: []string{apiGroup}, Verbs: verbs, Resources: []string{resource}, }, @@ -209,7 +221,7 @@ func clusterRoleBindings(subjects ...string) []v1.ClusterRoleBinding { } } -func roles(resource string, verbs ...string) []v1.Role { +func roles(apiGroup, resource string, verbs ...string) []v1.Role { return []v1.Role{ { ObjectMeta: metav1.ObjectMeta{ @@ -218,6 +230,7 @@ func roles(resource string, verbs ...string) []v1.Role { }, Rules: []v1.PolicyRule{ { + APIGroups: []string{apiGroup}, Verbs: verbs, Resources: []string{resource}, }, diff --git a/internal/rakkess.go b/internal/rakkess.go index d0a9c69..392d50d 100644 --- a/internal/rakkess.go +++ b/internal/rakkess.go @@ -55,7 +55,7 @@ func Resource(ctx context.Context, opts *options.RakkessOptions) (result.Resourc // Subject determines the subjects with access right to the given resource and // prints the result as a matrix with verbs in the horizontal and subject names // in the vertical direction. -func Subject(ctx context.Context, opts *options.RakkessOptions, resource, resourceName string) error { +func Subject(ctx context.Context, opts *options.RakkessOptions, resourceWithOptionalAPIGroup, resourceName string) error { if err := validation.OutputFormat(opts.OutputFormat); err != nil { return err } @@ -64,12 +64,15 @@ func Subject(ctx context.Context, opts *options.RakkessOptions, resource, resour if err != nil { return errors.Wrap(err, "cannot create k8s REST mapper") } - versionedResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: resource}) + + // the apiGroup might be unspecified in the query, but will be populated in the response if there were only one such resource + gr := schema.ParseGroupResource(resourceWithOptionalAPIGroup) + versionedResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: gr.Resource, Group: gr.Group}) if err != nil { return errors.Wrap(err, "determine requested resource") } - subjectAccess, err := client.GetSubjectAccess(ctx, opts, versionedResource.Resource, resourceName) + subjectAccess, err := client.GetSubjectAccess(ctx, opts, versionedResource.GroupResource(), resourceName) if err != nil { return errors.Wrap(err, "get subject access") } From ec169583986338d8bcdfde19ff70de4538f5ba69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 23 Aug 2024 17:23:00 +0300 Subject: [PATCH 5/5] Update go.mod --- go.mod | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5803aa1..e3daf38 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,6 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/corneliusweig/tabwriter v0.0.0-20190512204542-5f8a091e83b5 github.com/googleapis/gnostic v0.4.1 - github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect - github.com/imdario/mergo v0.3.7 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.2.1 @@ -18,4 +16,63 @@ require ( k8s.io/klog/v2 v2.80.1 ) -go 1.16 +require ( + cloud.google.com/go v0.81.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.12 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.0 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/evanphx/json-patch v4.9.0+incompatible // indirect + github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-logr/logr v1.2.0 // indirect + github.com/go-openapi/jsonpointer v0.19.3 // indirect + github.com/go-openapi/jsonreference v0.19.3 // indirect + github.com/go-openapi/spec v0.19.5 // indirect + github.com/go-openapi/swag v0.19.5 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/btree v1.0.0 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect + github.com/imdario/mergo v0.3.7 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect + golang.org/x/text v0.3.5 // indirect + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 // indirect + k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect + sigs.k8s.io/kustomize/api v0.8.8 // indirect + sigs.k8s.io/kustomize/kyaml v0.10.17 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.1.0 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) + +go 1.20