Skip to content

Commit fdbfed0

Browse files
committed
unofficial tool which validates if freshmaker naming could comply with release-version name requirements
1 parent e7b9dee commit fdbfed0

File tree

2 files changed

+283
-1
lines changed

2 files changed

+283
-1
lines changed

cmd/opm/root/cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/operator-framework/operator-registry/cmd/opm/render"
1818
"github.com/operator-framework/operator-registry/cmd/opm/serve"
1919
"github.com/operator-framework/operator-registry/cmd/opm/validate"
20+
validatefreshmaker "github.com/operator-framework/operator-registry/cmd/opm/validate-freshmaker"
2021
"github.com/operator-framework/operator-registry/cmd/opm/version"
2122
)
2223

@@ -44,7 +45,7 @@ To view help related to alpha features, set HELP_ALPHA=true in the environment.`
4445
logrus.Panic(err.Error())
4546
}
4647

47-
cmd.AddCommand(registry.NewOpmRegistryCmd(showAlphaHelp), alpha.NewCmd(showAlphaHelp), initcmd.NewCmd(), migrate.NewCmd(), serve.NewCmd(), render.NewCmd(showAlphaHelp), validate.NewCmd(), generate.NewCmd())
48+
cmd.AddCommand(registry.NewOpmRegistryCmd(showAlphaHelp), alpha.NewCmd(showAlphaHelp), initcmd.NewCmd(), migrate.NewCmd(), serve.NewCmd(), render.NewCmd(showAlphaHelp), validate.NewCmd(), validatefreshmaker.NewCmd(), generate.NewCmd())
4849
index.AddCommand(cmd, showAlphaHelp)
4950
version.AddCommand(cmd)
5051

cmd/opm/validate-freshmaker/cmd.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package validate_freshmaker
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"os"
9+
"regexp"
10+
"strings"
11+
12+
"github.com/sirupsen/logrus"
13+
"github.com/spf13/cobra"
14+
15+
"github.com/operator-framework/operator-registry/alpha/action"
16+
"github.com/operator-framework/operator-registry/alpha/declcfg"
17+
"github.com/operator-framework/operator-registry/alpha/property"
18+
"github.com/operator-framework/operator-registry/cmd/opm/internal/util"
19+
)
20+
21+
const (
22+
substitutesForAnnotation = "olm.substitutesFor"
23+
maxNameLength = 63
24+
maxReleaseLength = 20
25+
)
26+
27+
type ValidationResult struct {
28+
Schema string `json:"schema"`
29+
Name string `json:"name"`
30+
Package string `json:"package"`
31+
Valid bool `json:"valid"`
32+
Errors []string `json:"errors,omitempty"`
33+
}
34+
35+
type ValidationOutput struct {
36+
Results []ValidationResult `json:"results"`
37+
}
38+
39+
func NewCmd() *cobra.Command {
40+
var (
41+
render action.Render
42+
output string
43+
)
44+
45+
cmd := &cobra.Command{
46+
Use: "validate-freshmaker [catalog-image | catalog-directory | bundle-image | bundle-directory]...",
47+
Short: "Validate freshmaker release versioning in bundles",
48+
Long: `Validate freshmaker release versioning in bundles from the provided
49+
catalog images, file-based catalog directories, bundle images, and bundle directories.
50+
51+
Freshmaker usage is identified by bundles having:
52+
1. An olm.substitutesFor annotation (value is immaterial)
53+
2. A property of type "olm.package" with value.version containing a plus sign (+)
54+
55+
The release versioning is the portion after the plus sign.
56+
Release versioning naming requirement: <package>-v<version-without-release>-<release-version>
57+
where:
58+
- release-version: dot-delimited sequences of alphanumerics and hyphens, max 20 characters
59+
- total constructed name: max 63 characters
60+
`,
61+
Args: cobra.MinimumNArgs(1),
62+
Run: func(cmd *cobra.Command, args []string) {
63+
render.Refs = args
64+
65+
// Discard verbose logging
66+
logrus.SetOutput(io.Discard)
67+
68+
reg, err := util.CreateCLIRegistry(cmd)
69+
if err != nil {
70+
log.Fatal(err)
71+
}
72+
defer func() {
73+
_ = reg.Destroy()
74+
}()
75+
76+
render.Registry = reg
77+
78+
cfg, err := render.Run(cmd.Context())
79+
if err != nil {
80+
log.Fatal(err)
81+
}
82+
83+
results := validateBundles(cfg)
84+
85+
var writeFunc func(ValidationOutput, io.Writer) error
86+
switch output {
87+
case "yaml":
88+
writeFunc = writeYAML
89+
case "json":
90+
writeFunc = writeJSON
91+
case "text":
92+
writeFunc = writeText
93+
default:
94+
log.Fatalf("invalid --output value %q, expected (json|yaml|text)", output)
95+
}
96+
97+
if err := writeFunc(ValidationOutput{Results: results}, os.Stdout); err != nil {
98+
log.Fatal(err)
99+
}
100+
},
101+
}
102+
103+
cmd.Flags().StringVarP(&output, "output", "o", "text", "Output format (json|yaml|text)")
104+
105+
return cmd
106+
}
107+
108+
func validateBundles(cfg *declcfg.DeclarativeConfig) []ValidationResult {
109+
var results []ValidationResult
110+
111+
for _, bundle := range cfg.Bundles {
112+
result := validateBundle(bundle)
113+
// Only include freshmaker bundles in the output
114+
if result.Name != "" {
115+
results = append(results, result)
116+
}
117+
}
118+
119+
return results
120+
}
121+
122+
func validateBundle(bundle declcfg.Bundle) ValidationResult {
123+
// Parse properties
124+
props, err := property.Parse(bundle.Properties)
125+
if err != nil {
126+
// Can't parse properties, skip this bundle
127+
return ValidationResult{}
128+
}
129+
130+
// Check for olm.package property with version containing "+"
131+
var packageProp *property.Package
132+
for _, p := range props.Packages {
133+
if strings.Contains(p.Version, "+") {
134+
packageProp = &p
135+
break
136+
}
137+
}
138+
139+
// Check for substitutesFor annotation
140+
hasSubstitutesFor := false
141+
for _, csvMeta := range props.CSVMetadatas {
142+
if _, ok := csvMeta.Annotations[substitutesForAnnotation]; ok {
143+
hasSubstitutesFor = true
144+
break
145+
}
146+
}
147+
148+
// Only validate freshmaker bundles
149+
isFreshmaker := packageProp != nil && hasSubstitutesFor
150+
if !isFreshmaker {
151+
return ValidationResult{}
152+
}
153+
154+
result := ValidationResult{
155+
Schema: "olm.bundle",
156+
Name: bundle.Name,
157+
Package: bundle.Package,
158+
Valid: true,
159+
Errors: []string{},
160+
}
161+
162+
// Extract release version (portion after "+")
163+
parts := strings.SplitN(packageProp.Version, "+", 2)
164+
if len(parts) != 2 {
165+
result.Valid = false
166+
result.Errors = append(result.Errors, "version contains '+' but no release version found")
167+
return result
168+
}
169+
170+
versionWithoutRelease := parts[0]
171+
releaseVersion := parts[1]
172+
173+
// Construct the expected name
174+
constructedName := fmt.Sprintf("%s-v%s-%s", bundle.Package, versionWithoutRelease, releaseVersion)
175+
176+
// Validate release version format (dot-delimited sequences of alphanumerics and hyphens)
177+
if !isValidReleaseVersion(releaseVersion) {
178+
result.Valid = false
179+
result.Errors = append(result.Errors,
180+
fmt.Sprintf("release version %q has invalid format (must be dot-delimited sequences of alphanumerics and hyphens)", releaseVersion))
181+
}
182+
183+
// Validate release version length
184+
if len(releaseVersion) > maxReleaseLength {
185+
result.Valid = false
186+
result.Errors = append(result.Errors,
187+
fmt.Sprintf("release version %q exceeds maximum length of %d characters (length: %d)",
188+
releaseVersion, maxReleaseLength, len(releaseVersion)))
189+
}
190+
191+
// Validate total constructed name length
192+
if len(constructedName) > maxNameLength {
193+
result.Valid = false
194+
result.Errors = append(result.Errors,
195+
fmt.Sprintf("constructed name %q exceeds maximum length of %d characters (length: %d)",
196+
constructedName, maxNameLength, len(constructedName)))
197+
}
198+
199+
return result
200+
}
201+
202+
// isValidReleaseVersion checks if the release version is composed of dot-delimited sequences
203+
// of alphanumerics and hyphens
204+
func isValidReleaseVersion(s string) bool {
205+
matched, _ := regexp.MatchString(`^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$`, s)
206+
return matched
207+
}
208+
209+
func writeJSON(output ValidationOutput, w io.Writer) error {
210+
enc := json.NewEncoder(w)
211+
enc.SetIndent("", " ")
212+
enc.SetEscapeHTML(false)
213+
return enc.Encode(output)
214+
}
215+
216+
func writeYAML(output ValidationOutput, w io.Writer) error {
217+
// Convert to JSON bytes first
218+
data, err := json.Marshal(output)
219+
if err != nil {
220+
return err
221+
}
222+
223+
// Create a temporary DeclarativeConfig to use the existing WriteYAML encoder
224+
// Since we have a simple structure, we'll just use JSON for now
225+
// (In production, you might want to use a proper YAML library)
226+
enc := json.NewEncoder(w)
227+
enc.SetIndent("", " ")
228+
enc.SetEscapeHTML(false)
229+
var obj interface{}
230+
if err := json.Unmarshal(data, &obj); err != nil {
231+
return err
232+
}
233+
return enc.Encode(obj)
234+
}
235+
236+
func writeText(output ValidationOutput, w io.Writer) error {
237+
var total, valid, invalid int
238+
239+
for _, r := range output.Results {
240+
total++
241+
if r.Valid {
242+
valid++
243+
} else {
244+
invalid++
245+
}
246+
}
247+
248+
fmt.Fprintf(w, "Freshmaker Bundle Validation Summary\n")
249+
fmt.Fprintf(w, "=====================================\n\n")
250+
fmt.Fprintf(w, "Total freshmaker bundles: %d\n", total)
251+
fmt.Fprintf(w, "Valid: %d\n", valid)
252+
fmt.Fprintf(w, "Invalid: %d\n\n", invalid)
253+
254+
if invalid > 0 {
255+
fmt.Fprintf(w, "Invalid Bundles:\n")
256+
fmt.Fprintf(w, "----------------\n\n")
257+
for _, r := range output.Results {
258+
if !r.Valid {
259+
fmt.Fprintf(w, "Bundle: %s\n", r.Name)
260+
fmt.Fprintf(w, " Package: %s\n", r.Package)
261+
fmt.Fprintf(w, " Validation Errors:\n")
262+
for _, err := range r.Errors {
263+
fmt.Fprintf(w, " - %s\n", err)
264+
}
265+
fmt.Fprintf(w, "\n")
266+
}
267+
}
268+
}
269+
270+
if valid > 0 {
271+
fmt.Fprintf(w, "Valid Bundles:\n")
272+
fmt.Fprintf(w, "--------------\n\n")
273+
for _, r := range output.Results {
274+
if r.Valid {
275+
fmt.Fprintf(w, " - %s (package: %s)\n", r.Name, r.Package)
276+
}
277+
}
278+
}
279+
280+
return nil
281+
}

0 commit comments

Comments
 (0)