Skip to content

Commit 855808a

Browse files
committed
refactor: move list to api package
This makes our special list function available to other packages than get and uses it in logs.
1 parent 14d3057 commit 855808a

File tree

15 files changed

+239
-199
lines changed

15 files changed

+239
-199
lines changed

api/list.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
// ListN lists resources with some ux-improvements like hinting when a resource
71+
// has been found in a different project of the same organization.
72+
func (c *Client) ListN(ctx context.Context, list runtimeclient.ObjectList, options ...ListOpt) error {
73+
opts := &ListOpts{}
74+
for _, opt := range options {
75+
opt(opts)
76+
}
77+
78+
if opts.allNamespaces {
79+
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
80+
return fmt.Errorf("error when listing across all namespaces: %w", err)
81+
}
82+
return nil
83+
}
84+
85+
// we now need a bit of reflection code from the apimachinery package
86+
// as the ObjectList interface provides no way to get or set the list
87+
// items directly.
88+
89+
// we need to get a pointer to the items field of the list and turn it
90+
// into a reflect value so that we can change the items in case we want
91+
// to search in all projects.
92+
itemsPtr, err := meta.GetItemsPtr(list)
93+
if err != nil {
94+
return err
95+
}
96+
items, err := conversion.EnforcePtr(itemsPtr)
97+
if err != nil {
98+
return err
99+
}
100+
101+
if !opts.allProjects {
102+
// here a special logic applies. We are searching in the
103+
// current set project. If we are searching for a specific
104+
// named object and did not find it in the current set project,
105+
// we are searching in all projects for it. If we found it in
106+
// another project, we return an error saying that we found the
107+
// named object somewhere else.
108+
109+
opts.clientListOptions = append(opts.clientListOptions, runtimeclient.InNamespace(c.Project))
110+
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
111+
return err
112+
}
113+
// if we did not search for a specific named object or we
114+
// actually found the object we were searching for in the
115+
// current project, we can stop here. If we were not able to
116+
// find it, we need to search in all projects for it.
117+
if opts.searchForName == "" || items.Len() > 0 {
118+
return nil
119+
}
120+
}
121+
// we want to search in all projects, so we need to get them first...
122+
projects, err := c.Projects(ctx, "")
123+
if err != nil {
124+
return fmt.Errorf("error when searching for projects: %w", err)
125+
}
126+
127+
for _, proj := range projects {
128+
tempOpts := slices.Clone(opts.clientListOptions)
129+
// we ensured the list is a pointer type and that is has an
130+
// 'Items' field which is a slice above, so we don't need to do
131+
// this again here and instead use the reflect functions directly.
132+
tempList := reflect.New(reflect.TypeOf(list).Elem()).Interface().(runtimeclient.ObjectList)
133+
if err := c.List(ctx, tempList, append(tempOpts, runtimeclient.InNamespace(proj.Name))...); err != nil {
134+
return fmt.Errorf("error when searching in project %s: %w", proj.Name, err)
135+
}
136+
tempListItems := reflect.ValueOf(tempList).Elem().FieldByName("Items")
137+
for i := 0; i < tempListItems.Len(); i++ {
138+
items.Set(reflect.Append(items, tempListItems.Index(i)))
139+
}
140+
}
141+
142+
// if the user did not search for a specific named resource we can already
143+
// quit as this case should not throw an error if no item could be
144+
// found
145+
if opts.searchForName == "" {
146+
return nil
147+
}
148+
149+
// we can now be sure that the user searched for a named object in
150+
// either the current project or in all projects.
151+
if items.Len() == 0 {
152+
// we did not find the named object in any project. We return
153+
// an error here so that the command can be exited with a
154+
// non-zero code.
155+
return opts.namedResourceNotFound(c.Project)
156+
}
157+
// if the user searched in all projects for a specific resource and
158+
// something was found, we can already return with no error.
159+
if opts.allProjects {
160+
return nil
161+
}
162+
// we found the named object at least in one different project,
163+
// so we return a hint to the user to search in these projects
164+
var identifiedProjects []string
165+
for i := 0; i < items.Len(); i++ {
166+
// the "Items" field of a list type is a slice of types and not
167+
// a slice of pointer types (e.g. "[]corev1.Pod" and not
168+
// "[]*corev1.Pod"), but the clientruntime.Object interface is
169+
// implemented on pointer types (e.g. *corev1.Pod). So we need
170+
// to convert.
171+
if !items.Index(i).CanAddr() {
172+
// if the type of the "Items" slice is a pointer type
173+
// already, something is odd as this normally isn't the
174+
// case. We ignore the item in this case.
175+
continue
176+
}
177+
obj, isRuntimeClientObj := items.Index(i).Addr().Interface().(runtimeclient.Object)
178+
if !isRuntimeClientObj {
179+
// very unlikely case: the items of the list did not
180+
// implement runtimeclient.Object. As we can not get
181+
// the project of the object, we just ignore it.
182+
continue
183+
}
184+
identifiedProjects = append(identifiedProjects, obj.GetNamespace())
185+
}
186+
return opts.namedResourceNotFound(c.Project, identifiedProjects...)
187+
}
188+
189+
// Projects returns either all existing Projects or only the specific project
190+
// identified by the "onlyName" parameter
191+
func (c *Client) Projects(ctx context.Context, onlyName string) ([]management.Project, error) {
192+
org, err := c.Organization()
193+
if err != nil {
194+
return nil, err
195+
}
196+
opts := []runtimeclient.ListOption{
197+
runtimeclient.InNamespace(org),
198+
}
199+
if onlyName != "" {
200+
opts = append(opts, runtimeclient.MatchingFields(
201+
map[string]string{"metadata.name": onlyName},
202+
))
203+
}
204+
205+
projectList := &management.ProjectList{}
206+
if err := c.List(
207+
ctx,
208+
projectList,
209+
opts...,
210+
); err != nil {
211+
return nil, err
212+
}
213+
return projectList.Items, nil
214+
}

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)