diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go index e779c5fafd3..5f713a1471d 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go @@ -124,3 +124,7 @@ func (u *Unstructured) GetNestedInt64(fields ...string) int64 { } return val } + +func (u *Unstructured) ToKubernetesObject() *unstructured.Unstructured { + return &unstructured.Unstructured{Object: u.Object} +} diff --git a/pkg/registry/apis/provisioning/export.go b/pkg/registry/apis/provisioning/export.go index 75059acc04d..6aca00ade2e 100644 --- a/pkg/registry/apis/provisioning/export.go +++ b/pkg/registry/apis/provisioning/export.go @@ -53,7 +53,7 @@ func (c *exportConnector) ProducesObject(verb string) any { } func (*exportConnector) ConnectMethods() []string { - return []string{http.MethodGet, http.MethodPost} + return []string{http.MethodPost} } func (*exportConnector) NewConnectOptions() (runtime.Object, bool, string) { diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go index cc4f114468e..0f6525a4f76 100644 --- a/pkg/registry/apis/provisioning/files.go +++ b/pkg/registry/apis/provisioning/files.go @@ -134,12 +134,7 @@ func (s *filesConnector) getParser(repo repository.Repository) (*fileParser, err if err != nil { return nil, err } - return &fileParser{ - namespace: ns, - repo: repo, - client: client, - kinds: newKindsLookup(client), - }, nil + return newFileParser(ns, repo, client, newKindsLookup(client)), nil } func (s *filesConnector) doRead(ctx context.Context, logger *slog.Logger, repo repository.Repository, path string, ref string) (int, *provisioning.ResourceWrapper, error) { diff --git a/pkg/registry/apis/provisioning/import.go b/pkg/registry/apis/provisioning/import.go new file mode 100644 index 00000000000..2bc65a15d8c --- /dev/null +++ b/pkg/registry/apis/provisioning/import.go @@ -0,0 +1,174 @@ +package provisioning + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "path/filepath" + "strings" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" +) + +type importConnector struct { + repoGetter RepoGetter + client *resourceClient + logger *slog.Logger +} + +func (*importConnector) New() runtime.Object { + // This is added as the "ResponseType" regardless what ProducesObject() returns + return &provisioning.ResourceWrapper{} +} + +func (*importConnector) Destroy() {} + +func (*importConnector) NamespaceScoped() bool { + return true +} + +func (*importConnector) GetSingularName() string { + return "Import" +} + +func (*importConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (c *importConnector) ProducesObject(verb string) any { + return c.New() +} + +func (*importConnector) ConnectMethods() []string { + return []string{http.MethodPost} +} + +func (*importConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (c *importConnector) Connect( + ctx context.Context, + name string, + opts runtime.Object, + responder rest.Responder, +) (http.Handler, error) { + logger := c.logger.With("repository_name", name) + repo, err := c.repoGetter.GetRepository(ctx, name) + if err != nil { + return nil, err + } + ns := repo.Config().GetNamespace() + + // TODO: We need some way to filter what we import. + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + client, err := c.client.Client(ns) + if err != nil { + responder.Error(apierrors.NewInternalError(fmt.Errorf("failed to create a dynamic client: %w", err))) + return + } + kinds := newKindsLookup(client) + fileParser := newFileParser(ns, repo, client, kinds) + + ref := r.URL.Query().Get("ref") + if ref == "" { + responder.Error(apierrors.NewBadRequest("no ref value was given")) + return + } + + tree, err := repo.ReadTree(r.Context(), logger, ref) + if err != nil { + responder.Error(err) // TODO: do we want to do anything more for this? + return + } + for _, entry := range tree { + logger := logger.With("file", entry.Path) + if !entry.Blob { + logger.DebugContext(ctx, "ignoring non-blob entry") + continue + } + + name := filepath.Base(entry.Path) + if strings.ContainsRune(name, '.') { + name = name[:strings.LastIndex(name, ".")] + } + logger = logger.With("dashboard_name", name) + + info, err := repo.Read(r.Context(), logger, entry.Path, ref) + if err != nil { + logger.DebugContext(ctx, "error on reading the entry", "error", err) + responder.Error(apierrors.NewInternalError(fmt.Errorf("failed to read %s: %w", entry.Path, err))) + return + } + + // NOTE: We don't need validation because we're about to apply the data for real. + // If the data is invalid, we'll know then! + file, err := fileParser.parse(r.Context(), logger, info, false) + if err != nil { + logger.DebugContext(ctx, "error on parsing the entry's data", "error", err) + if errors.Is(err, ErrUnableToReadResourceBytes) { + // Non-resource data is not relevant to us. + logger.DebugContext(ctx, "ignoring file due to being a non-resource", "error", err) + continue + } + responder.Error(apierrors.NewInternalError(fmt.Errorf("failed to read %s: %w", entry.Path, err))) + return + } + + wrapper := file.AsResourceWrapper() + resource := wrapper.Resource.File + gv, err := schema.ParseGroupVersion(resource.GetNestedString("apiVersion")) + if err != nil { + logger.DebugContext(ctx, "invalid GroupVersion was given as the apiVersion of the parsed object?", "error", err) + responder.Error(apierrors.NewInternalError(fmt.Errorf("got an invalid groupversion from a parsed file %s: %w", entry.Path, err))) + return + } + gvk := gv.WithKind(resource.GetNestedString("kind")) + logger = logger.With("gvk", gvk) + gvr, ok := kinds.Resource(gvk) + if !ok { + logger.DebugContext(ctx, "got GVK of a resource we don't know how to control; ignoring") + continue + } + iface := client.Resource(gvr).Namespace(ns) + _, err = iface.Get(r.Context(), name, metav1.GetOptions{}) + // FIXME: Remove the 'false &&' when .Get returns 404 on 404 instead of 500. Until then, this is a really ugly workaround. + if false && err != nil && !apierrors.IsNotFound(err) { + logger.DebugContext(ctx, "failed to check if the object already exists", "error", err) + responder.Error(apierrors.NewInternalError(fmt.Errorf("failed to check if object already exists: %w", err))) + return + } + if err != nil { // IsNotFound + _, err = iface.Create(r.Context(), resource.ToKubernetesObject(), metav1.CreateOptions{}) + } else { // already exists + _, err = iface.Update(ctx, resource.ToKubernetesObject(), metav1.UpdateOptions{}) + } + if err != nil { + logger.DebugContext(ctx, "failed to upsert object", "error", err) + responder.Error(err) // FIXME + return + } + + // TODO: Folders + logger.DebugContext(ctx, "successfully upserted the object") + } + + responder.Object(http.StatusOK, &provisioning.ResourceWrapper{}) + }), nil +} + +var ( + _ rest.Connecter = (*importConnector)(nil) + _ rest.Storage = (*importConnector)(nil) + _ rest.Scoper = (*importConnector)(nil) + _ rest.SingularNameProvider = (*importConnector)(nil) + _ rest.StorageMetadata = (*importConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/parser.go b/pkg/registry/apis/provisioning/parser.go index 6886f456ab0..dcb5409ab93 100644 --- a/pkg/registry/apis/provisioning/parser.go +++ b/pkg/registry/apis/provisioning/parser.go @@ -33,6 +33,15 @@ type fileParser struct { kinds *kindsLookup } +func newFileParser(ns string, repo repository.Repository, client *dynamic.DynamicClient, kinds *kindsLookup) *fileParser { + return &fileParser{ + namespace: ns, + repo: repo, + client: client, + kinds: kinds, + } +} + type parsedFile struct { // Original file info info *repository.FileInfo diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 8b4b5dffb4f..54302c5d585 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -158,6 +158,11 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv client: b.client, logger: b.logger.With("connector", "export"), } + importConnector := &importConnector{ + repoGetter: b, + client: b.client, + logger: b.logger.With("connector", "export"), + } storage := map[string]rest.Storage{} storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage @@ -175,6 +180,7 @@ func (b *ProvisioningAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserv logger: b.logger.With("connector", "files"), } storage[provisioning.RepositoryResourceInfo.StoragePath("export")] = exportConnector + storage[provisioning.RepositoryResourceInfo.StoragePath("import")] = importConnector apiGroupInfo.VersionedResourcesStorageMap[provisioning.VERSION] = storage return nil } @@ -481,41 +487,48 @@ func (b *ProvisioningAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3. sub.Post.Description = "Currently only supports github webhooks" } + ref := &spec3.Parameter{ + ParameterProps: spec3.ParameterProps{ + Name: "ref", + In: "query", + Example: "", + Examples: map[string]*spec3.Example{ + "": { + ExampleProps: spec3.ExampleProps{ + Summary: "The default", + }, + }, + "branch": { + ExampleProps: spec3.ExampleProps{ + Value: "my-branch", + Summary: "Select branch", + }, + }, + "commit": { + ExampleProps: spec3.ExampleProps{ + Value: "7f7cc2153", + Summary: "Commit hash (or prefix)", + }, + }, + }, + Description: "branch or commit hash", + Schema: spec.StringProperty(), + Required: false, + }} + + sub = oas.Paths.Paths[repoprefix+"/import"] + if sub != nil && sub.Post != nil { + ref := *ref + ref.ParameterProps.Required = true + sub.Post.Parameters = []*spec3.Parameter{&ref} + } + // hide the version with no path delete(oas.Paths.Paths, repoprefix+"/files") // update the version with a path sub = oas.Paths.Paths[repoprefix+"/files/{path}"] if sub != nil { - ref := &spec3.Parameter{ - ParameterProps: spec3.ParameterProps{ - Name: "ref", - In: "query", - Example: "", - Examples: map[string]*spec3.Example{ - "": { - ExampleProps: spec3.ExampleProps{ - Summary: "The default", - }, - }, - "branch": { - ExampleProps: spec3.ExampleProps{ - Value: "my-branch", - Summary: "Select branch", - }, - }, - "commit": { - ExampleProps: spec3.ExampleProps{ - Value: "7f7cc2153", - Summary: "Commit hash (or prefix)", - }, - }, - }, - Description: "optional branch or commit hash", - Schema: spec.StringProperty(), - Required: false, - }} - sub.Get.Description = "Read value from upstream repository" sub.Get.Parameters = []*spec3.Parameter{ref} diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go index 1048fc2e6f5..8c5d3ee3cc3 100644 --- a/pkg/registry/apis/provisioning/repository/github.go +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -98,7 +98,7 @@ func (r *githubRepository) Read(ctx context.Context, logger *slog.Logger, filePa owner := r.config.Spec.GitHub.Owner repo := r.config.Spec.GitHub.Repository - content, _, err := r.gh.GetContents(ctx, owner, repo, filePath, ref) + content, dirContent, err := r.gh.GetContents(ctx, owner, repo, filePath, ref) if err != nil { if errors.Is(err, pgh.ErrResourceNotFound) { return nil, &apierrors.StatusError{ @@ -111,6 +111,9 @@ func (r *githubRepository) Read(ctx context.Context, logger *slog.Logger, filePa return nil, fmt.Errorf("get contents: %w", err) } + if dirContent != nil { + return nil, fmt.Errorf("input path was a directory") + } data, err := content.GetFileContent() if err != nil { @@ -124,6 +127,42 @@ func (r *githubRepository) Read(ctx context.Context, logger *slog.Logger, filePa }, nil } +func (r *githubRepository) ReadTree(ctx context.Context, logger *slog.Logger, ref string) ([]FileTreeEntry, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + owner := r.config.Spec.GitHub.Owner + repo := r.config.Spec.GitHub.Repository + logger = logger.With("owner", owner, "repo", repo, "ref", ref) + + tree, truncated, err := r.gh.GetTree(ctx, owner, repo, ref, true) + if err != nil { + if errors.Is(err, pgh.ErrResourceNotFound) { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: fmt.Sprintf("tree not found; ref=%s", ref), + Code: http.StatusNotFound, + }, + } + } + } + if truncated { + logger.WarnContext(ctx, "tree from github was truncated") + } + + entries := make([]FileTreeEntry, 0, len(tree)) + for _, entry := range tree { + converted := FileTreeEntry{ + Path: entry.GetPath(), + Size: entry.GetSize(), + Hash: entry.GetSHA(), + Blob: !entry.IsDirectory(), + } + entries = append(entries, converted) + } + return entries, nil +} + func (r *githubRepository) Create(ctx context.Context, logger *slog.Logger, path, ref string, data []byte, comment string) error { if ref == "" { ref = r.config.Spec.GitHub.Branch diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go index 611b07ca77c..e9cdcbf533b 100644 --- a/pkg/registry/apis/provisioning/repository/github/client.go +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -32,6 +32,13 @@ type Client interface { // If ".." appears in the "path", this method will return an error. GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) + // GetTree returns the Git tree in the repository. + // When recursive is given, subtrees are mapped into the returned array. + // + // The truncated bool will be set to true if the tree is larger than 7 MB or 100 000 entries. + // When truncated is true, you may wish to read each subtree manually instead. + GetTree(ctx context.Context, owner, repository, ref string, recursive bool) (entries []RepositoryContent, truncated bool, err error) + // CreateFile creates a new file in the repository under the given path. // The file is created on the branch given. // The message given is the commit message. If none is given, an appropriate default is used. @@ -85,6 +92,8 @@ type RepositoryContent interface { // Get the SHA hash. This is usually a SHA-256, but may also be SHA-512. // Directories have SHA hashes, too (TODO: how is this calculated?). GetSHA() string + // The size of the file. Not necessarily non-zero, even if the file is supposed to be non-zero. + GetSize() int64 } type CreateFileOptions struct { diff --git a/pkg/registry/apis/provisioning/repository/github/client_mock.go b/pkg/registry/apis/provisioning/repository/github/client_mock.go index e1faa8f7655..250e1e9036f 100644 --- a/pkg/registry/apis/provisioning/repository/github/client_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/client_mock.go @@ -180,6 +180,43 @@ func (_m *MockClient) GetContents(ctx context.Context, owner string, repository return r0, r1, r2 } +// GetTree provides a mock function with given fields: ctx, owner, repository, ref, recursive +func (_m *MockClient) GetTree(ctx context.Context, owner string, repository string, ref string, recursive bool) ([]RepositoryContent, bool, error) { + ret := _m.Called(ctx, owner, repository, ref, recursive) + + if len(ret) == 0 { + panic("no return value specified for GetTree") + } + + var r0 []RepositoryContent + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) ([]RepositoryContent, bool, error)); ok { + return rf(ctx, owner, repository, ref, recursive) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) []RepositoryContent); ok { + r0 = rf(ctx, owner, repository, ref, recursive) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]RepositoryContent) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool) bool); ok { + r1 = rf(ctx, owner, repository, ref, recursive) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, string, bool) error); ok { + r2 = rf(ctx, owner, repository, ref, recursive) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // ListWebhooks provides a mock function with given fields: ctx, owner, repository func (_m *MockClient) ListWebhooks(ctx context.Context, owner string, repository string) ([]WebhookConfig, error) { ret := _m.Called(ctx, owner, repository) diff --git a/pkg/registry/apis/provisioning/repository/github/real.go b/pkg/registry/apis/provisioning/repository/github/real.go index d9de5587df8..5ae940a7661 100644 --- a/pkg/registry/apis/provisioning/repository/github/real.go +++ b/pkg/registry/apis/provisioning/repository/github/real.go @@ -51,6 +51,41 @@ func (r *realImpl) GetContents(ctx context.Context, owner, repository, path, ref } } +func (r *realImpl) GetTree(ctx context.Context, owner, repository, ref string, recursive bool) ([]RepositoryContent, bool, error) { + tree, _, err := r.gh.Git.GetTree(ctx, owner, repository, ref, recursive) + if err != nil { + var ghErr *github.ErrorResponse + if !errors.As(err, &ghErr) { + return nil, false, err + } + if ghErr.Response.StatusCode == http.StatusServiceUnavailable { + return nil, false, ErrServiceUnavailable + } + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil, false, ErrResourceNotFound + } + return nil, false, err + } + + var entries []RepositoryContent + for _, te := range tree.Entries { + rrc := &realRepositoryContent{ + real: &github.RepositoryContent{ + Path: te.Path, + Size: te.Size, + SHA: te.SHA, + }, + } + if te.GetType() == "tree" { + rrc.real.Type = github.String("dir") + } else { + rrc.real.Type = te.Type + } + entries = append(entries, rrc) + } + return entries, tree.GetTruncated(), nil +} + func (r *realImpl) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error { if strings.Contains(path, "..") { return ErrPathTraversalDisallowed @@ -300,3 +335,15 @@ func (c realRepositoryContent) GetPath() string { func (c realRepositoryContent) GetSHA() string { return c.real.GetSHA() } + +func (c realRepositoryContent) GetSize() int64 { + if c.real.Size != nil { + return int64(*c.real.Size) + } + if c.real.Content != nil { + if c, err := c.real.GetContent(); err == nil { + return int64(len(c)) + } + } + return 0 +} diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local.go index 5ef1de58869..49992345c60 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local.go @@ -2,8 +2,11 @@ package repository import ( "context" + "crypto/sha1" + "encoding/hex" "errors" "fmt" + "io" "log/slog" "net/http" "os" @@ -134,6 +137,8 @@ func (r *localRepository) Read(ctx context.Context, logger *slog.Logger, path st info, err := os.Stat(fullpath) if errors.Is(err, os.ErrNotExist) { return nil, ErrFileNotFound + } else if err != nil { + return nil, err } //nolint:gosec @@ -142,15 +147,95 @@ func (r *localRepository) Read(ctx context.Context, logger *slog.Logger, path st return nil, err } + hash, _, err := r.calculateFileHash(ctx, fullpath) + if err != nil { + return nil, err + } + return &FileInfo{ Path: path, Data: data, + Hash: hash, Modified: &metav1.Time{ Time: info.ModTime(), }, }, nil } +// ReadResource implements provisioning.Repository. +func (r *localRepository) ReadTree(ctx context.Context, logger *slog.Logger, ref string) ([]FileTreeEntry, error) { + if ref != "" { + return nil, apierrors.NewBadRequest("local repository does not support ref") + } + if r.path == "" { + return nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Message: "the service is missing a root path", + Code: http.StatusFailedDependency, + }, + } + } + + return r.recursivelyReadTree(ctx, r.path) +} + +func (r *localRepository) recursivelyReadTree(ctx context.Context, path string) ([]FileTreeEntry, error) { + osEntries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + entries := make([]FileTreeEntry, 0, len(osEntries)) + for _, oe := range osEntries { + if err := ctx.Err(); err != nil { + return nil, err + } + + oePath := filepath.Join(path, oe.Name()) + if oe.IsDir() { + entries = append(entries, FileTreeEntry{ + Path: oePath, + Hash: "", + Size: 0, + Blob: false, + }) + subEntries, err := r.recursivelyReadTree(ctx, oePath) + if err != nil { + return nil, err + } + entries = append(entries, subEntries...) + } else { + hash, size, err := r.calculateFileHash(ctx, oePath) + if err != nil { + return nil, fmt.Errorf("failed to read and calculate hash of path %s: %w", oePath, err) + } + entries = append(entries, FileTreeEntry{ + Path: oePath, + Blob: true, + Hash: hash, + Size: size, + }) + } + } + return entries, nil +} + +func (r *localRepository) calculateFileHash(ctx context.Context, path string) (string, int64, error) { + file, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return "", 0, err + } + + // TODO: Define what hashing algorithm we want to use for the entire repository. Maybe a config option? + hasher := sha1.New() + // TODO: context-aware io.Copy? Is that even possible with a reasonable impl? + size, err := io.Copy(hasher, file) + if err != nil { + return "", 0, err + } + // NOTE: EncodeToString (& hex.Encode for that matter) return lower-case hex. + return hex.EncodeToString(hasher.Sum(nil)), size, nil +} + func (r *localRepository) Create(ctx context.Context, logger *slog.Logger, path string, ref string, data []byte, comment string) error { if ref != "" { return apierrors.NewBadRequest("local repository does not support ref") diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index 410f697f6ae..d053e2e79d6 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -41,6 +41,21 @@ type FileInfo struct { Modified *metav1.Time } +// An entry in the file tree, as returned by 'ReadFileTree'. Like FileInfo, but contains less information. +type FileTreeEntry struct { + // The path to the file from the base path given (if any). + // No leading or trailing slashes will be contained within. + Path string + // The hash for the file. Lower-case hex. + // Empty string if Blob is false. + Hash string + // The size of the file. + // 0 if Blob is false. + Size int64 + // Whether this entry is a blob or a subtree. + Blob bool +} + type Repository interface { // The saved Kubernetes object. Config() *provisioning.Repository @@ -57,6 +72,13 @@ type Repository interface { // This data will be parsed and validated before it is shown to end users Read(ctx context.Context, logger *slog.Logger, path, ref string) (*FileInfo, error) + // Read all file names from the tree. + // This data will be parsed and validated before it is shown. + // + // TODO: Make some API contract that lets us ignore files that aren't relevant to us (e.g. CI/CD, CODEOWNERS, other configs or source code). + // TODO: Test scale: do we want to stream entries instead somehow? + ReadTree(ctx context.Context, logger *slog.Logger, ref string) ([]FileTreeEntry, error) + // Write a file to the repository. // The data has already been validated and is ready for save Create(ctx context.Context, logger *slog.Logger, path, ref string, data []byte, message string) error diff --git a/pkg/registry/apis/provisioning/repository/s3.go b/pkg/registry/apis/provisioning/repository/s3.go index bf43e2a94c4..c3e5f5368ed 100644 --- a/pkg/registry/apis/provisioning/repository/s3.go +++ b/pkg/registry/apis/provisioning/repository/s3.go @@ -63,6 +63,15 @@ func (r *s3Repository) Read(ctx context.Context, logger *slog.Logger, path strin } } +func (r *s3Repository) ReadTree(ctx context.Context, logger *slog.Logger, ref string) ([]FileTreeEntry, error) { + return nil, &errors.StatusError{ + ErrStatus: metav1.Status{ + Message: "read file tree resource is not yet implemented", + Code: http.StatusNotImplemented, + }, + } +} + func (r *s3Repository) Create(ctx context.Context, logger *slog.Logger, path string, ref string, data []byte, comment string) error { return &errors.StatusError{ ErrStatus: metav1.Status{ diff --git a/pkg/registry/apis/provisioning/repository/unknown.go b/pkg/registry/apis/provisioning/repository/unknown.go index bbe7a36628b..0ece8a61d6b 100644 --- a/pkg/registry/apis/provisioning/repository/unknown.go +++ b/pkg/registry/apis/provisioning/repository/unknown.go @@ -58,6 +58,15 @@ func (r *unknownRepository) Read(ctx context.Context, logger *slog.Logger, path, } } +func (r *unknownRepository) ReadTree(ctx context.Context, logger *slog.Logger, ref string) ([]FileTreeEntry, error) { + return nil, &errors.StatusError{ + ErrStatus: metav1.Status{ + Message: "read file tree resource is not yet implemented", + Code: http.StatusNotImplemented, + }, + } +} + func (r *unknownRepository) Create(ctx context.Context, logger *slog.Logger, path, ref string, data []byte, comment string) error { return &errors.StatusError{ ErrStatus: metav1.Status{