Provisioning: Add refs endpoint (#108014)
This commit is contained in:
@@ -119,6 +119,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
|
||||
&ResourceStats{},
|
||||
&Job{},
|
||||
&JobList{},
|
||||
&RefList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user