@@ -4,9 +4,15 @@ import (
44 "bufio"
55 "errors"
66 "fmt"
7+ "log"
78 "net/url"
9+ "os"
10+ "path"
811 "regexp"
12+ "slices"
913 "strings"
14+
15+ "gopkg.in/yaml.v3"
1016)
1117
1218var supportedResourceTypes = []string {"modules" , "templates" }
@@ -29,6 +35,13 @@ type coderResourceReadme struct {
2935 frontmatter coderResourceFrontmatter
3036}
3137
38+ type coderResourceReadmes map [string ]coderResourceReadme
39+
40+ func (crr coderResourceReadmes ) Get (filePath string ) (coderResourceReadme , bool ) {
41+ rm , ok := crr [filePath ]
42+ return rm , ok
43+ }
44+
3245func validateCoderResourceDisplayName (displayName * string ) error {
3346 if displayName != nil && * displayName == "" {
3447 return errors .New ("if defined, display_name must not be empty string" )
@@ -220,3 +233,139 @@ func validateCoderResourceReadme(rm coderResourceReadme) []error {
220233
221234 return errs
222235}
236+
237+ func parseCoderResourceReadme (resourceType string , rm readme ) (coderResourceReadme , error ) {
238+ fm , body , err := separateFrontmatter (rm .rawText )
239+ if err != nil {
240+ return coderResourceReadme {}, fmt .Errorf ("%q: failed to parse frontmatter: %v" , rm .filePath , err )
241+ }
242+
243+ yml := coderResourceFrontmatter {}
244+ if err := yaml .Unmarshal ([]byte (fm ), & yml ); err != nil {
245+ return coderResourceReadme {}, fmt .Errorf ("%q: failed to parse: %v" , rm .filePath , err )
246+ }
247+
248+ return coderResourceReadme {
249+ resourceType : resourceType ,
250+ filePath : rm .filePath ,
251+ body : body ,
252+ frontmatter : yml ,
253+ }, nil
254+ }
255+
256+ func parseCoderResourceReadmeFiles (resourceType string , rms []readme ) (coderResourceReadmes , error ) {
257+ resources := coderResourceReadmes (map [string ]coderResourceReadme {})
258+ var yamlParsingErrs []error
259+ for _ , rm := range rms {
260+ p , err := parseCoderResourceReadme (resourceType , rm )
261+ if err != nil {
262+ yamlParsingErrs = append (yamlParsingErrs , err )
263+ continue
264+ }
265+
266+ resources [p .filePath ] = p
267+ }
268+ if len (yamlParsingErrs ) != 0 {
269+ return nil , validationPhaseError {
270+ phase : validationPhaseReadmeParsing ,
271+ errors : yamlParsingErrs ,
272+ }
273+ }
274+
275+ yamlValidationErrors := []error {}
276+ for _ , readme := range resources {
277+ errors := validateCoderResourceReadme (readme )
278+ if len (errors ) > 0 {
279+ yamlValidationErrors = append (yamlValidationErrors , errors ... )
280+ }
281+ }
282+ if len (yamlValidationErrors ) != 0 {
283+ return nil , validationPhaseError {
284+ phase : validationPhaseReadmeParsing ,
285+ errors : yamlValidationErrors ,
286+ }
287+ }
288+
289+ return resources , nil
290+ }
291+
292+ // Todo: Need to beef up this function by grabbing each image/video URL from
293+ // the body's AST
294+ func validateCoderResourceRelativeUrls (resources coderResourceReadmes ) error {
295+ return nil
296+ }
297+
298+ func aggregateCoderResourceReadmeFiles (resourceType string ) ([]readme , error ) {
299+ registryFiles , err := os .ReadDir (rootRegistryPath )
300+ if err != nil {
301+ return nil , err
302+ }
303+
304+ var allReadmeFiles []readme
305+ var errs []error
306+ for _ , rf := range registryFiles {
307+ if ! rf .IsDir () {
308+ continue
309+ }
310+
311+ resourceRootPath := path .Join (rootRegistryPath , rf .Name (), resourceType )
312+ resourceDirs , err := os .ReadDir (resourceRootPath )
313+ if err != nil {
314+ if ! errors .Is (err , os .ErrNotExist ) {
315+ errs = append (errs , err )
316+ }
317+ continue
318+ }
319+
320+ for _ , rd := range resourceDirs {
321+ if ! rd .IsDir () || rd .Name () == ".coder" {
322+ continue
323+ }
324+
325+ resourceReadmePath := path .Join (resourceRootPath , rd .Name (), "README.md" )
326+ rm , err := os .ReadFile (resourceReadmePath )
327+ if err != nil {
328+ errs = append (errs , err )
329+ continue
330+ }
331+
332+ allReadmeFiles = append (allReadmeFiles , readme {
333+ filePath : resourceReadmePath ,
334+ rawText : string (rm ),
335+ })
336+ }
337+ }
338+
339+ if len (errs ) != 0 {
340+ return nil , validationPhaseError {
341+ phase : validationPhaseFileLoad ,
342+ errors : errs ,
343+ }
344+ }
345+ return allReadmeFiles , nil
346+ }
347+
348+ func validateAllCoderResourceFilesOfType (resourceType string ) error {
349+ if ! slices .Contains (supportedResourceTypes , resourceType ) {
350+ return fmt .Errorf ("resource type %q is not part of supported list [%s]" , resourceType , strings .Join (supportedResourceTypes , ", " ))
351+ }
352+
353+ allReadmeFiles , err := aggregateCoderResourceReadmeFiles (resourceType )
354+ if err != nil {
355+ return err
356+ }
357+
358+ log .Printf ("Processing %d README files\n " , len (allReadmeFiles ))
359+ resources , err := parseCoderResourceReadmeFiles (resourceType , allReadmeFiles )
360+ if err != nil {
361+ return err
362+ }
363+ log .Printf ("Processed %d README files as valid Coder resources with type %q" , len (resources ), resourceType )
364+
365+ err = validateCoderResourceRelativeUrls (resources )
366+ if err != nil {
367+ return err
368+ }
369+ log .Printf ("All relative URLs for %s READMEs are valid\n " , resourceType )
370+ return nil
371+ }
0 commit comments