diff --git a/pkg/apis/provisioning/v0alpha1/register.go b/pkg/apis/provisioning/v0alpha1/register.go index ad99a42bdce..bb7ddaa2a9c 100644 --- a/pkg/apis/provisioning/v0alpha1/register.go +++ b/pkg/apis/provisioning/v0alpha1/register.go @@ -82,6 +82,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error { &WebhookResponse{}, &ResourceWrapper{}, &FileList{}, + &HistoryList{}, ) return nil } diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index 2ea84ee7c62..9b09d136dd3 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -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"` +} diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 13361647a7e..799eba91ee7 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index b59451417a9..94b26c8ad1f 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -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"), }, }, diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index 5534f211faa..526bfe363ea 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -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 diff --git a/pkg/registry/apis/provisioning/history.go b/pkg/registry/apis/provisioning/history.go new file mode 100644 index 00000000000..96aabd60a21 --- /dev/null +++ b/pkg/registry/apis/provisioning/history.go @@ -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 +} diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 1cde0d50559..977208b8409 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -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 { diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go index 4b82d512851..c59affc147b 100644 --- a/pkg/registry/apis/provisioning/repository/github.go +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -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\-\_\/\.]+$`) diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go index 56939156d1c..086067c17bf 100644 --- a/pkg/registry/apis/provisioning/repository/github/client.go +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -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 diff --git a/pkg/registry/apis/provisioning/repository/github/client_mock.go b/pkg/registry/apis/provisioning/repository/github/client_mock.go index 82445488f89..1ac250eb505 100644 --- a/pkg/registry/apis/provisioning/repository/github/client_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/client_mock.go @@ -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) diff --git a/pkg/registry/apis/provisioning/repository/github/real.go b/pkg/registry/apis/provisioning/repository/github/real.go index 542897a9705..9d43bceea30 100644 --- a/pkg/registry/apis/provisioning/repository/github/real.go +++ b/pkg/registry/apis/provisioning/repository/github/real.go @@ -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 { diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local.go index d525a1c9ec3..76747bb6c60 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local.go @@ -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 diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index 69cbac13c1b..135285d3fef 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -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 diff --git a/pkg/registry/apis/provisioning/repository/s3.go b/pkg/registry/apis/provisioning/repository/s3.go index a3681a8e15c..fd4491709f7 100644 --- a/pkg/registry/apis/provisioning/repository/s3.go +++ b/pkg/registry/apis/provisioning/repository/s3.go @@ -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 diff --git a/pkg/registry/apis/provisioning/repository/unknown.go b/pkg/registry/apis/provisioning/repository/unknown.go index eae74ec2d42..0144b619f0b 100644 --- a/pkg/registry/apis/provisioning/repository/unknown.go +++ b/pkg/registry/apis/provisioning/repository/unknown.go @@ -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 diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index ffe3145670f..c3c255dceca 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -121,6 +121,15 @@ func TestIntegrationProvisioning(t *testing.T) { "get" ] }, + { + "name": "repositories/history", + "singularName": "", + "namespaced": true, + "kind": "HistoryList", + "verbs": [ + "get" + ] + }, { "name": "repositories/import", "singularName": "",