Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/grafanactl/resources/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Command() *cobra.Command {
cmd.AddCommand(editCmd(configOpts))
cmd.AddCommand(getCmd(configOpts))
cmd.AddCommand(listCmd(configOpts))
cmd.AddCommand(getMetaCmd(configOpts))
cmd.AddCommand(pullCmd(configOpts))
cmd.AddCommand(pushCmd(configOpts))
cmd.AddCommand(serveCmd(configOpts))
Expand Down
7 changes: 7 additions & 0 deletions cmd/grafanactl/resources/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type fetchRequest struct {
StopOnError bool
ExcludeManaged bool
ExpectSingleTarget bool
LabelSelector string
Processors []remote.Processor
}

Expand Down Expand Up @@ -50,6 +51,12 @@ func fetchResources(ctx context.Context, opts fetchRequest, args []string) (*fet
return nil, err
}

if opts.LabelSelector != "" {
for i := range filters {
filters[i].LabelSelector = opts.LabelSelector
}
}

pull, err := remote.NewDefaultPuller(ctx, opts.Config)
if err != nil {
return nil, err
Expand Down
12 changes: 8 additions & 4 deletions cmd/grafanactl/resources/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import (
)

type getOpts struct {
IO cmdio.Options
OnError OnErrorMode
IO cmdio.Options
OnError OnErrorMode
LabelSelector string
}

func (opts *getOpts) setup(flags *pflag.FlagSet) {
Expand All @@ -31,6 +32,8 @@ func (opts *getOpts) setup(flags *pflag.FlagSet) {
opts.IO.RegisterCustomCodec("wide", &tableCodec{wide: true})
opts.IO.DefaultFormat("text")

flags.StringVarP(&opts.LabelSelector, "selector", "l", "", "Filter resources by label selector (e.g. -l key=value,other=value)")

// Bind all the flags
opts.IO.BindFlags(flags)
}
Expand Down Expand Up @@ -98,8 +101,9 @@ func getCmd(configOpts *cmdconfig.Options) *cobra.Command {
}

res, err := fetchResources(ctx, fetchRequest{
Config: cfg,
StopOnError: opts.OnError.StopOnError(),
Config: cfg,
StopOnError: opts.OnError.StopOnError(),
LabelSelector: opts.LabelSelector,
}, args)
if err != nil {
return err
Expand Down
236 changes: 236 additions & 0 deletions cmd/grafanactl/resources/get_meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package resources

import (
"errors"
"fmt"
"io"
"time"

cmdconfig "github.com/grafana/grafanactl/cmd/grafanactl/config"
cmdio "github.com/grafana/grafanactl/cmd/grafanactl/io"
"github.com/grafana/grafanactl/internal/format"
"github.com/grafana/grafanactl/internal/resources"
"github.com/grafana/grafanactl/internal/resources/discovery"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/metadata"
)

type getMetaOpts struct {
IO cmdio.Options
LabelSelector string
}

func (opts *getMetaOpts) setup(flags *pflag.FlagSet) {
opts.IO.RegisterCustomCodec("text", &partialMetaTableCodec{wide: false})
opts.IO.RegisterCustomCodec("wide", &partialMetaTableCodec{wide: true})
opts.IO.DefaultFormat("text")

flags.StringVarP(&opts.LabelSelector, "selector", "l", "", "Filter resources by label selector (e.g. -l key=value,other=value)")

opts.IO.BindFlags(flags)
}

func (opts *getMetaOpts) Validate() error {
return opts.IO.Validate()
}

func getMetaCmd(configOpts *cmdconfig.Options) *cobra.Command {
opts := &getMetaOpts{}

cmd := &cobra.Command{
Use: "get-meta RESOURCE_SELECTOR",
Args: cobra.ExactArgs(1),
Short: "Get partial object metadata for Grafana resources",
Long: "Get partial object metadata (name, namespace, labels, annotations) for Grafana resources.",
Example: `
# All instances of a resource type:

grafanactl resources get-meta dashboards

# One or more specific instances:

grafanactl resources get-meta dashboards/foo
grafanactl resources get-meta dashboards/foo,bar

# Long kind format with version:

grafanactl resources get-meta dashboards.v1alpha1.dashboard.grafana.app
grafanactl resources get-meta dashboards.v1alpha1.dashboard.grafana.app/foo
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

codec, err := opts.IO.Codec()
if err != nil {
return err
}

cfg, err := configOpts.LoadRESTConfig(ctx)
if err != nil {
return err
}

sels, err := resources.ParseSelectors(args)
if err != nil {
return err
}

reg, err := discovery.NewDefaultRegistry(ctx, cfg)
if err != nil {
return err
}

filters, err := reg.MakeFilters(discovery.MakeFiltersOptions{
Selectors: sels,
PreferredVersionOnly: true,
})
if err != nil {
return err
}

if len(filters) != 1 {
return fmt.Errorf("expected exactly one resource type, got %d", len(filters))
}

filter := filters[0]

mdClient, err := metadata.NewForConfig(&cfg.Config)
if err != nil {
return err
}

gvr := filter.Descriptor.GroupVersionResource()
rc := mdClient.Resource(gvr).Namespace(cfg.Namespace)

var list metav1.PartialObjectMetadataList

switch filter.Type {
case resources.FilterTypeAll:
result, err := rc.List(ctx, metav1.ListOptions{})
if err != nil {
return err
}

list = *result

case resources.FilterTypeSingle, resources.FilterTypeMultiple:
g, ctx := errgroup.WithContext(ctx)
items := make([]metav1.PartialObjectMetadata, len(filter.ResourceUIDs))

for i, name := range filter.ResourceUIDs {
g.Go(func() error {
item, err := rc.Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}

items[i] = *item
return nil
})
}

if err := g.Wait(); err != nil {
return err
}

list.Items = items
}

return codec.Encode(cmd.OutOrStdout(), &list)
},
}

opts.setup(cmd.Flags())

return cmd
}

type partialMetaTableCodec struct {
wide bool
}

func (c *partialMetaTableCodec) Format() format.Format {
if c.wide {
return "wide"
}

return "text"
}

func (c *partialMetaTableCodec) Encode(output io.Writer, input any) error {
list, ok := input.(*metav1.PartialObjectMetadataList)
if !ok {
return fmt.Errorf("expected *metav1.PartialObjectMetadataList, got %T", input)
}

table := &metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{
{
Name: "NAME",
Type: "string",
Format: "name",
Priority: 0,
Description: "The name of the resource.",
},
{
Name: "NAMESPACE",
Type: "string",
Priority: 0,
Description: "The namespace of the resource.",
},
{
Name: "AGE",
Type: "string",
Format: "date-time",
Priority: 0,
Description: "The age of the resource.",
},
},
}

if c.wide {
table.ColumnDefinitions = append(table.ColumnDefinitions, metav1.TableColumnDefinition{
Name: "LABELS",
Type: "string",
Priority: 0,
Description: "The labels of the resource.",
})
}

for i := range list.Items {
item := &list.Items[i]
age := duration.HumanDuration(time.Since(item.CreationTimestamp.Time))

var row metav1.TableRow
if c.wide {
row = metav1.TableRow{
Cells: []any{item.Name, item.Namespace, age, labels.FormatLabels(item.Labels)},
Object: runtime.RawExtension{Object: item},
}
} else {
row = metav1.TableRow{
Cells: []any{item.Name, item.Namespace, age},
Object: runtime.RawExtension{Object: item},
}
}

table.Rows = append(table.Rows, row)
}

printer := printers.NewTablePrinter(printers.PrintOptions{
Wide: c.wide,
})

return printer.PrintObj(table, output)
}

func (c *partialMetaTableCodec) Decode(io.Reader, any) error {
return errors.New("partialMetaTableCodec does not support decoding")
}
1 change: 1 addition & 0 deletions docs/reference/cli/grafanactl_resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Manipulate Grafana resources.
* [grafanactl resources delete](grafanactl_resources_delete.md) - Delete resources from Grafana
* [grafanactl resources edit](grafanactl_resources_edit.md) - Edit resources from Grafana
* [grafanactl resources get](grafanactl_resources_get.md) - Get resources from Grafana
* [grafanactl resources get-meta](grafanactl_resources_get-meta.md) - Get partial object metadata for Grafana resources
* [grafanactl resources list](grafanactl_resources_list.md) - List available Grafana API resources
* [grafanactl resources pull](grafanactl_resources_pull.md) - Pull resources from Grafana
* [grafanactl resources push](grafanactl_resources_push.md) - Push resources to Grafana
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/cli/grafanactl_resources_get-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## grafanactl resources get-meta

Get partial object metadata for Grafana resources

### Synopsis

Get partial object metadata (name, namespace, labels, annotations) for Grafana resources.

```
grafanactl resources get-meta RESOURCE_SELECTOR [flags]
```

### Examples

```

# All instances of a resource type:

grafanactl resources get-meta dashboards

# One or more specific instances:

grafanactl resources get-meta dashboards/foo
grafanactl resources get-meta dashboards/foo,bar

# Long kind format with version:

grafanactl resources get-meta dashboards.v1alpha1.dashboard.grafana.app
grafanactl resources get-meta dashboards.v1alpha1.dashboard.grafana.app/foo

```

### Options

```
-h, --help help for get-meta
-o, --output string Output format. One of: json, text, wide, yaml (default "text")
-l, --selector string Filter resources by label selector (e.g. -l key=value,other=value)
```

### Options inherited from parent commands

```
--config string Path to the configuration file to use
--context string Name of the context to use
--no-color Disable color output
-v, --verbose count Verbose mode. Multiple -v options increase the verbosity (maximum: 3).
```

### SEE ALSO

* [grafanactl resources](grafanactl_resources.md) - Manipulate Grafana resources

1 change: 1 addition & 0 deletions docs/reference/cli/grafanactl_resources_get.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ grafanactl resources get [RESOURCE_SELECTOR]... [flags]
fail — continue processing all resources and exit 1 if any failed (default)
abort — stop on the first error and exit 1 (default "fail")
-o, --output string Output format. One of: json, text, wide, yaml (default "text")
-l, --selector string Filter resources by label selector (e.g. -l key=value,other=value)
```

### Options inherited from parent commands
Expand Down
7 changes: 4 additions & 3 deletions internal/resources/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ func (t FilterType) String() string {
// Unlike Selector, filters use the Descriptor to identify the resource type,
// which fully defines the target API resource.
type Filter struct {
Type FilterType
Descriptor Descriptor
ResourceUIDs []string
Type FilterType
Descriptor Descriptor
ResourceUIDs []string
LabelSelector string
}

func (f Filter) String() string {
Expand Down
2 changes: 1 addition & 1 deletion internal/resources/remote/puller.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (p *Puller) Pull(ctx context.Context, req PullRequest) (*OperationSummary,
errg.Go(func() error {
switch filt.Type {
case resources.FilterTypeAll:
res, err := p.client.List(ctx, filt.Descriptor, metav1.ListOptions{})
res, err := p.client.List(ctx, filt.Descriptor, metav1.ListOptions{LabelSelector: filt.LabelSelector})
if err != nil {
if req.StopOnError {
return err
Expand Down
Loading