Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions api/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package api

import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

// GetObject gets the object in the current client project with some
// ux-improvements like hinting when the object has been found in a different
// project of the same organization.
func (c *Client) GetObject(ctx context.Context, name string, obj runtimeclient.Object) error {
list := &unstructured.UnstructuredList{}
gvks, _, err := c.Scheme().ObjectKinds(obj)
if err != nil || len(gvks) != 1 {
return fmt.Errorf("unable to determine GVK from object %T", obj)
}
list.SetGroupVersionKind(gvks[0])
if err := c.ListObjects(ctx, list, MatchName(name)); err != nil {
return err
}
// this *should* already be handled by ListN
if len(list.Items) == 0 {
return fmt.Errorf("resource %q was not found in any project", name)
}
return runtime.DefaultUnstructuredConverter.FromUnstructured(list.Items[0].Object, obj)
}
216 changes: 216 additions & 0 deletions api/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package api

import (
"context"
"errors"
"fmt"
"reflect"
"slices"
"strings"

management "github.com/ninech/apis/management/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/conversion"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

type ListOpts struct {
clientListOptions []runtimeclient.ListOption
searchForName string
allProjects bool `help:"apply the get over all projects." short:"A"`
allNamespaces bool `help:"apply the get over all namespaces." hidden:""`
}

type ListOpt func(opts *ListOpts)

func MatchName(name string) ListOpt {
return func(cmd *ListOpts) {
if len(name) == 0 {
cmd.searchForName = ""
return
}
cmd.clientListOptions = append(cmd.clientListOptions, runtimeclient.MatchingFields{"metadata.name": name})
cmd.searchForName = name
}
}

func MatchLabel(k, v string) ListOpt {
return func(cmd *ListOpts) {
cmd.clientListOptions = append(cmd.clientListOptions, runtimeclient.MatchingLabels{k: v})
}
}

func AllProjects() ListOpt {
return func(cmd *ListOpts) {
cmd.allProjects = true
}
}

func AllNamespaces() ListOpt {
return func(cmd *ListOpts) {
cmd.allNamespaces = true
}
}

func (opts *ListOpts) namedResourceNotFound(project string, foundInProjects ...string) error {
if opts.allProjects {
return fmt.Errorf("resource %q was not found in any project", opts.searchForName)
}
errorMessage := fmt.Sprintf("resource %q was not found in project %s", opts.searchForName, project)
if len(foundInProjects) > 0 {
errorMessage = errorMessage + fmt.Sprintf(
", but it was found in project(s): %s. "+
"Maybe you want to use the '--project' flag to specify one of these projects?",
strings.Join(foundInProjects, " ,"),
)
}
return errors.New(errorMessage)
}

// ListObjects lists objects in the current client project with some
// ux-improvements like hinting when a resource has been found in a different
// project of the same organization.
func (c *Client) ListObjects(ctx context.Context, list runtimeclient.ObjectList, options ...ListOpt) error {
opts := &ListOpts{}
for _, opt := range options {
opt(opts)
}

if opts.allNamespaces {
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
return fmt.Errorf("error when listing across all namespaces: %w", err)
}
return nil
}

// we now need a bit of reflection code from the apimachinery package
// as the ObjectList interface provides no way to get or set the list
// items directly.

// we need to get a pointer to the items field of the list and turn it
// into a reflect value so that we can change the items in case we want
// to search in all projects.
itemsPtr, err := meta.GetItemsPtr(list)
if err != nil {
return err
}
items, err := conversion.EnforcePtr(itemsPtr)
if err != nil {
return err
}

if !opts.allProjects {
// here a special logic applies. We are searching in the
// current set project. If we are searching for a specific
// named object and did not find it in the current set project,
// we are searching in all projects for it. If we found it in
// another project, we return an error saying that we found the
// named object somewhere else.

opts.clientListOptions = append(opts.clientListOptions, runtimeclient.InNamespace(c.Project))
if err := c.List(ctx, list, opts.clientListOptions...); err != nil {
return err
}
// if we did not search for a specific named object or we
// actually found the object we were searching for in the
// current project, we can stop here. If we were not able to
// find it, we need to search in all projects for it.
if opts.searchForName == "" || items.Len() > 0 {
return nil
}
}
// we want to search in all projects, so we need to get them first...
projects, err := c.Projects(ctx, "")
if err != nil {
return fmt.Errorf("error when searching for projects: %w", err)
}

for _, proj := range projects {
tempOpts := slices.Clone(opts.clientListOptions)
// we ensured the list is a pointer type and that is has an
// 'Items' field which is a slice above, so we don't need to do
// this again here and instead use the reflect functions directly.
tempList := reflect.New(reflect.TypeOf(list).Elem()).Interface().(runtimeclient.ObjectList)
tempList.GetObjectKind().SetGroupVersionKind(list.GetObjectKind().GroupVersionKind())
if err := c.List(ctx, tempList, append(tempOpts, runtimeclient.InNamespace(proj.Name))...); err != nil {
return fmt.Errorf("error when searching in project %s: %w", proj.Name, err)
}
tempListItems := reflect.ValueOf(tempList).Elem().FieldByName("Items")
for i := 0; i < tempListItems.Len(); i++ {
items.Set(reflect.Append(items, tempListItems.Index(i)))
}
}

// if the user did not search for a specific named resource we can already
// quit as this case should not throw an error if no item could be
// found
if opts.searchForName == "" {
return nil
}

// we can now be sure that the user searched for a named object in
// either the current project or in all projects.
if items.Len() == 0 {
// we did not find the named object in any project. We return
// an error here so that the command can be exited with a
// non-zero code.
return opts.namedResourceNotFound(c.Project)
}
// if the user searched in all projects for a specific resource and
// something was found, we can already return with no error.
if opts.allProjects {
return nil
}
// we found the named object at least in one different project,
// so we return a hint to the user to search in these projects
var identifiedProjects []string
for i := 0; i < items.Len(); i++ {
// the "Items" field of a list type is a slice of types and not
// a slice of pointer types (e.g. "[]corev1.Pod" and not
// "[]*corev1.Pod"), but the clientruntime.Object interface is
// implemented on pointer types (e.g. *corev1.Pod). So we need
// to convert.
if !items.Index(i).CanAddr() {
// if the type of the "Items" slice is a pointer type
// already, something is odd as this normally isn't the
// case. We ignore the item in this case.
continue
}
obj, isRuntimeClientObj := items.Index(i).Addr().Interface().(runtimeclient.Object)
if !isRuntimeClientObj {
// very unlikely case: the items of the list did not
// implement runtimeclient.Object. As we can not get
// the project of the object, we just ignore it.
continue
}
identifiedProjects = append(identifiedProjects, obj.GetNamespace())
}
return opts.namedResourceNotFound(c.Project, identifiedProjects...)
}

// Projects returns either all existing Projects or only the specific project
// identified by the "onlyName" parameter
func (c *Client) Projects(ctx context.Context, onlyName string) ([]management.Project, error) {
org, err := c.Organization()
if err != nil {
return nil, err
}
opts := []runtimeclient.ListOption{
runtimeclient.InNamespace(org),
}
if onlyName != "" {
opts = append(opts, runtimeclient.MatchingFields(
map[string]string{"metadata.name": onlyName},
))
}

projectList := &management.ProjectList{}
if err := c.List(
ctx,
projectList,
opts...,
); err != nil {
return nil, err
}
return projectList.Items, nil
}
2 changes: 1 addition & 1 deletion get/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (cmd *allCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error
if get.AllProjects {
projectName = ""
}
projectList, err := projects(ctx, client, projectName)
projectList, err := client.Projects(ctx, projectName)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion get/apiserviceaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
func (asa *apiServiceAccountsCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
asaList := &iam.APIServiceAccountList{}

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

Expand Down
2 changes: 1 addition & 1 deletion get/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type applicationsCmd struct {

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

Expand Down
4 changes: 2 additions & 2 deletions get/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ type buildCmd struct {
func (cmd *buildCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
buildList := &apps.BuildList{}

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

if err := get.list(ctx, client, buildList, opts...); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion get/cloudvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (cmd *cloudVMCmd) Run(ctx context.Context, client *api.Client, get *Cmd) er

cloudVMList := &infrastructure.CloudVirtualMachineList{}

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

Expand Down
2 changes: 1 addition & 1 deletion get/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type clustersCmd struct {
func (l *clustersCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
clusterList := &infrastructure.KubernetesClusterList{}

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

Expand Down
Loading