mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GRN parsing service (#56750)
* GRN parsing service * move GRN package into infra and update fields * remove orgID from GRNs (collapse into tenantID)
This commit is contained in:
13
pkg/infra/grn/doc.go
Normal file
13
pkg/infra/grn/doc.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// package GRN provides utilities for working with Grafana Resource Names
|
||||
// (GRNs).
|
||||
|
||||
// A GRN is an identifier which encodes all data necessary to retrieve a given
|
||||
// resource from its respective service.
|
||||
|
||||
// A GRN string is expressed in the format:
|
||||
//
|
||||
// grn:${tenant_id}:${kind}/${id}
|
||||
//
|
||||
// The format of the final id is defined by the owning service and not
|
||||
// validated by the GRN parser. Prefer using UIDs where possible.
|
||||
package grn
|
||||
9
pkg/infra/grn/errors.go
Normal file
9
pkg/infra/grn/errors.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package grn
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidGRN = errutil.NewBase(errutil.StatusValidationFailed, "grn.InvalidGRN")
|
||||
)
|
||||
75
pkg/infra/grn/grn.go
Normal file
75
pkg/infra/grn/grn.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package grn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/pkg/strconv"
|
||||
)
|
||||
|
||||
type GRN struct {
|
||||
// TenantID contains the ID of the tenant (in hosted grafana) or
|
||||
// organization (in other environments) the resource belongs to. This field
|
||||
// may be omitted for global Grafana resources which are not associated with
|
||||
// an organization.
|
||||
TenantID int
|
||||
|
||||
// The kind of resource being identified, for e.g. "dashboard" or "user".
|
||||
// The caller is responsible for validating the value.
|
||||
ResourceKind string
|
||||
|
||||
// ResourceIdentifier is used by the underlying service to identify the
|
||||
// resource.
|
||||
ResourceIdentifier string
|
||||
}
|
||||
|
||||
// ParseStr attempts to parse a string into a GRN. It returns an error if the
|
||||
// given string does not match the GRN format, but does not validate the values.
|
||||
func ParseStr(str string) (GRN, error) {
|
||||
ret := GRN{}
|
||||
parts := strings.Split(str, ":")
|
||||
|
||||
if len(parts) != 3 {
|
||||
return ret, ErrInvalidGRN.Errorf("%q is not a complete GRN", str)
|
||||
}
|
||||
|
||||
if parts[0] != "grn" {
|
||||
return ret, ErrInvalidGRN.Errorf("%q does not look like a GRN", str)
|
||||
}
|
||||
|
||||
// split the final segment into Kind and ID. This only splits after the
|
||||
// first occurrence of "/"; a ResourceIdentifier may contain "/"
|
||||
kind, id, found := strings.Cut(parts[2], "/")
|
||||
if !found { // missing "/"
|
||||
return ret, ErrInvalidGRN.Errorf("invalid resource identifier in GRN %q", str)
|
||||
}
|
||||
ret.ResourceIdentifier = id
|
||||
ret.ResourceKind = kind
|
||||
|
||||
if parts[1] != "" {
|
||||
tID, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return ret, ErrInvalidGRN.Errorf("ID segment cannot be converted to an integer")
|
||||
} else {
|
||||
ret.TenantID = tID
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// MustParseStr is a wrapper around ParseStr that panics if the given input is
|
||||
// not a valid GRN. This is intended for use in tests.
|
||||
func MustParseStr(str string) GRN {
|
||||
grn, err := ParseStr(str)
|
||||
if err != nil {
|
||||
panic("bad grn!")
|
||||
}
|
||||
return grn
|
||||
}
|
||||
|
||||
// String returns a string representation of a grn in the format
|
||||
// grn:tenantID:kind/resourceIdentifier
|
||||
func (g *GRN) String() string {
|
||||
return fmt.Sprintf("grn:%d:%s/%s", g.TenantID, g.ResourceKind, g.ResourceIdentifier)
|
||||
}
|
||||
77
pkg/infra/grn/grn_test.go
Normal file
77
pkg/infra/grn/grn_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package grn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseGRNStr(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expect GRN
|
||||
expectErr bool
|
||||
}{
|
||||
{ // empty
|
||||
"",
|
||||
GRN{},
|
||||
true,
|
||||
},
|
||||
{ // too few parts
|
||||
"grn:dashboards",
|
||||
GRN{},
|
||||
true,
|
||||
},
|
||||
{ // too many parts
|
||||
"grn::dashboards:user:orgs:otherthings:hello:stillgoing",
|
||||
GRN{},
|
||||
true,
|
||||
},
|
||||
{ // Does not look like a GRN
|
||||
"hrn:grafana::123:dashboards/foo",
|
||||
GRN{},
|
||||
true,
|
||||
},
|
||||
{ // Missing Kind
|
||||
"grn::foo",
|
||||
GRN{},
|
||||
true,
|
||||
},
|
||||
{ // good!
|
||||
"grn::roles/Admin",
|
||||
GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin"},
|
||||
false,
|
||||
},
|
||||
{ // good!
|
||||
"grn::roles/Admin/with/some/slashes",
|
||||
GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "Admin/with/some/slashes"},
|
||||
false,
|
||||
},
|
||||
{ // good!
|
||||
"grn:123456789:roles/Admin/with/some/slashes",
|
||||
GRN{TenantID: 123456789, ResourceKind: "roles", ResourceIdentifier: "Admin/with/some/slashes"},
|
||||
false,
|
||||
},
|
||||
{ // Weird, but valid.
|
||||
"grn::roles///Admin/with/leading/slashes",
|
||||
GRN{TenantID: 0, ResourceKind: "roles", ResourceIdentifier: "//Admin/with/leading/slashes"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("ParseStr(%q)", test.input), func(t *testing.T) {
|
||||
got, err := ParseStr(test.input)
|
||||
if test.expectErr && err == nil {
|
||||
t.Fatal("wrong result. Expected error, got success")
|
||||
}
|
||||
|
||||
if err != nil && !test.expectErr {
|
||||
t.Fatalf("wrong result. Expected success, got error %s", err.Error())
|
||||
}
|
||||
|
||||
if got != test.expect {
|
||||
t.Fatalf("wrong result. Wanted %s, got %s\n", test.expect.String(), got.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user