Addresses part of https://github.com/coder/registry/issues/194 ## Description This PR beefs up the validation for the validation logic that we already had in place. This PR does not include adding validation for templates (which will be addressed in a second PR). ### Changes made - Added logic to reject unknown frontmatter fields for modules and contributor profile README files - Added logic to handle frontmatter fields that were previously missed in validation steps (GitHub username for contributors and Operating Systems for modules) - Updated a few comments (added some new comments, formatted existing comments to meet 100-column width) ### Type of Change - [ ] New module - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [x] Other ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally
141 lines
4.3 KiB
Go
141 lines
4.3 KiB
Go
package main
|
||
|
||
import (
|
||
"errors"
|
||
"os"
|
||
"path"
|
||
"slices"
|
||
"strings"
|
||
|
||
"golang.org/x/xerrors"
|
||
)
|
||
|
||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".images")
|
||
|
||
// validateCoderResourceSubdirectory validates that the structure of a module or template within a namespace follows all
|
||
// expected file conventions
|
||
func validateCoderResourceSubdirectory(dirPath string) []error {
|
||
resourceDir, err := os.Stat(dirPath)
|
||
if err != nil {
|
||
// It's valid for a specific resource directory not to exist. It's just that if it does exist, it must follow
|
||
// specific rules.
|
||
if !errors.Is(err, os.ErrNotExist) {
|
||
return []error{addFilePathToError(dirPath, err)}
|
||
}
|
||
}
|
||
|
||
if !resourceDir.IsDir() {
|
||
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
|
||
}
|
||
|
||
files, err := os.ReadDir(dirPath)
|
||
if err != nil {
|
||
return []error{addFilePathToError(dirPath, err)}
|
||
}
|
||
|
||
var errs []error
|
||
for _, f := range files {
|
||
// The .coder subdirectories are sometimes generated as part of our Bun tests. These subdirectories will never
|
||
// be committed to the repo, but in the off chance that they don't get cleaned up properly, we want to skip over
|
||
// them.
|
||
if !f.IsDir() || f.Name() == ".coder" {
|
||
continue
|
||
}
|
||
|
||
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
|
||
if _, err := os.Stat(resourceReadmePath); err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||
} else {
|
||
errs = append(errs, addFilePathToError(resourceReadmePath, err))
|
||
}
|
||
}
|
||
|
||
mainTerraformPath := path.Join(dirPath, f.Name(), "main.tf")
|
||
if _, err := os.Stat(mainTerraformPath); err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||
} else {
|
||
errs = append(errs, addFilePathToError(mainTerraformPath, err))
|
||
}
|
||
}
|
||
}
|
||
return errs
|
||
}
|
||
|
||
// validateRegistryDirectory validates that the contents of `/registry` follow all expected file conventions. This
|
||
// includes the top-level structure of the individual namespace directories.
|
||
func validateRegistryDirectory() []error {
|
||
namespaceDirs, err := os.ReadDir(rootRegistryPath)
|
||
if err != nil {
|
||
return []error{err}
|
||
}
|
||
|
||
var allErrs []error
|
||
for _, nDir := range namespaceDirs {
|
||
namespacePath := path.Join(rootRegistryPath, nDir.Name())
|
||
if !nDir.IsDir() {
|
||
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", namespacePath))
|
||
continue
|
||
}
|
||
|
||
contributorReadmePath := path.Join(namespacePath, "README.md")
|
||
if _, err := os.Stat(contributorReadmePath); err != nil {
|
||
allErrs = append(allErrs, err)
|
||
}
|
||
|
||
files, err := os.ReadDir(namespacePath)
|
||
if err != nil {
|
||
allErrs = append(allErrs, err)
|
||
continue
|
||
}
|
||
|
||
for _, f := range files {
|
||
// TODO: Decide if there's anything more formal that we want to ensure about non-directories at the top
|
||
// level of each user namespace.
|
||
if !f.IsDir() {
|
||
continue
|
||
}
|
||
|
||
segment := f.Name()
|
||
filePath := path.Join(namespacePath, segment)
|
||
|
||
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
|
||
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
|
||
continue
|
||
}
|
||
if !slices.Contains(supportedResourceTypes, segment) {
|
||
continue
|
||
}
|
||
|
||
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
|
||
allErrs = append(allErrs, errs...)
|
||
}
|
||
}
|
||
}
|
||
|
||
return allErrs
|
||
}
|
||
|
||
// validateRepoStructure validates that the structure of the repo is "correct enough" to do all necessary validation
|
||
// checks. It is NOT an exhaustive validation of the entire repo structure – it only checks the parts of the repo that
|
||
// are relevant for the main validation steps
|
||
func validateRepoStructure() error {
|
||
var errs []error
|
||
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
|
||
errs = append(errs, vrdErrs...)
|
||
}
|
||
|
||
if _, err := os.Stat("./.icons"); err != nil {
|
||
errs = append(errs, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||
}
|
||
|
||
if len(errs) != 0 {
|
||
return validationPhaseError{
|
||
phase: validationPhaseStructure,
|
||
errors: errs,
|
||
}
|
||
}
|
||
return nil
|
||
}
|