istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/tools/featuresgen/cmd/root.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cmd
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"reflect"
    21  	"regexp"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/spf13/cobra"
    26  	"gopkg.in/yaml.v2"
    27  )
    28  
    29  const (
    30  	Copyright = `// Copyright Istio Authors
    31  //
    32  // Licensed under the Apache License, Version 2.0 (the "License");
    33  // you may not use this file except in compliance with the License.
    34  // You may obtain a copy of the License at
    35  //
    36  //     http://www.apache.org/licenses/LICENSE-2.0
    37  //
    38  // Unless required by applicable law or agreed to in writing, software
    39  // distributed under the License is distributed on an "AS IS" BASIS,
    40  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    41  // See the License for the specific language governing permissions and
    42  // limitations under the License.
    43  
    44  `
    45  
    46  	GeneratedWarning = `
    47  //WARNING: THIS IS AN AUTO-GENERATED FILE, DO NOT EDIT.
    48  `
    49  
    50  	TypeDefFeature = `
    51  type Feature string
    52  `
    53  
    54  	Package = `
    55  package features
    56  `
    57  
    58  	ConstOpen = `
    59  const (
    60  `
    61  
    62  	ConstClose = `
    63  )
    64  `
    65  )
    66  
    67  var (
    68  	input  string
    69  	output string
    70  
    71  	rootCmd = &cobra.Command{
    72  		Use:   "featuresgen [OPTIONS]",
    73  		Short: "FeaturesGen generates valid test labels from a yaml file",
    74  		Run: func(cmd *cobra.Command, args []string) {
    75  			createLabelsFile()
    76  		},
    77  	}
    78  
    79  	alphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9-]`)
    80  	dotsRegex         = regexp.MustCompile(`[\.]`)
    81  	replaceDashRegex  = regexp.MustCompile(`-(.)`)
    82  )
    83  
    84  func Execute() {
    85  	if err := rootCmd.Execute(); err != nil {
    86  		fmt.Println("Error running featuresgen:")
    87  		panic(err)
    88  	}
    89  }
    90  
    91  func init() {
    92  	rootCmd.Flags().StringVarP(&input, "inputFile", "i", "features.yaml", "the YAML file to use as input")
    93  	rootCmd.Flags().StringVarP(&output, "outputFile", "o", "features.gen.go", "output Go file with labels as string consts")
    94  }
    95  
    96  // Parses a map in the yaml file
    97  func readMap(m map[any]any, path []string) []string {
    98  	var labels []string
    99  	for k, v := range m {
   100  		// If we see "values," then the element is a root and we shouldn't put it in our label name
   101  		if k == "values" {
   102  			labels = append(labels, readVal(v, path)...)
   103  		} else {
   104  			if len(path) > 0 || k.(string) != "features" {
   105  				path = append(path, k.(string))
   106  			}
   107  			labels = append(labels, readVal(v, path)...)
   108  			if len(path) > 0 {
   109  				path = path[:len(path)-1]
   110  			}
   111  		}
   112  	}
   113  	return labels
   114  }
   115  
   116  // Parses a slice in the yaml file
   117  func readSlice(slc []any, path []string) []string {
   118  	labels := make([]string, 0)
   119  	for _, v := range slc {
   120  		labels = append(labels, readVal(v, path)...)
   121  	}
   122  	return labels
   123  }
   124  
   125  // Determines the type of a node in the yaml file and parses it accordingly
   126  func readVal(v any, path []string) []string {
   127  	typ := reflect.TypeOf(v).Kind()
   128  	if typ == reflect.Int || typ == reflect.String {
   129  		path = append(path, v.(string))
   130  		return []string{createConstantString(path)}
   131  	} else if typ == reflect.Slice {
   132  		return readSlice(v.([]any), path)
   133  	} else if typ == reflect.Map {
   134  		return readMap(v.(map[any]any), path)
   135  	}
   136  	panic("Found invalid type in YAML file")
   137  }
   138  
   139  func removeDashAndTitle(s string) string {
   140  	// nolint: staticcheck
   141  	return strings.Title(s[1:])
   142  }
   143  
   144  // Writes a label to the constants file
   145  func createConstantString(path []string) string {
   146  	name := ""
   147  	value := ""
   148  	for i := 0; i < len(path); i++ {
   149  		namePart := alphanumericRegex.ReplaceAllString(path[i], "")
   150  		namePart = replaceDashRegex.ReplaceAllStringFunc(namePart, removeDashAndTitle)
   151  		// nolint: staticcheck
   152  		namePart = strings.Title(namePart)
   153  		name += namePart
   154  		name += "_"
   155  
   156  		value += dotsRegex.ReplaceAllString(path[i], "")
   157  		value += "."
   158  	}
   159  	name = strings.TrimSuffix(name, "_")
   160  	value = strings.TrimSuffix(value, ".")
   161  	return fmt.Sprintf("\t%s\tFeature = \"%s\"", name, value)
   162  }
   163  
   164  // Reads the yaml file and generates a string constant for each leaf node
   165  func createLabelsFromYaml() string {
   166  	data, err := os.ReadFile(input)
   167  	if err != nil {
   168  		pwd, _ := os.Getwd()
   169  		fmt.Println("Error running featuresgen on file: ", pwd, "/", input)
   170  		panic(err)
   171  	}
   172  	m := make(map[any]any)
   173  
   174  	err = yaml.Unmarshal(data, &m)
   175  	if err != nil {
   176  		pwd, _ := os.Getwd()
   177  		fmt.Println("Error running featuresgen on file: ", pwd, "/", input)
   178  		panic(err)
   179  	}
   180  
   181  	constants := readVal(m, make([]string, 0))
   182  	// The parsing of the yaml file doesn't seem to happen in a consistent order. To avoid a different file every time 'make gen' is run, we sort the list.
   183  	sort.Strings(constants)
   184  	return strings.Join(constants, "\n")
   185  }
   186  
   187  func check(err error) {
   188  	if err != nil {
   189  		panic(err)
   190  	}
   191  }
   192  
   193  // Main function that writes the new generated labels file
   194  func createLabelsFile() {
   195  	f, err := os.Create("./" + output)
   196  	if err != nil {
   197  		fmt.Println("Error running featuresgen:")
   198  		panic(err)
   199  	}
   200  
   201  	defer f.Close()
   202  
   203  	_, err = f.WriteString(Copyright)
   204  	check(err)
   205  	_, err = f.WriteString(GeneratedWarning)
   206  	check(err)
   207  	_, err = f.WriteString(Package)
   208  	check(err)
   209  	_, err = f.WriteString(TypeDefFeature)
   210  	check(err)
   211  	_, err = f.WriteString(ConstOpen)
   212  	check(err)
   213  	_, err = f.WriteString(createLabelsFromYaml())
   214  	check(err)
   215  	_, err = f.WriteString(ConstClose)
   216  	check(err)
   217  	err = f.Sync()
   218  	check(err)
   219  }