diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index 9b4f80704bb..94c67ef24ea 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -150,10 +150,16 @@ type ResourceWrapper struct { Path string `json:"path,omitempty"` // The commit hash (if exists) - Commit string `json:"commit,omitempty"` + Ref string `json:"ref,omitempty"` - // Resource from the upstream repository - Resource common.Unstructured `json:"resource"` + // The repo hash value + Hash string `json:"hash,omitempty"` + + // The modified time in the remote file system + Timestamp *metav1.Time `json:"timestamp,omitempty"` + + // The raw resource body + Value common.Unstructured `json:"value"` } // Dummy object to return for webhooks diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 8a7c2f584ec..066a91e700b 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -197,7 +197,11 @@ func (in *RepositoryStatus) DeepCopy() *RepositoryStatus { func (in *ResourceWrapper) DeepCopyInto(out *ResourceWrapper) { *out = *in out.TypeMeta = in.TypeMeta - in.Resource.DeepCopyInto(&out.Resource) + if in.Timestamp != nil { + in, out := &in.Timestamp, &out.Timestamp + *out = (*in).DeepCopy() + } + in.Value.DeepCopyInto(&out.Value) return } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 2f9f20f5230..b28721eb917 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -395,25 +395,38 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC Format: "", }, }, - "commit": { + "ref": { SchemaProps: spec.SchemaProps{ Description: "The commit hash (if exists)", Type: []string{"string"}, Format: "", }, }, - "resource": { + "hash": { SchemaProps: spec.SchemaProps{ - Description: "Resource from the upstream repository", + Description: "The repo hash value", + Type: []string{"string"}, + Format: "", + }, + }, + "timestamp": { + SchemaProps: spec.SchemaProps{ + Description: "The modified time in the remote file system", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "The raw resource body", Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), }, }, }, - Required: []string{"resource"}, + Required: []string{"value"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/registry/apis/provisioning/errors.go b/pkg/registry/apis/provisioning/errors.go new file mode 100644 index 00000000000..f34cbe5c21e --- /dev/null +++ b/pkg/registry/apis/provisioning/errors.go @@ -0,0 +1,8 @@ +package provisioning + +import "errors" + +var ( + ErrFileNotFound = errors.New("file not found") + ErrNamespaceMismatch = errors.New("the file namespace does not match target namespace") +) diff --git a/pkg/registry/apis/provisioning/file.go b/pkg/registry/apis/provisioning/file.go index 86e5b7c976a..a3588137e45 100644 --- a/pkg/registry/apis/provisioning/file.go +++ b/pkg/registry/apis/provisioning/file.go @@ -130,20 +130,21 @@ func (s *readConnector) getValidatedBody(_ context.Context, data []byte) (*unstr } func (s *readConnector) doRead(ctx context.Context, repo Repository, path string, commit string) (*provisioning.ResourceWrapper, error) { - data, err := repo.Read(ctx, path, commit) + info, err := repo.Read(ctx, path, commit) if err != nil { return nil, err } - obj, _, err := s.getValidatedBody(ctx, data) + obj, _, err := s.getValidatedBody(ctx, info.Data) if err != nil { return nil, err } - return &provisioning.ResourceWrapper{ - Path: path, - Commit: commit, - Resource: v0alpha1.Unstructured{ + Path: info.Path, + Ref: info.Ref, + Hash: info.Hash, + Timestamp: info.Modified, + Value: v0alpha1.Unstructured{ Object: obj.Object, }, }, nil diff --git a/pkg/registry/apis/provisioning/github.go b/pkg/registry/apis/provisioning/github.go index de42fcf39c1..753d089b447 100644 --- a/pkg/registry/apis/provisioning/github.go +++ b/pkg/registry/apis/provisioning/github.go @@ -91,9 +91,8 @@ func (r *githubRepository) Test(ctx context.Context) error { } // ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath string, commit string) ([]byte, error) { - ref := commit - if commit == "" { +func (r *githubRepository) Read(ctx context.Context, filePath string, ref string) (*FileInfo, error) { + if ref == "" { ref = r.config.Spec.GitHub.Branch } @@ -118,8 +117,12 @@ func (r *githubRepository) Read(ctx context.Context, filePath string, commit str if err != nil { return nil, fmt.Errorf("get content: %w", err) } - - return []byte(data), nil + return &FileInfo{ + Path: filePath, + Ref: ref, + Data: []byte(data), + Hash: content.GetSHA(), + }, nil } func (r *githubRepository) Create(ctx context.Context, path string, data []byte, comment string) error { @@ -255,30 +258,30 @@ func (r *githubRepository) onPushEvent(ctx context.Context, event *github.PushEv r.logger.Info("process commit", "commit", commit.GetID(), "message", commit.GetMessage()) for _, file := range commit.Added { - resource, err := r.Read(ctx, file, commit.GetID()) + info, err := r.Read(ctx, file, commit.GetID()) if err != nil { return fmt.Errorf("read added resource: %w", err) } - r.logger.Info("added file", "file", file, "resource", string(resource), "commit", commit.GetID()) + r.logger.Info("added file", "file", file, "resource", string(info.Data), "commit", commit.GetID()) } for _, file := range commit.Modified { - resource, err := r.Read(ctx, file, commit.GetID()) + info, err := r.Read(ctx, file, commit.GetID()) if err != nil { return fmt.Errorf("read modified resource: %w", err) } - r.logger.Info("modified file", "file", file, "resource", string(resource), "commit", commit.GetID()) + r.logger.Info("modified file", "file", file, "resource", string(info.Data), "commit", commit.GetID()) } for _, file := range commit.Removed { - resource, err := r.Read(ctx, file, beforeRef) + info, err := r.Read(ctx, file, beforeRef) if err != nil { return fmt.Errorf("read removed resource: %w", err) } - r.logger.Info("removed file", "file", file, "resource", string(resource), "commit", commit.GetID()) + r.logger.Info("removed file", "file", file, "resource", string(info.Data), "commit", commit.GetID()) } beforeRef = commit.GetID() diff --git a/pkg/registry/apis/provisioning/local.go b/pkg/registry/apis/provisioning/local.go index 42b65fa796e..3da531f3e8f 100644 --- a/pkg/registry/apis/provisioning/local.go +++ b/pkg/registry/apis/provisioning/local.go @@ -10,7 +10,7 @@ import ( "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/registry/rest" @@ -108,7 +108,7 @@ func (r *localRepository) Validate() (fields field.ErrorList) { // Test implements provisioning.Repository. func (r *localRepository) Test(ctx context.Context) error { return &apierrors.StatusError{ - ErrStatus: v1.Status{ + ErrStatus: metav1.Status{ Message: "test is not yet implemented", Code: http.StatusNotImplemented, }, @@ -116,27 +116,44 @@ func (r *localRepository) Test(ctx context.Context) error { } // ReadResource implements provisioning.Repository. -func (r *localRepository) Read(ctx context.Context, path string, commit string) ([]byte, error) { - if commit != "" { - return nil, apierrors.NewBadRequest("local repository does not support commits") +func (r *localRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { + if ref != "" { + return nil, apierrors.NewBadRequest("local repository does not support ref") } if r.path == "" { return nil, &apierrors.StatusError{ - ErrStatus: v1.Status{ + ErrStatus: metav1.Status{ Message: "the service is missing a root path", Code: http.StatusFailedDependency, }, } } + fullpath := filepath.Join(r.path, path) + info, err := os.Stat(fullpath) + if errors.Is(err, os.ErrNotExist) { + return nil, ErrFileNotFound + } + //nolint:gosec - return os.ReadFile(filepath.Join(r.path, path)) + data, err := os.ReadFile(fullpath) + if err != nil { + return nil, err + } + + return &FileInfo{ + Path: path, + Data: data, + Modified: &metav1.Time{ + Time: info.ModTime(), + }, + }, nil } func (r *localRepository) Create(ctx context.Context, path string, data []byte, comment string) error { if r.path == "" { return &apierrors.StatusError{ - ErrStatus: v1.Status{ + ErrStatus: metav1.Status{ Message: "the service is missing a root path", Code: http.StatusFailedDependency, }, @@ -153,7 +170,7 @@ func (r *localRepository) Create(ctx context.Context, path string, data []byte, func (r *localRepository) Update(ctx context.Context, path string, data []byte, comment string) error { if r.path == "" { return &apierrors.StatusError{ - ErrStatus: v1.Status{ + ErrStatus: metav1.Status{ Message: "the service is missing a root path", Code: http.StatusFailedDependency, }, @@ -170,7 +187,7 @@ func (r *localRepository) Update(ctx context.Context, path string, data []byte, func (r *localRepository) Delete(ctx context.Context, path string, comment string) error { if r.path == "" { return &apierrors.StatusError{ - ErrStatus: v1.Status{ + ErrStatus: metav1.Status{ Message: "the service is missing a root path", Code: http.StatusFailedDependency, }, diff --git a/pkg/registry/apis/provisioning/s3.go b/pkg/registry/apis/provisioning/s3.go index 50a582937e4..3603df3aefa 100644 --- a/pkg/registry/apis/provisioning/s3.go +++ b/pkg/registry/apis/provisioning/s3.go @@ -51,7 +51,7 @@ func (r *s3Repository) Test(ctx context.Context) error { } // ReadResource implements provisioning.Repository. -func (r *s3Repository) Read(ctx context.Context, path string, commit string) ([]byte, error) { +func (r *s3Repository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { return nil, &errors.StatusError{ ErrStatus: metav1.Status{ Message: "read resource is not yet implemented", diff --git a/pkg/registry/apis/provisioning/types.go b/pkg/registry/apis/provisioning/types.go index 21c94cb5481..e213abe0b37 100644 --- a/pkg/registry/apis/provisioning/types.go +++ b/pkg/registry/apis/provisioning/types.go @@ -29,6 +29,19 @@ func (f UndoFunc) Chain(ctx context.Context, next UndoFunc) UndoFunc { } } +type FileInfo struct { + // Path to the file on disk + Path string + // The raw bytes + Data []byte + // The git branch or reference commit + Ref string + // The git hash for a given file + Hash string + // When was the file changed (if known) + Modified *metav1.Time +} + type Repository interface { // The saved object Config() *provisioning.Repository @@ -43,7 +56,7 @@ type Repository interface { // Read a file from the resource // This data will be parsed and validated before it is shown to end users - Read(ctx context.Context, path string, commit string) ([]byte, error) + Read(ctx context.Context, path string, ref string) (*FileInfo, error) // Write a file to the repository. // The data has already been validated and is ready for save @@ -98,7 +111,7 @@ func (r *unknownRepository) Test(ctx context.Context) error { } // ReadResource implements provisioning.Repository. -func (r *unknownRepository) Read(ctx context.Context, path string, commit string) ([]byte, error) { +func (r *unknownRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { return nil, &errors.StatusError{ ErrStatus: metav1.Status{ Message: "read resource is not yet implemented",