[Prototyping] Add repository history endpoint (#97345)

This commit is contained in:
Roberto Jiménez Sánchez 2024-12-03 16:42:07 +01:00 committed by GitHub
parent d6e7edbd1e
commit a722b485da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 525 additions and 3 deletions

View File

@ -82,6 +82,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error {
&WebhookResponse{},
&ResourceWrapper{},
&FileList{},
&HistoryList{},
)
return nil
}

View File

@ -260,3 +260,27 @@ type FileItem struct {
Modified int64 `json:"modified,omitempty"`
Author string `json:"author,omitempty"`
}
// HistoryList is a list of versions of a resource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type HistoryList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// should be named "items", but avoid subresource error for now:
// kubernetes/kubernetes#126809
Items []HistoryItem `json:"items,omitempty"`
}
type Author struct {
Name string `json:"name"`
Username string `json:"username"`
AvatarURL string `json:"avatarURL,omitempty"`
}
type HistoryItem struct {
Ref string `json:"ref"`
Message string `json:"message"`
Authors []Author `json:"authors"`
CreatedAt int64 `json:"createdAt"`
}

View File

@ -11,6 +11,22 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Author) DeepCopyInto(out *Author) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Author.
func (in *Author) DeepCopy() *Author {
if in == nil {
return nil
}
out := new(Author)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EditingOptions) DeepCopyInto(out *EditingOptions) {
*out = *in
@ -115,6 +131,60 @@ func (in *HelloWorld) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HistoryItem) DeepCopyInto(out *HistoryItem) {
*out = *in
if in.Authors != nil {
in, out := &in.Authors, &out.Authors
*out = make([]Author, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoryItem.
func (in *HistoryItem) DeepCopy() *HistoryItem {
if in == nil {
return nil
}
out := new(HistoryItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HistoryList) DeepCopyInto(out *HistoryList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]HistoryItem, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HistoryList.
func (in *HistoryList) DeepCopy() *HistoryList {
if in == nil {
return nil
}
out := new(HistoryList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *HistoryList) 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 *LintIssue) DeepCopyInto(out *LintIssue) {
*out = *in

View File

@ -14,11 +14,14 @@ import (
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.EditingOptions": schema_pkg_apis_provisioning_v0alpha1_EditingOptions(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HelloWorld": schema_pkg_apis_provisioning_v0alpha1_HelloWorld(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem": schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryList": schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LintIssue": schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref),
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Repository": schema_pkg_apis_provisioning_v0alpha1_Repository(ref),
@ -33,6 +36,39 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
}
}
func schema_pkg_apis_provisioning_v0alpha1_Author(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{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"username": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"avatarURL": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name", "username"},
},
},
}
}
func schema_pkg_apis_provisioning_v0alpha1_EditingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -270,6 +306,104 @@ func schema_pkg_apis_provisioning_v0alpha1_HelloWorld(ref common.ReferenceCallba
}
}
func schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"ref": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"message": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"authors": {
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.Author"),
},
},
},
},
},
"createdAt": {
SchemaProps: spec.SchemaProps{
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
},
Required: []string{"ref", "message", "authors", "createdAt"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author"},
}
}
func schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "HistoryList is a list of versions of a resource",
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": {
SchemaProps: spec.SchemaProps{
Description: "should be named \"items\", but avoid subresource error for now: kubernetes/kubernetes#126809",
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.HistoryItem"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_provisioning_v0alpha1_LintIssue(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -534,9 +668,9 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref common.ReferenceC
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
"store": {
"existing": {
SchemaProps: spec.SchemaProps{
Description: "The value with the same name that is currently saved",
Description: "The same value, currently saved in the grafana database",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},

View File

@ -1,6 +1,6 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,HistoryItem,Authors
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,Errors
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,Lint
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,FileList,Items
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,GitHub
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,PreferYAML
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceObjects,Existing

View File

@ -0,0 +1,84 @@
package provisioning
import (
"context"
"fmt"
"log/slog"
"net/http"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
)
type historySubresource struct {
repoGetter RepoGetter
logger *slog.Logger
}
func (h *historySubresource) New() runtime.Object {
// This is added as the "ResponseType" regardless what ProducesObject() returns
return &provisioning.HistoryList{}
}
func (h *historySubresource) Destroy() {}
func (h *historySubresource) NamespaceScoped() bool {
return true
}
func (h *historySubresource) GetSingularName() string {
return "History"
}
func (h *historySubresource) ProducesMIMETypes(verb string) []string {
return []string{"application/json"}
}
func (h *historySubresource) ProducesObject(verb string) runtime.Object {
return &provisioning.HistoryList{}
}
func (h *historySubresource) ConnectMethods() []string {
return []string{http.MethodGet}
}
func (h *historySubresource) NewConnectOptions() (runtime.Object, bool, string) {
return nil, true, "" // true adds the {path} component
}
func (h *historySubresource) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
logger := h.logger.With("repository_name", name)
repo, err := h.repoGetter.GetRepository(ctx, name)
if err != nil {
logger.DebugContext(ctx, "failed to find repository", "error", err)
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
ref := query.Get("ref")
logger = logger.With("ref", ref)
ctx := r.Context()
var filePath string
prefix := fmt.Sprintf("/%s/history/", name)
idx := strings.Index(r.URL.Path, prefix)
if idx != -1 {
filePath = r.URL.Path[idx+len(prefix):]
}
logger = logger.With("path", filePath)
commits, err := repo.History(ctx, logger, filePath, ref)
if err != nil {
logger.DebugContext(ctx, "failed to get history", "error", err)
responder.Error(err)
return
}
responder.Object(http.StatusOK, &provisioning.HistoryList{Items: commits})
}), nil
}

View File

@ -198,6 +198,10 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv
client: b.client,
logger: b.logger.With("connector", "files"),
}
storage[provisioning.RepositoryResourceInfo.StoragePath("history")] = &historySubresource{
repoGetter: b,
logger: b.logger.With("connector", "history"),
}
storage[provisioning.RepositoryResourceInfo.StoragePath("import")] = &importConnector{
repoGetter: b,
client: b.client,
@ -562,6 +566,18 @@ func (b *ProvisioningAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.
sub.Post.Parameters = []*spec3.Parameter{ref}
}
sub = oas.Paths.Paths[repoprefix+"/history"]
if sub != nil {
sub.Get.Description = "Get the history of the repository"
sub.Get.Parameters = []*spec3.Parameter{ref}
}
sub = oas.Paths.Paths[repoprefix+"/history/{path}"]
if sub != nil {
sub.Get.Description = "Get the history of a path"
sub.Get.Parameters = []*spec3.Parameter{ref}
}
// Show a special list command
sub = oas.Paths.Paths[repoprefix+"/files"]
if sub != nil {

View File

@ -264,6 +264,55 @@ func (r *githubRepository) Delete(ctx context.Context, logger *slog.Logger, path
return r.gh.DeleteFile(ctx, owner, repo, path, ref, comment, file.GetSHA())
}
func (r *githubRepository) History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error) {
if ref == "" {
ref = r.config.Spec.GitHub.Branch
}
commits, err := r.gh.Commits(ctx, r.config.Spec.GitHub.Owner, r.config.Spec.GitHub.Repository, path, ref)
if err != nil {
if errors.Is(err, pgh.ErrResourceNotFound) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "path not found",
Code: http.StatusNotFound,
},
}
}
return nil, fmt.Errorf("get commits: %w", err)
}
ret := make([]provisioning.HistoryItem, 0, len(commits))
for _, commit := range commits {
authors := make([]provisioning.Author, 0)
if commit.Author != nil {
authors = append(authors, provisioning.Author{
Name: commit.Author.Name,
Username: commit.Author.Username,
AvatarURL: commit.Author.AvatarURL,
})
}
if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name {
authors = append(authors, provisioning.Author{
Name: commit.Committer.Name,
Username: commit.Committer.Username,
AvatarURL: commit.Committer.AvatarURL,
})
}
ret = append(ret, provisioning.HistoryItem{
Ref: commit.Ref,
Message: commit.Message,
Authors: authors,
CreatedAt: commit.CreatedAt.UnixNano(),
})
}
return ret, nil
}
// basicGitBranchNameRegex is a regular expression to validate a git branch name
// it does not cover all cases as positive lookaheads are not supported in Go's regexp
var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`)

View File

@ -5,6 +5,7 @@ package github
import (
"context"
"errors"
"time"
"github.com/google/go-github/v66/github"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -66,6 +67,9 @@ type Client interface {
// If ".." appears in the "path", this method will return an error.
DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error
// Commits returns the commits for the given path
Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error)
// CreateBranch creates a new branch in the repository.
CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error
// BranchExists checks if a branch exists in the repository.
@ -101,6 +105,20 @@ type RepositoryContent interface {
GetSize() int64
}
type CommitAuthor struct {
Name string
Username string
AvatarURL string
}
type Commit struct {
Ref string
Message string
Author *CommitAuthor
Committer *CommitAuthor
CreatedAt time.Time
}
type CommitFile interface {
GetSHA() string
GetFilename() string

View File

@ -59,6 +59,36 @@ func (_m *MockClient) ClearAllPullRequestFileComments(ctx context.Context, owner
return r0
}
// Commits provides a mock function with given fields: ctx, owner, repository, path, branch
func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) {
ret := _m.Called(ctx, owner, repository, path, branch)
if len(ret) == 0 {
panic("no return value specified for Commits")
}
var r0 []Commit
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]Commit, error)); ok {
return rf(ctx, owner, repository, path, branch)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []Commit); ok {
r0 = rf(ctx, owner, repository, path, branch)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]Commit)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok {
r1 = rf(ctx, owner, repository, path, branch)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName
func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error {
ret := _m.Called(ctx, owner, repository, sourceBranch, branchName)

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/google/go-github/v66/github"
)
@ -183,6 +184,62 @@ func (r *realImpl) DeleteFile(ctx context.Context, owner, repository, path, bran
return err
}
func (r *realImpl) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) {
commits, _, err := r.gh.Repositories.ListCommits(ctx, owner, repository, &github.CommitsListOptions{
Path: path,
SHA: branch,
})
if err != nil {
var ghErr *github.ErrorResponse
if !errors.As(err, &ghErr) {
return nil, err
}
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
return nil, ErrServiceUnavailable
}
if ghErr.Response.StatusCode == http.StatusNotFound {
return nil, ErrResourceNotFound
}
return nil, err
}
ret := make([]Commit, 0, len(commits))
for _, c := range commits {
var createdAt time.Time
var author *CommitAuthor
if c.GetCommit().GetAuthor() != nil {
author = &CommitAuthor{
Name: c.GetCommit().GetAuthor().GetName(),
Username: c.GetAuthor().GetLogin(),
AvatarURL: c.GetAuthor().GetAvatarURL(),
}
createdAt = c.GetCommit().GetAuthor().GetDate().Time
}
var committer *CommitAuthor
if c.GetCommitter() != nil {
committer = &CommitAuthor{
Name: c.GetCommit().GetCommitter().GetName(),
Username: c.GetCommitter().GetLogin(),
AvatarURL: c.GetCommitter().GetAvatarURL(),
}
}
ret = append(ret, Commit{
Ref: c.GetSHA(),
Message: c.GetCommit().GetMessage(),
Author: author,
Committer: committer,
CreatedAt: createdAt,
})
}
return ret, nil
}
func (r *realImpl) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error {
// Fail if the branch already exists
if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil {

View File

@ -267,6 +267,15 @@ func (r *localRepository) Delete(ctx context.Context, logger *slog.Logger, path
return os.Remove(filepath.Join(r.path, path))
}
func (r *localRepository) History(ctx context.Context, logger *slog.Logger, path string, ref string) ([]provisioning.HistoryItem, error) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Message: "history is not yet implemented",
Code: http.StatusNotImplemented,
},
}
}
// Webhook implements provisioning.Repository.
func (r *localRepository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
// webhooks are not supported with local

View File

@ -90,6 +90,9 @@ type Repository interface {
// Delete a file in the remote repository
Delete(ctx context.Context, logger *slog.Logger, path, ref, message string) error
// History of changes for a path
History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error)
// For repositories that support webhooks
Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc
// Hooks called after the repository has been created, updated or deleted

View File

@ -99,6 +99,15 @@ func (r *s3Repository) Delete(ctx context.Context, logger *slog.Logger, path str
}
}
func (r *s3Repository) History(ctx context.Context, logger *slog.Logger, path string, ref string) ([]provisioning.HistoryItem, error) {
return nil, &errors.StatusError{
ErrStatus: metav1.Status{
Message: "history is not yet implemented",
Code: http.StatusNotImplemented,
},
}
}
// Webhook implements provisioning.Repository.
func (r *s3Repository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
// webhooks are not supported with local

View File

@ -94,6 +94,15 @@ func (r *unknownRepository) Delete(ctx context.Context, logger *slog.Logger, pat
}
}
func (r *unknownRepository) History(ctx context.Context, logger *slog.Logger, path, ref string) ([]provisioning.HistoryItem, error) {
return nil, &errors.StatusError{
ErrStatus: metav1.Status{
Message: "history is not yet implemented",
Code: http.StatusNotImplemented,
},
}
}
// Webhook implements provisioning.Repository.
func (r *unknownRepository) Webhook(ctx context.Context, logger *slog.Logger, responder rest.Responder, factory FileReplicatorFactory) http.HandlerFunc {
// webhooks are not supported with local

View File

@ -121,6 +121,15 @@ func TestIntegrationProvisioning(t *testing.T) {
"get"
]
},
{
"name": "repositories/history",
"singularName": "",
"namespaced": true,
"kind": "HistoryList",
"verbs": [
"get"
]
},
{
"name": "repositories/import",
"singularName": "",