Provisioning: Add refs endpoint (#108014)

This commit is contained in:
Roberto Jiménez Sánchez
2025-07-11 13:58:39 -07:00
committed by GitHub
parent a33b634a9f
commit 05a2d26787
17 changed files with 727 additions and 0 deletions
@@ -119,6 +119,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
&ResourceStats{},
&Job{},
&JobList{},
&RefList{},
)
return nil
}
+18
View File
@@ -454,3 +454,21 @@ type HistoryItem struct {
Authors []Author `json:"authors"`
CreatedAt int64 `json:"createdAt"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type RefList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// +listType=atomic
Items []RefItem `json:"items"`
}
type RefItem struct {
// The name of the reference (branch or tag)
Name string `json:"name"`
// The SHA hash of the commit this ref points to
Hash string `json:"hash,omitempty"`
// The URL to the reference (branch or tag)
RefURL string `json:"refURL,omitempty"`
}
@@ -442,6 +442,53 @@ func (in *PullRequestJobOptions) DeepCopy() *PullRequestJobOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RefItem) DeepCopyInto(out *RefItem) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefItem.
func (in *RefItem) DeepCopy() *RefItem {
if in == nil {
return nil
}
out := new(RefItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RefList) DeepCopyInto(out *RefList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]RefItem, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefList.
func (in *RefList) DeepCopy() *RefList {
if in == nil {
return nil
}
out := new(RefList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RefList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Repository) DeepCopyInto(out *Repository) {
*out = *in
@@ -33,6 +33,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ManagerStats": schema_pkg_apis_provisioning_v0alpha1_ManagerStats(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.MigrateJobOptions": schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.PullRequestJobOptions": schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem": schema_pkg_apis_provisioning_v0alpha1_RefItem(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefList": schema_pkg_apis_provisioning_v0alpha1_RefList(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Repository": schema_pkg_apis_provisioning_v0alpha1_Repository(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryList": schema_pkg_apis_provisioning_v0alpha1_RepositoryList(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositorySpec": schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref),
@@ -950,6 +952,94 @@ func schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref common.Refe
}
}
func schema_pkg_apis_provisioning_v0alpha1_RefItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "The name of the reference (branch or tag)",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"hash": {
SchemaProps: spec.SchemaProps{
Description: "The SHA hash of the commit this ref points to",
Type: []string{"string"},
Format: "",
},
},
"refURL": {
SchemaProps: spec.SchemaProps{
Description: "The URL to the reference (branch or tag)",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name"},
},
},
}
}
func schema_pkg_apis_provisioning_v0alpha1_RefList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_provisioning_v0alpha1_Repository(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -4,6 +4,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Errors
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Summary
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ManagerStats,Stats
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RefList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryList,Items
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryView,Workflows
+85
View File
@@ -0,0 +1,85 @@
package provisioning
import (
"context"
"net/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana-app-sdk/logging"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
type refsConnector struct {
getter RepoGetter
}
func NewRefsConnector(getter RepoGetter) *refsConnector {
return &refsConnector{getter: getter}
}
func (*refsConnector) New() runtime.Object {
return &provisioning.RefList{}
}
func (*refsConnector) Destroy() {}
func (*refsConnector) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}
}
func (*refsConnector) ProducesObject(verb string) any {
return &provisioning.RefList{}
}
func (*refsConnector) ConnectMethods() []string {
return []string{http.MethodGet}
}
func (*refsConnector) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, ""
}
func (c *refsConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
logger := logging.FromContext(ctx).With("logger", "refs-connector", "repository_name", name)
ctx = logging.Context(ctx, logger)
repo, err := c.getter.GetHealthyRepository(ctx, name)
if err != nil {
logger.Debug("failed to find repository", "error", err)
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
responder.Error(apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method))
return
}
versionedRepo, ok := repo.(repository.Versioned)
if !ok {
responder.Error(apierrors.NewBadRequest("repository does not support versioned operations"))
return
}
refs, err := versionedRepo.ListRefs(ctx)
if err != nil {
responder.Error(err)
return
}
refsList := &provisioning.RefList{
Items: refs,
}
responder.Object(http.StatusOK, refsList)
}), nil
}
var (
_ rest.Storage = (*refsConnector)(nil)
_ rest.Connecter = (*refsConnector)(nil)
_ rest.StorageMetadata = (*refsConnector)(nil)
)
@@ -199,6 +199,7 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
// * Reading and modifying a repository's configuration requires administrator privileges.
// * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges.
// * Reading a repository's files requires viewer privileges.
// * Reading a repository's refs requires viewer privileges.
// * Editing a repository's files requires editor privileges.
// * Syncing a repository requires editor privileges.
// * Exporting a repository requires administrator privileges.
@@ -230,6 +231,12 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
}
return authorizer.DecisionDeny, "admin role is required", nil
case "refs":
// This is strictly a read operation. It is handy on the frontend for viewers.
if id.GetOrgRole().Includes(identity.RoleViewer) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "viewer role is required", nil
case "files":
// Access to files is controlled by the AccessClient
return authorizer.DecisionAllow, "", nil
@@ -350,6 +357,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
getter: b,
}
storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access)
storage[provisioning.RepositoryResourceInfo.StoragePath("refs")] = NewRefsConnector(b)
storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{
getter: b,
lister: b.resourceLister,
@@ -764,6 +772,22 @@ func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, err
sub.Get.Parameters = []*spec3.Parameter{ref}
}
// Show refs endpoint documentation
sub = oas.Paths.Paths[repoprefix+"/refs"]
if sub != nil {
sub.Get.Description = "Get the repository references"
sub.Get.Summary = "Repository refs listing"
sub.Get.Parameters = []*spec3.Parameter{}
sub.Post = nil
sub.Put = nil
sub.Delete = nil
// Replace the content type for this response
mt := sub.Get.Responses.StatusCodeResponses[200].Content
s := defs[defsBase+"RefList"].Schema
mt["*/*"].Schema = &s
}
// Show a special list command
sub = oas.Paths.Paths[repoprefix+"/files"]
if sub != nil {
@@ -393,6 +393,64 @@ func (_c *MockGitRepository_LatestRef_Call) RunAndReturn(run func(context.Contex
return _c
}
// ListRefs provides a mock function with given fields: ctx
func (_m *MockGitRepository) ListRefs(ctx context.Context) ([]v0alpha1.RefItem, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for ListRefs")
}
var r0 []v0alpha1.RefItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]v0alpha1.RefItem, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) []v0alpha1.RefItem); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]v0alpha1.RefItem)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockGitRepository_ListRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRefs'
type MockGitRepository_ListRefs_Call struct {
*mock.Call
}
// ListRefs is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockGitRepository_Expecter) ListRefs(ctx interface{}) *MockGitRepository_ListRefs_Call {
return &MockGitRepository_ListRefs_Call{Call: _e.mock.On("ListRefs", ctx)}
}
func (_c *MockGitRepository_ListRefs_Call) Run(run func(ctx context.Context)) *MockGitRepository_ListRefs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockGitRepository_ListRefs_Call) Return(_a0 []v0alpha1.RefItem, _a1 error) *MockGitRepository_ListRefs_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockGitRepository_ListRefs_Call) RunAndReturn(run func(context.Context) ([]v0alpha1.RefItem, error)) *MockGitRepository_ListRefs_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: ctx, path, ref
func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) {
ret := _m.Called(ctx, path, ref)
@@ -476,6 +476,27 @@ func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provis
}}
}
func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) {
refs, err := r.client.ListRefs(ctx)
if err != nil {
return nil, fmt.Errorf("list refs: %w", err)
}
refItems := make([]provisioning.RefItem, 0, len(refs))
for _, ref := range refs {
// Only branches
if !strings.HasPrefix(ref.Name, "refs/heads/") {
continue
}
refItems = append(refItems, provisioning.RefItem{
Name: strings.TrimPrefix(ref.Name, "refs/heads/"),
Hash: ref.Hash.String(),
})
}
return refItems, nil
}
func (r *gitRepository) LatestRef(ctx context.Context) (string, error) {
ctx, _ = r.logger(ctx, "")
branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch))
@@ -1190,6 +1190,97 @@ func TestGitRepository_Delete(t *testing.T) {
}
}
func TestGitRepository_ListRefs(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.FakeClient)
gitConfig RepositoryConfig
wantError bool
wantRefs []provisioning.RefItem
errorType error
}{
{
name: "success - list refs",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.ListRefsReturns([]nanogit.Ref{
{
Name: "refs/heads/main",
Hash: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"),
},
{
Name: "refs/heads/feature",
Hash: hash.MustFromHex("1234567890abcdef1234567890abcdef12345678"),
},
{
Name: "refs/tags/v1.0.0",
Hash: hash.MustFromHex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
},
}, nil)
},
gitConfig: RepositoryConfig{
Branch: "main",
},
wantError: false,
wantRefs: []provisioning.RefItem{
{
Name: "main",
Hash: "abcdef1234567890abcdef1234567890abcdef12",
},
{
Name: "feature",
Hash: "1234567890abcdef1234567890abcdef12345678",
},
},
},
{
name: "failure - list refs error",
setupMock: func(mockClient *mocks.FakeClient) {
mockClient.ListRefsReturns(nil, errors.New("list refs failed"))
},
gitConfig: RepositoryConfig{
Branch: "main",
},
wantError: true,
errorType: errors.New("list refs failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &mocks.FakeClient{}
tt.setupMock(mockClient)
gitRepo := &gitRepository{
client: mockClient,
gitConfig: tt.gitConfig,
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
Type: provisioning.GitHubRepositoryType,
},
},
}
refs, err := gitRepo.ListRefs(context.Background())
if tt.wantError {
require.Error(t, err)
if tt.errorType != nil {
require.Contains(t, err.Error(), tt.errorType.Error())
}
require.Nil(t, refs)
} else {
require.NoError(t, err)
require.NotNil(t, refs)
require.Equal(t, len(tt.wantRefs), len(refs))
for i, wantRef := range tt.wantRefs {
require.Equal(t, wantRef.Name, refs[i].Name)
require.Equal(t, wantRef.Hash, refs[i].Hash)
}
}
})
}
}
func TestGitRepository_LatestRef(t *testing.T) {
tests := []struct {
name string
@@ -200,6 +200,20 @@ func (r *githubRepository) History(ctx context.Context, path, ref string) ([]pro
return ret, nil
}
// ListRefs list refs from the git repository and add the ref URL to the ref item
func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) {
refs, err := r.gitRepo.ListRefs(ctx)
if err != nil {
return nil, fmt.Errorf("list refs: %w", err)
}
for i := range refs {
refs[i].RefURL = fmt.Sprintf("%s/tree/%s", r.config.Spec.GitHub.URL, refs[i].Name)
}
return refs, nil
}
func (r *githubRepository) LatestRef(ctx context.Context) (string, error) {
return r.gitRepo.LatestRef(ctx)
}
@@ -921,6 +921,40 @@ func TestGitHubRepositoryDelegation(t *testing.T) {
mockGitRepo.AssertExpectations(t)
})
t.Run("ListRefs delegates to git repo but adds ref URL", func(t *testing.T) {
mockGitRepo := git.NewMockGitRepository(t)
// The git repo returns refs without RefURL
gitRepoRefs := []provisioning.RefItem{
{Name: "main", Hash: "abc123def456"},
{Name: "feature", Hash: "def456ghi789"},
}
mockGitRepo.On("ListRefs", ctx).Return(gitRepoRefs, nil)
repo := &githubRepository{
config: config,
gitRepo: mockGitRepo,
}
result, err := repo.ListRefs(ctx)
require.NoError(t, err)
// The returned refs should have RefURL set
expectedRefs := []provisioning.RefItem{
{
Name: "main",
Hash: "abc123def456",
RefURL: "https://github.com/grafana/grafana/tree/main",
},
{
Name: "feature",
Hash: "def456ghi789",
RefURL: "https://github.com/grafana/grafana/tree/feature",
},
}
assert.Equal(t, expectedRefs, result)
mockGitRepo.AssertExpectations(t)
})
t.Run("CompareFiles delegates to git repo", func(t *testing.T) {
mockGitRepo := git.NewMockGitRepository(t)
expectedChanges := []repository.VersionedFileChange{
@@ -167,5 +167,6 @@ type Versioned interface {
// History of changes for a path
History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error)
LatestRef(ctx context.Context) (string, error)
ListRefs(ctx context.Context) ([]provisioning.RefItem, error)
CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error)
}
@@ -198,6 +198,64 @@ func (_c *MockVersioned_LatestRef_Call) RunAndReturn(run func(context.Context) (
return _c
}
// ListRefs provides a mock function with given fields: ctx
func (_m *MockVersioned) ListRefs(ctx context.Context) ([]v0alpha1.RefItem, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for ListRefs")
}
var r0 []v0alpha1.RefItem
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]v0alpha1.RefItem, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) []v0alpha1.RefItem); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]v0alpha1.RefItem)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockVersioned_ListRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRefs'
type MockVersioned_ListRefs_Call struct {
*mock.Call
}
// ListRefs is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockVersioned_Expecter) ListRefs(ctx interface{}) *MockVersioned_ListRefs_Call {
return &MockVersioned_ListRefs_Call{Call: _e.mock.On("ListRefs", ctx)}
}
func (_c *MockVersioned_ListRefs_Call) Run(run func(ctx context.Context)) *MockVersioned_ListRefs_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockVersioned_ListRefs_Call) Return(_a0 []v0alpha1.RefItem, _a1 error) *MockVersioned_ListRefs_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockVersioned_ListRefs_Call) RunAndReturn(run func(context.Context) ([]v0alpha1.RefItem, error)) *MockVersioned_ListRefs_Call {
_c.Call.Return(run)
return _c
}
// NewMockVersioned creates a new instance of MockVersioned. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockVersioned(t interface {
@@ -1859,6 +1859,79 @@
}
]
},
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/repositories/{name}/refs": {
"get": {
"tags": [
"Repository"
],
"summary": "Repository refs listing",
"description": "Get the repository references",
"operationId": "getRepositoryRefs",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "object",
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"items": {
"type": "array",
"items": {
"default": {}
},
"x-kubernetes-list-type": "atomic"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"default": {}
}
}
}
}
}
}
},
"x-kubernetes-action": "connect",
"x-kubernetes-group-version-kind": {
"group": "provisioning.grafana.app",
"version": "v0alpha1",
"kind": "RefList"
}
},
"parameters": [
{
"name": "name",
"in": "path",
"description": "name of the RefList",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
},
{
"name": "namespace",
"in": "path",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"schema": {
"type": "string",
"uniqueItems": true
}
}
]
},
"/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/repositories/{name}/render/{guid}": {
"get": {
"tags": [
@@ -3037,6 +3110,70 @@
}
}
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefItem": {
"type": "object",
"required": [
"name"
],
"properties": {
"hash": {
"description": "The SHA hash of the commit this ref points to",
"type": "string"
},
"name": {
"description": "The name of the reference (branch or tag)",
"type": "string",
"default": ""
},
"refURL": {
"description": "The URL to the reference (branch or tag)",
"type": "string"
}
}
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefList": {
"type": "object",
"required": [
"items"
],
"properties": {
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
},
"items": {
"type": "array",
"items": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefItem"
}
]
},
"x-kubernetes-list-type": "atomic"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
},
"metadata": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
}
]
}
},
"x-kubernetes-group-version-kind": [
{
"group": "provisioning.grafana.app",
"kind": "RefList",
"version": "v0alpha1"
}
]
},
"com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.Repository": {
"description": "When this code is changed, make sure to update the code generation. As of writing, this can be done via the hack dir in the root of the repo: ./hack/update-codegen.sh provisioning If you've opened the generated files in this dir at some point in VSCode, you may also have to re-open them to clear errors.",
"type": "object",
@@ -83,6 +83,36 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
Suffix("files/").
Do(context.Background())
require.NoError(t, rsp.Error())
// Verify that we can list refs
rsp = helper.AdminREST.Get().
Namespace("default").
Resource("repositories").
Name(name).
Suffix("refs").
Do(context.Background())
if expectedRepo.Spec.Type == provisioning.LocalRepositoryType {
require.ErrorContains(t, rsp.Error(), "does not support versioned operations")
} else {
require.NoError(t, rsp.Error())
refs := &provisioning.RefList{}
err = rsp.Into(refs)
require.NoError(t, err)
require.True(t, len(refs.Items) >= 1, "should have at least one ref")
var foundBranch bool
for _, ref := range refs.Items {
// FIXME: this assertion should be improved for all git types and take things from config
if ref.Name == "integration-test" {
require.Equal(t, "0f3370c212b04b9704e00f6926ef339bf91c7a1b", ref.Hash)
require.Equal(t, "https://github.com/grafana/grafana-git-sync-demo/tree/integration-test", ref.RefURL)
foundBranch = true
}
}
require.True(t, foundBranch, "branch should be found")
}
})
}
@@ -229,6 +229,10 @@ const injectedRtkApi = api
query: (queryArg) => ({ url: `/repositories/${queryArg.name}/jobs/${queryArg.uid}` }),
providesTags: ['Repository'],
}),
getRepositoryRefs: build.query<GetRepositoryRefsApiResponse, GetRepositoryRefsApiArg>({
query: (queryArg) => ({ url: `/repositories/${queryArg.name}/refs` }),
providesTags: ['Repository'],
}),
getRepositoryRenderWithPath: build.query<
GetRepositoryRenderWithPathApiResponse,
GetRepositoryRenderWithPathApiArg
@@ -590,6 +594,18 @@ export type GetRepositoryJobsWithPathApiArg = {
/** Original Job UID */
uid: string;
};
export type GetRepositoryRefsApiResponse = /** status 200 OK */ {
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
apiVersion?: string;
items: any[];
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
metadata?: any;
};
export type GetRepositoryRefsApiArg = {
/** name of the RefList */
name: string;
};
export type GetRepositoryRenderWithPathApiResponse = unknown;
export type GetRepositoryRenderWithPathApiArg = {
/** name of the Repository */
@@ -1222,6 +1238,7 @@ export const {
useGetRepositoryJobsQuery,
useCreateRepositoryJobsMutation,
useGetRepositoryJobsWithPathQuery,
useGetRepositoryRefsQuery,
useGetRepositoryRenderWithPathQuery,
useGetRepositoryResourcesQuery,
useGetRepositoryStatusQuery,