diff --git a/cmd/krel/cmd/testdata/validation-data/invalid-indent.yaml b/cmd/krel/cmd/testdata/validation-data/invalid-indent.yaml new file mode 100644 index 00000000000..5ea7828fd2a --- /dev/null +++ b/cmd/krel/cmd/testdata/validation-data/invalid-indent.yaml @@ -0,0 +1,10 @@ +pr: 126108 +releasenote: + text: |- + Reduced state change noise when volume expansion fails. Also mark certain failures as infeasible. + + ACTION REQUIRED: If you are using the `RecoverVolumeExpansionFailure` alpha feature gate + then after upgrading to this release, you need to update some objects. + For any existing PersistentVolumeClaimss with `status.allocatedResourceStatus` set to either + "ControllerResizeFailed" or "NodeResizeFailed", clear the `status.allocatedResourceStatus`. +pr_body: "" \ No newline at end of file diff --git a/cmd/krel/cmd/testdata/validation-data/invalid-multi-line.yaml b/cmd/krel/cmd/testdata/validation-data/invalid-multi-line.yaml new file mode 100644 index 00000000000..5c5e303322b --- /dev/null +++ b/cmd/krel/cmd/testdata/validation-data/invalid-multi-line.yaml @@ -0,0 +1,5 @@ +pr: 125163 +releasenote: + text: 'ACTION REQUIRED: The Dynamic Resource Allocation (DRA) driver's DaemonSet must be deployed + with a service account that enables writing ResourceSlice and reading ResourceClaim + objects.' \ No newline at end of file diff --git a/cmd/krel/cmd/testdata/validation-data/invalid-yaml-start.yaml b/cmd/krel/cmd/testdata/validation-data/invalid-yaml-start.yaml new file mode 100644 index 00000000000..767f9775357 --- /dev/null +++ b/cmd/krel/cmd/testdata/validation-data/invalid-yaml-start.yaml @@ -0,0 +1,3 @@ +pr: 125157 +releasenote: + text: `kubeadm`: The `NodeSwap` check that kubeadm performs during preflight, has a new warning to verify if swap has been configured correctly. \ No newline at end of file diff --git a/cmd/krel/cmd/testdata/validation-data/missing-punctuation.yaml b/cmd/krel/cmd/testdata/validation-data/missing-punctuation.yaml new file mode 100644 index 00000000000..0af291b3d79 --- /dev/null +++ b/cmd/krel/cmd/testdata/validation-data/missing-punctuation.yaml @@ -0,0 +1,4 @@ +pr: 125157 +releasenote: + text: "`kubeadm`: The `NodeSwap` check that kubeadm performs during preflight, has a new warning to verify if swap has been configured correctly" +pr_body: "" diff --git a/cmd/krel/cmd/testdata/validation-data/valid.yaml b/cmd/krel/cmd/testdata/validation-data/valid.yaml new file mode 100644 index 00000000000..f179bf2f800 --- /dev/null +++ b/cmd/krel/cmd/testdata/validation-data/valid.yaml @@ -0,0 +1,4 @@ +pr: 125157 +releasenote: + text: "`kubeadm`: The `NodeSwap` check that kubeadm performs during preflight, has a new warning to verify if swap has been configured correctly." +pr_body: "" diff --git a/cmd/krel/cmd/validate.go b/cmd/krel/cmd/validate.go new file mode 100644 index 00000000000..3ffdf57b674 --- /dev/null +++ b/cmd/krel/cmd/validate.go @@ -0,0 +1,119 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/spf13/cobra" + "k8s.io/release/pkg/notes" + "sigs.k8s.io/yaml" +) + +func init() { + // Add the validation subcommand to the root command + rootCmd.AddCommand(validateCmd) +} + +// validate represents the subcommand for `krel validate`. +var validateCmd = &cobra.Command{ + Use: "validate", + Short: "The subcommand for validating release notes for the Release Notes subteam of SIG Release", + Long: `krel validate + +The 'validate' subcommand of krel has been developed to: + +1. Check release notes maps for valid yaml. + +2. Check release notes maps for valid punctuation.`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Extract the release notes path from args + releaseNotesPath := args[0] + + // Run the PR creation function + return runValidateReleaseNotes(releaseNotesPath) + }, +} + +func runValidateReleaseNotes(releaseNotesPath string) (err error) { + // Check if the directory exists + if _, err := os.Stat(releaseNotesPath); os.IsNotExist(err) { + return fmt.Errorf("release notes path %s does not exist", releaseNotesPath) + } + + // Validate the YAML files in the directory + err = filepath.Walk(releaseNotesPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Only process YAML files + if filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" { + fmt.Printf("Validating YAML file: %s\n", path) + + // Validate YAML + if err := ValidateYamlMap(path); err != nil { + return fmt.Errorf("YAML validation failed for %s: %v", path, err) + } + + // (Optional) You can add custom punctuation validation here + // For example, you could check for missing periods at the end of lines + + fmt.Printf("YAML file %s is valid.\n", path) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to validate release notes: %v", err) + } + + fmt.Println("All release notes are valid.") + return nil +} + +// ValidateYamlMap reads a YAML map file, unmarshals it into a map, and then re-marshals it +// to validate the correctness of the content. +func ValidateYamlMap(filePath string) error { + // Read the YAML file + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + // Unmarshal the YAML data into a map for manipulation and validation + var testMap notes.ReleaseNotesMap + if err := yaml.Unmarshal(data, &testMap); err != nil { + return fmt.Errorf("YAML unmarshal failed for %s:%w", filePath, err) + } + + // Check the map for valid punctuation in the "text" field + if err := validateTextFieldPunctuation(testMap); err != nil { + return fmt.Errorf("punctuation check failed for file %s: %w", filePath, err) + } + + // Re-marshall the YAML to check if it can be successfully serialized again + _, err = yaml.Marshal(testMap) + if err != nil { + return fmt.Errorf("while re-marshaling map for file %s:%w", filePath, err) + } + + fmt.Printf("File %s is valid YAML.\n", filePath) + return nil +} + +// validateTextFieldPunctuation checks if the "text" field in a YAML map +// ends with valid punctuation (., !, ?). +func validateTextFieldPunctuation(data notes.ReleaseNotesMap) error { + validPunctuation := regexp.MustCompile(`[.!?]$`) + + text := *data.ReleaseNote.Text + if !validPunctuation.MatchString(strings.TrimSpace(text)) { + return fmt.Errorf("the 'text' field does not end with valid punctuation: '%s'", text) + } + + return nil +} diff --git a/cmd/krel/cmd/validation_test.go b/cmd/krel/cmd/validation_test.go new file mode 100644 index 00000000000..3f58f832846 --- /dev/null +++ b/cmd/krel/cmd/validation_test.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunValidateReleaseNotes(t *testing.T) { + testDataPath := "testdata/validation-data" + + // Valid YAML returns no error + err := runValidateReleaseNotes(filepath.Join(testDataPath, "valid.yaml")) + assert.NoError(t, err, "Expected no error for valid YAML file") + + // Try a non-existent path + err = runValidateReleaseNotes("nonexistent/path") + assert.Error(t, err, "Expected error for non-existent path") + assert.Contains(t, err.Error(), "does not exist", "Error should be about non-existent path") + + // Missing punctuation YAML returns error + err = runValidateReleaseNotes(filepath.Join(testDataPath, "missing-punctuation.yaml")) + assert.Error(t, err, "Expected error for missing punctuation YAML file") + assert.Contains(t, err.Error(), "field does not end with valid punctuation", "Error should be about missing punctuation") + + // Try invalid yaml starting with "`" + err = runValidateReleaseNotes(filepath.Join(testDataPath, "invalid-yaml-start.yaml")) + assert.Error(t, err, "Expected error for invalid yaml") + assert.Contains(t, err.Error(), "validation failed for testdata/validation-data/invalid-yaml-start", "Error should be about invalid yaml") + + // Try invalid multi line yaml + err = runValidateReleaseNotes(filepath.Join(testDataPath, "invalid-multi-line.yaml")) + assert.Error(t, err, "Expected error for invalid yaml") + assert.Contains(t, err.Error(), "YAML validation failed for testdata/validation-data/invalid-multi-line.yaml", "Error should be about invalid yaml") + + // Try invalid indent + err = runValidateReleaseNotes(filepath.Join(testDataPath, "invalid-indent.yaml")) + assert.Error(t, err, "Expected error for invalid yaml") + assert.Contains(t, err.Error(), "YAML validation failed for testdata/validation-data/invalid-indent.yaml", "Error should be about invalid yaml") +}