|
| 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 | +} |
0 commit comments