-
Notifications
You must be signed in to change notification settings - Fork 56
refactor: use coder/slog + minor go style changes #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
2c20ae5
b1d22f2
6b4093c
097b8a2
ed629c1
85743cd
6e8fc77
4fc0a54
8ab9fe9
11e4da3
333d962
94f0cb0
9fd7eb4
673df61
518d860
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,9 @@ package main | |
|
||
import ( | ||
"bufio" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net/url" | ||
"os" | ||
"path" | ||
|
@@ -15,7 +15,14 @@ import ( | |
"gopkg.in/yaml.v3" | ||
) | ||
|
||
var supportedResourceTypes = []string{"modules", "templates"} | ||
var ( | ||
supportedResourceTypes = []string{"modules", "templates"} | ||
|
||
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but | ||
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's | ||
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement. | ||
terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=") | ||
) | ||
|
||
type coderResourceFrontmatter struct { | ||
Description string `yaml:"description"` | ||
|
@@ -25,9 +32,8 @@ type coderResourceFrontmatter struct { | |
Tags []string `yaml:"tags"` | ||
} | ||
|
||
// coderResourceReadme represents a README describing a Terraform resource used | ||
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both | ||
// Coder Modules and Coder Templates | ||
// coderResourceReadme represents a README describing a Terraform resource used to help create Coder workspaces. | ||
// As of 2025-04-15, this encapsulates both Coder Modules and Coder Templates. | ||
type coderResourceReadme struct { | ||
resourceType string | ||
filePath string | ||
|
@@ -50,35 +56,33 @@ func validateCoderResourceDescription(description string) error { | |
} | ||
|
||
func validateCoderResourceIconURL(iconURL string) []error { | ||
problems := []error{} | ||
|
||
if iconURL == "" { | ||
problems = append(problems, errors.New("icon URL cannot be empty")) | ||
return problems | ||
return []error{errors.New("icon URL cannot be empty")} | ||
} | ||
|
||
errs := []error{} | ||
|
||
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") | ||
if isAbsoluteURL { | ||
if _, err := url.ParseRequestURI(iconURL); err != nil { | ||
problems = append(problems, errors.New("absolute icon URL is not correctly formatted")) | ||
errs = append(errs, errors.New("absolute icon URL is not correctly formatted")) | ||
} | ||
if strings.Contains(iconURL, "?") { | ||
problems = append(problems, errors.New("icon URLs cannot contain query parameters")) | ||
errs = append(errs, errors.New("icon URLs cannot contain query parameters")) | ||
} | ||
return problems | ||
return errs | ||
} | ||
|
||
// Would normally be skittish about having relative paths like this, but it | ||
// should be safe because we have guarantees about the structure of the | ||
// repo, and where this logic will run | ||
// Would normally be skittish about having relative paths like this, but it should be safe because we have guarantees | ||
// about the structure of the repo, and where this logic will run. | ||
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") || | ||
strings.HasPrefix(iconURL, "/") || | ||
strings.HasPrefix(iconURL, "../../../../.icons") | ||
if !isPermittedRelativeURL { | ||
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL)) | ||
errs = append(errs, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL)) | ||
} | ||
|
||
return problems | ||
return errs | ||
} | ||
|
||
func validateCoderResourceTags(tags []string) error { | ||
|
@@ -89,9 +93,8 @@ func validateCoderResourceTags(tags []string) error { | |
return nil | ||
} | ||
|
||
// All of these tags are used for the module/template filter controls in the | ||
// Registry site. Need to make sure they can all be placed in the browser | ||
// URL without issue | ||
// All of these tags are used for the module/template filter controls in the Registry site. Need to make sure they | ||
// can all be placed in the browser URL without issue. | ||
invalidTags := []string{} | ||
for _, t := range tags { | ||
if t != url.QueryEscape(t) { | ||
|
@@ -105,16 +108,11 @@ func validateCoderResourceTags(tags []string) error { | |
return nil | ||
} | ||
|
||
// Todo: This is a holdover from the validation logic used by the Coder Modules | ||
// repo. It gives us some assurance, but realistically, we probably want to | ||
// parse any Terraform code snippets, and make some deeper guarantees about how | ||
// it's structured. Just validating whether it *can* be parsed as Terraform | ||
// would be a big improvement. | ||
var terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=") | ||
|
||
func validateCoderResourceReadmeBody(body string) []error { | ||
trimmed := strings.TrimSpace(body) | ||
var errs []error | ||
|
||
trimmed := strings.TrimSpace(body) | ||
// TODO: this may cause unexpected behaviour since the errors slice may have a 0 length. Add a test. | ||
errs = append(errs, validateReadmeBody(trimmed)...) | ||
|
||
foundParagraph := false | ||
|
@@ -124,15 +122,15 @@ func validateCoderResourceReadmeBody(body string) []error { | |
lineNum := 0 | ||
isInsideCodeBlock := false | ||
isInsideTerraform := false | ||
nextLine := "" | ||
|
||
lineScanner := bufio.NewScanner(strings.NewReader(trimmed)) | ||
for lineScanner.Scan() { | ||
lineNum++ | ||
nextLine := lineScanner.Text() | ||
nextLine = lineScanner.Text() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the point of this change, since
for i, value := range exampleSlice {
// Stuff
} Is there an optimization that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this is just an optimization to reduce memory allocations. Very minor in this case since I doubt this loop has a lot of iterations, but without this a new string for The Go compiler already does an optimization itself for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that actually true nowadays? I know it used to be, but they changed the Is Go doing escape analysis for the variable to see if it needs to be kept around for closures, and only reusing the declaration if it knows that the variable doesn't need to be long-lived? JS enforces scoped behavior for every loop that uses let and const, but I imagine it can't do those optimizations natively because it's a JIT language There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I believe so, yes. It essentially looks for internal function calls or go routine calls that pass in the declared variable. If I understand correctly the optimization will still be applied if it can detect that the variable is only used within the scope of that loop. If we wanted to, we can write a simple benchmark to explore this. |
||
|
||
// Code assumes that invalid headers would've already been handled by | ||
// the base validation function, so we don't need to check deeper if the | ||
// first line isn't an h1 | ||
// Code assumes that invalid headers would've already been handled by the base validation function, so we don't | ||
// need to check deeper if the first line isn't an h1. | ||
if lineNum == 1 { | ||
if !strings.HasPrefix(nextLine, "# ") { | ||
break | ||
|
@@ -159,15 +157,13 @@ func validateCoderResourceReadmeBody(body string) []error { | |
continue | ||
} | ||
|
||
// Code assumes that we can treat this case as the end of the "h1 | ||
// section" and don't need to process any further lines | ||
// Code assumes that we can treat this case as the end of the "h1 section" and don't need to process any further lines. | ||
if lineNum > 1 && strings.HasPrefix(nextLine, "#") { | ||
break | ||
} | ||
|
||
// Code assumes that if we've reached this point, the only other options | ||
// are: (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset | ||
// references made via [] syntax | ||
// Code assumes that if we've reached this point, the only other options are: | ||
// (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset references made via [] syntax. | ||
trimmedLine := strings.TrimSpace(nextLine) | ||
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<") | ||
foundParagraph = foundParagraph || isParagraph | ||
|
@@ -257,9 +253,9 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin | |
|
||
yamlValidationErrors := []error{} | ||
for _, readme := range resources { | ||
errors := validateCoderResourceReadme(readme) | ||
if len(errors) > 0 { | ||
yamlValidationErrors = append(yamlValidationErrors, errors...) | ||
errs := validateCoderResourceReadme(readme) | ||
if len(errs) > 0 { | ||
yamlValidationErrors = append(yamlValidationErrors, errs...) | ||
} | ||
} | ||
if len(yamlValidationErrors) != 0 { | ||
|
@@ -272,7 +268,7 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin | |
return resources, nil | ||
} | ||
|
||
// Todo: Need to beef up this function by grabbing each image/video URL from | ||
// TODO: Need to beef up this function by grabbing each image/video URL from | ||
// the body's AST | ||
func validateCoderResourceRelativeUrls(resources map[string]coderResourceReadme) error { | ||
return nil | ||
|
@@ -286,13 +282,14 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) { | |
|
||
var allReadmeFiles []readme | ||
var errs []error | ||
var resourceDirs []os.DirEntry | ||
for _, rf := range registryFiles { | ||
if !rf.IsDir() { | ||
continue | ||
} | ||
|
||
resourceRootPath := path.Join(rootRegistryPath, rf.Name(), resourceType) | ||
resourceDirs, err := os.ReadDir(resourceRootPath) | ||
resourceDirs, err = os.ReadDir(resourceRootPath) | ||
if err != nil { | ||
if !errors.Is(err, os.ErrNotExist) { | ||
errs = append(errs, err) | ||
|
@@ -338,17 +335,16 @@ func validateAllCoderResourceFilesOfType(resourceType string) error { | |
return err | ||
} | ||
|
||
log.Printf("Processing %d README files\n", len(allReadmeFiles)) | ||
logger.Info(context.Background(), "Processing README files", "num_files", len(allReadmeFiles)) | ||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType) | ||
logger.Info(context.Background(), "Processed README files as valid Coder resources", "num_files", len(resources), "type", resourceType) | ||
|
||
err = validateCoderResourceRelativeUrls(resources) | ||
if err != nil { | ||
if err = validateCoderResourceRelativeUrls(resources); err != nil { | ||
cstyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return err | ||
} | ||
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType) | ||
logger.Info(context.Background(), "All relative URLs for READMEs are valid", "type", resourceType) | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log" | ||
"net/url" | ||
"os" | ||
"path" | ||
|
@@ -35,7 +35,7 @@ type contributorProfileReadme struct { | |
|
||
func validateContributorDisplayName(displayName string) error { | ||
if displayName == "" { | ||
return fmt.Errorf("missing display_name") | ||
return errors.New("missing display_name") | ||
} | ||
|
||
return nil | ||
|
@@ -53,17 +53,16 @@ func validateContributorLinkedinURL(linkedinURL *string) error { | |
return nil | ||
} | ||
|
||
// validateContributorSupportEmail does best effort validation of a contributors email address. We can't 100% validate | ||
// that this is correct without actually sending an email, especially because some contributors are individual developers | ||
// and we don't want to do that on every single run of the CI pipeline. The best we can do is verify the general structure. | ||
func validateContributorSupportEmail(email *string) []error { | ||
if email == nil { | ||
return nil | ||
} | ||
|
||
errs := []error{} | ||
|
||
// Can't 100% validate that this is correct without actually sending | ||
// an email, and especially with some contributors being individual | ||
// developers, we don't want to do that on every single run of the CI | ||
// pipeline. Best we can do is verify the general structure | ||
username, server, ok := strings.Cut(*email, "@") | ||
if !ok { | ||
errs = append(errs, fmt.Errorf("email address %q is missing @ symbol", *email)) | ||
|
@@ -113,21 +112,18 @@ func validateContributorStatus(status string) error { | |
return nil | ||
} | ||
|
||
// Can't validate the image actually leads to a valid resource in a pure | ||
// function, but can at least catch obvious problems | ||
// Can't validate the image actually leads to a valid resource in a pure function, but can at least catch obvious problems. | ||
func validateContributorAvatarURL(avatarURL *string) []error { | ||
if avatarURL == nil { | ||
return nil | ||
} | ||
|
||
errs := []error{} | ||
if *avatarURL == "" { | ||
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string")) | ||
return errs | ||
return []error{errors.New("avatar URL must be omitted or non-empty string")} | ||
} | ||
|
||
// Have to use .Parse instead of .ParseRequestURI because this is the | ||
// one field that's allowed to be a relative URL | ||
errs := []error{} | ||
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL. | ||
if _, err := url.Parse(*avatarURL); err != nil { | ||
errs = append(errs, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL)) | ||
} | ||
|
@@ -220,8 +216,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil | |
|
||
yamlValidationErrors := []error{} | ||
for _, p := range profilesByNamespace { | ||
errors := validateContributorReadme(p) | ||
if len(errors) > 0 { | ||
if errors := validateContributorReadme(p); len(errors) > 0 { | ||
yamlValidationErrors = append(yamlValidationErrors, errors...) | ||
continue | ||
} | ||
|
@@ -245,11 +240,12 @@ func aggregateContributorReadmeFiles() ([]readme, error) { | |
allReadmeFiles := []readme{} | ||
errs := []error{} | ||
for _, e := range dirEntries { | ||
dirPath := path.Join(rootRegistryPath, e.Name()) | ||
if !e.IsDir() { | ||
continue | ||
} | ||
|
||
dirPath := path.Join(rootRegistryPath, e.Name()) | ||
cstyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
readmePath := path.Join(dirPath, "README.md") | ||
rmBytes, err := os.ReadFile(readmePath) | ||
if err != nil { | ||
|
@@ -273,20 +269,17 @@ func aggregateContributorReadmeFiles() ([]readme, error) { | |
} | ||
|
||
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error { | ||
// This function only validates relative avatar URLs for now, but it can be | ||
// beefed up to validate more in the future | ||
// This function only validates relative avatar URLs for now, but it can be beefed up to validate more in the future. | ||
errs := []error{} | ||
|
||
for _, con := range contributors { | ||
// If the avatar URL is missing, we'll just assume that the Registry | ||
// site build step will take care of filling in the data properly | ||
// If the avatar URL is missing, we'll just assume that the Registry site build step will take care of filling | ||
// in the data properly. | ||
if con.frontmatter.AvatarURL == nil { | ||
continue | ||
} | ||
|
||
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || | ||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/") | ||
if !isRelativeURL { | ||
if !strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || !strings.HasPrefix(*con.frontmatter.AvatarURL, "/") { | ||
continue | ||
} | ||
|
||
|
@@ -297,8 +290,7 @@ func validateContributorRelativeUrls(contributors map[string]contributorProfileR | |
|
||
absolutePath := strings.TrimSuffix(con.filePath, "README.md") + | ||
*con.frontmatter.AvatarURL | ||
_, err := os.ReadFile(absolutePath) | ||
if err != nil { | ||
if _, err := os.ReadFile(absolutePath); err != nil { | ||
errs = append(errs, fmt.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, *con.frontmatter.AvatarURL)) | ||
} | ||
} | ||
|
@@ -318,19 +310,18 @@ func validateAllContributorFiles() error { | |
return err | ||
} | ||
|
||
log.Printf("Processing %d README files\n", len(allReadmeFiles)) | ||
logger.Info(context.Background(), "Processing README files", "num_files", len(allReadmeFiles)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still new to structured logging. Is there any special behavior/benefit you get if you use the same key multiple times? I guess I'm just wondering how much of a concern it is to make sure you're using the same keys each time you describe the same "resource", particularly for a function call that takes a variadic slice of empty interfaces (so basically zero type-safety) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not the end of the world if you don't use the same key, but it does make searching for logs in some kind of log aggregation system much easier. For example, a system I used to work on referred to the same internal Again this is likely less important in the case of the registry but still a good practice. |
||
contributors, err := parseContributorFiles(allReadmeFiles) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("Processed %d README files as valid contributor profiles", len(contributors)) | ||
logger.Info(context.Background(), "Processed README files as valid contributor profiles", "num_contributors", len(contributors)) | ||
|
||
err = validateContributorRelativeUrls(contributors) | ||
if err != nil { | ||
if err = validateContributorRelativeUrls(contributors); err != nil { | ||
cstyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return err | ||
} | ||
log.Println("All relative URLs for READMEs are valid") | ||
logger.Info(context.Background(), "All relative URLs for READMEs are valid") | ||
|
||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath) | ||
logger.Info(context.Background(), "Processed all READMEs in directory", "dir", rootRegistryPath) | ||
return nil | ||
} |
Uh oh!
There was an error while loading. Please reload this page.