Skip to content

Commit a71de0b

Browse files
authored
Merge pull request #210 from ninech/refactor-list
refactor: move list to api package
2 parents 14d3057 + de5c9fe commit a71de0b

File tree

16 files changed

+267
-199
lines changed

16 files changed

+267
-199
lines changed

api/get.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
10+
)
11+
12+
// GetObject gets the object in the current client project with some
13+
// ux-improvements like hinting when the object has been found in a different
14+
// project of the same organization.
15+
func (c *Client) GetObject(ctx context.Context, name string, obj runtimeclient.Object) error {
16+
list := &unstructured.UnstructuredList{}
17+
gvks, _, err := c.Scheme().ObjectKinds(obj)
18+
if err != nil || len(gvks) != 1 {
19+
return fmt.Errorf("unable to determine GVK from object %T", obj)
20+
}
21+
list.SetGroupVersionKind(gvks[0])
22+
if err := c.ListObjects(ctx, list, MatchName(name)); err != nil {
23+
return err
24+
}
25+
// this *should* already be handled by ListN
26+
if len(list.Items) == 0 {
27+
return fmt.Errorf("resource %q was not found in any project", name)
28+
}
29+
return runtime.DefaultUnstructuredConverter.FromUnstructured(list.Items[0].Object, obj)
30+
}

api/list.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"slices"
9+
"strings"
10+
11+
management "github.com/ninech/apis/management/v1alpha1"
12+
"k8s.io/apimachinery/pkg/api/meta"
13+
"k8s.io/apimachinery/pkg/conversion"
14+
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
15+
)
16+
17+
type ListOpts struct {
18+
clientListOptions []runtimeclient.ListOption
19+
searchForName string
20+
allProjects bool `help:"apply the get over all projects." short:"A"`
21+
allNamespaces bool `help:"apply the get over all namespaces." hidden:""`
22+
}
23+
24+
type ListOpt func(opts *ListOpts)
25+
26+
func MatchName(name string) ListOpt {
27+
return func(cmd *ListOpts) {
28+
if len(name) == 0 {
29+
cmd.searchForName = ""
30+
return
31+
}
32+
cmd.clientListOptions = append(cmd.clientListOptions, runtimeclient.MatchingFields{"metadata.name": name})
33+
cmd.searchForName = name
34+
}
35+
}
36+
37+
func MatchLabel(k, v string) ListOpt {
38+
return func(cmd *ListOpts) {
39+
cmd.clientListOptions = append(cmd.clientListOptions, runtimeclient.MatchingLabels{k: v})
40+
}
41+
}
42+
43+
func AllProjects() ListOpt {
44+
return func(cmd *ListOpts) {
45+
cmd.allProjects = true
46+
}
47+
}
48+
49+
func AllNamespaces() ListOpt {
50+
return func(cmd *ListOpts) {
51+
cmd.allNamespaces = true
52+
}
53+
}
54+
55+
func (opts *ListOpts) namedResourceNotFound(project string, foundInProjects ...string) error {
56+
if opts.allProjects {
57+
return fmt.Errorf("resource %q was not found in any project", opts.searchForName)
58+
}
59+
errorMessage := fmt.Sprintf("resource %q was not found in project %s", opts.searchForName, project)
60+
if len(foundInProjects) > 0 {
61+
errorMessage = errorMessage + fmt.Sprintf(
62+
", but it was found in project(s): %s. "+
63+
"Maybe you want to use the '--project' flag to specify one of these projects?",
64+
strings.Join(foundInProjects, " ,"),
65+
)
66+
}
67+
return errors.New(errorMessage)
68+
}
69+
70+
// ListObjects lists objects in the current client project with some
71+
// ux-improvements like hinting when a resource has been found in a different
72+
// project of the same organization.
73+
func (c *Client) ListObjects(ctx context.Context, list runtimeclient.ObjectList, options ...ListOpt) error {
74+
opts := &ListOpts{}
75+
for _, opt := range options {
76+
opt(opts)
77+
}
78+
79+
if opts.allNamespaces {
80+
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
81+
return fmt.Errorf("error when listing across all namespaces: %w", err)
82+
}
83+
return nil
84+
}
85+
86+
// we now need a bit of reflection code from the apimachinery package
87+
// as the ObjectList interface provides no way to get or set the list
88+
// items directly.
89+
90+
// we need to get a pointer to the items field of the list and turn it
91+
// into a reflect value so that we can change the items in case we want
92+
// to search in all projects.
93+
itemsPtr, err := meta.GetItemsPtr(list)
94+
if err != nil {
95+
return err
96+
}
97+
items, err := conversion.EnforcePtr(itemsPtr)
98+
if err != nil {
99+
return err
100+
}
101+
102+
if !opts.allProjects {
103+
// here a special logic applies. We are searching in the
104+
// current set project. If we are searching for a specific
105+
// named object and did not find it in the current set project,
106+
// we are searching in all projects for it. If we found it in
107+
// another project, we return an error saying that we found the
108+
// named object somewhere else.
109+
110+
opts.clientListOptions = append(opts.clientListOptions, runtimeclient.InNamespace(c.Project))
111+
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
112+
return err
113+
}
114+
// if we did not search for a specific named object or we
115+
// actually found the object we were searching for in the
116+
// current project, we can stop here. If we were not able to
117+
// find it, we need to search in all projects for it.
118+
if opts.searchForName == "" || items.Len() > 0 {
119+
return nil
120+
}
121+
}
122+
// we want to search in all projects, so we need to get them first...
123+
projects, err := c.Projects(ctx, "")
124+
if err != nil {
125+
return fmt.Errorf("error when searching for projects: %w", err)
126+
}
127+
128+
for _, proj := range projects {
129+
tempOpts := slices.Clone(opts.clientListOptions)
130+
// we ensured the list is a pointer type and that is has an
131+
// 'Items' field which is a slice above, so we don't need to do
132+
// this again here and instead use the reflect functions directly.
133+
tempList := reflect.New(reflect.TypeOf(list).Elem()).Interface().(runtimeclient.ObjectList)
134+
tempList.GetObjectKind().SetGroupVersionKind(list.GetObjectKind().GroupVersionKind())
135+
if err := c.List(ctx, tempList, append(tempOpts, runtimeclient.InNamespace(proj.Name))...); err != nil {
136+
return fmt.Errorf("error when searching in project %s: %w", proj.Name, err)
137+
}
138+
tempListItems := reflect.ValueOf(tempList).Elem().FieldByName("Items")
139+
for i := 0; i < tempListItems.Len(); i++ {
140+
items.Set(reflect.Append(items, tempListItems.Index(i)))
141+
}
142+
}
143+
144+
// if the user did not search for a specific named resource we can already
145+
// quit as this case should not throw an error if no item could be
146+
// found
147+
if opts.searchForName == "" {
148+
return nil
149+
}
150+
151+
// we can now be sure that the user searched for a named object in
152+
// either the current project or in all projects.
153+
if items.Len() == 0 {
154+
// we did not find the named object in any project. We return
155+
// an error here so that the command can be exited with a
156+
// non-zero code.
157+
return opts.namedResourceNotFound(c.Project)
158+
}
159+
// if the user searched in all projects for a specific resource and
160+
// something was found, we can already return with no error.
161+
if opts.allProjects {
162+
return nil
163+
}
164+
// we found the named object at least in one different project,
165+
// so we return a hint to the user to search in these projects
166+
var identifiedProjects []string
167+
for i := 0; i < items.Len(); i++ {
168+
// the "Items" field of a list type is a slice of types and not
169+
// a slice of pointer types (e.g. "[]corev1.Pod" and not
170+
// "[]*corev1.Pod"), but the clientruntime.Object interface is
171+
// implemented on pointer types (e.g. *corev1.Pod). So we need
172+
// to convert.
173+
if !items.Index(i).CanAddr() {
174+
// if the type of the "Items" slice is a pointer type
175+
// already, something is odd as this normally isn't the
176+
// case. We ignore the item in this case.
177+
continue
178+
}
179+
obj, isRuntimeClientObj := items.Index(i).Addr().Interface().(runtimeclient.Object)
180+
if !isRuntimeClientObj {
181+
// very unlikely case: the items of the list did not
182+
// implement runtimeclient.Object. As we can not get
183+
// the project of the object, we just ignore it.
184+
continue
185+
}
186+
identifiedProjects = append(identifiedProjects, obj.GetNamespace())
187+
}
188+
return opts.namedResourceNotFound(c.Project, identifiedProjects...)
189+
}
190+
191+
// Projects returns either all existing Projects or only the specific project
192+
// identified by the "onlyName" parameter
193+
func (c *Client) Projects(ctx context.Context, onlyName string) ([]management.Project, error) {
194+
org, err := c.Organization()
195+
if err != nil {
196+
return nil, err
197+
}
198+
opts := []runtimeclient.ListOption{
199+
runtimeclient.InNamespace(org),
200+
}
201+
if onlyName != "" {
202+
opts = append(opts, runtimeclient.MatchingFields(
203+
map[string]string{"metadata.name": onlyName},
204+
))
205+
}
206+
207+
projectList := &management.ProjectList{}
208+
if err := c.List(
209+
ctx,
210+
projectList,
211+
opts...,
212+
); err != nil {
213+
return nil, err
214+
}
215+
return projectList.Items, nil
216+
}

get/all.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (cmd *allCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error
3333
if get.AllProjects {
3434
projectName = ""
3535
}
36-
projectList, err := projects(ctx, client, projectName)
36+
projectList, err := client.Projects(ctx, projectName)
3737
if err != nil {
3838
return err
3939
}

get/apiserviceaccount.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const (
2525
func (asa *apiServiceAccountsCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
2626
asaList := &iam.APIServiceAccountList{}
2727

28-
if err := get.list(ctx, client, asaList, matchName(asa.Name)); err != nil {
28+
if err := get.list(ctx, client, asaList, api.MatchName(asa.Name)); err != nil {
2929
return err
3030
}
3131

get/application.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type applicationsCmd struct {
2828

2929
func (cmd *applicationsCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
3030
appList := &apps.ApplicationList{}
31-
if err := get.list(ctx, client, appList, matchName(cmd.Name)); err != nil {
31+
if err := get.list(ctx, client, appList, api.MatchName(cmd.Name)); err != nil {
3232
return err
3333
}
3434

get/build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ type buildCmd struct {
3636
func (cmd *buildCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
3737
buildList := &apps.BuildList{}
3838

39-
opts := []listOpt{matchName(cmd.Name)}
39+
opts := []api.ListOpt{api.MatchName(cmd.Name)}
4040
if len(cmd.ApplicationName) != 0 {
41-
opts = append(opts, matchLabel(util.ApplicationNameLabel, cmd.ApplicationName))
41+
opts = append(opts, api.MatchLabel(util.ApplicationNameLabel, cmd.ApplicationName))
4242
}
4343

4444
if err := get.list(ctx, client, buildList, opts...); err != nil {

get/cloudvm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (cmd *cloudVMCmd) Run(ctx context.Context, client *api.Client, get *Cmd) er
2020

2121
cloudVMList := &infrastructure.CloudVirtualMachineList{}
2222

23-
if err := get.list(ctx, client, cloudVMList, matchName(cmd.Name)); err != nil {
23+
if err := get.list(ctx, client, cloudVMList, api.MatchName(cmd.Name)); err != nil {
2424
return err
2525
}
2626

get/clusters.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type clustersCmd struct {
2020
func (l *clustersCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
2121
clusterList := &infrastructure.KubernetesClusterList{}
2222

23-
if err := get.list(ctx, client, clusterList, matchName(l.Name)); err != nil {
23+
if err := get.list(ctx, client, clusterList, api.MatchName(l.Name)); err != nil {
2424
return err
2525
}
2626

0 commit comments

Comments
 (0)