go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers-sdk/v1/lr/cli/cmd/docs.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package cmd 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "os" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 15 "github.com/rs/zerolog/log" 16 "github.com/spf13/cobra" 17 "go.mondoo.com/cnquery/providers-sdk/v1/lr" 18 "go.mondoo.com/cnquery/providers-sdk/v1/lr/docs" 19 "sigs.k8s.io/yaml" 20 ) 21 22 func init() { 23 docsYamlCmd.Flags().String("docs-file", "", "optional file path to write content to a file") 24 docsYamlCmd.Flags().String("version", defaultVersion, "optional version to mark resource, default is latest") 25 docsCmd.AddCommand(docsYamlCmd) 26 docsJSONCmd.Flags().String("dist", "", "folder for output json generation") 27 docsCmd.AddCommand(docsJSONCmd) 28 rootCmd.AddCommand(docsCmd) 29 } 30 31 const defaultVersion = "latest" 32 33 var docsCmd = &cobra.Command{ 34 Use: "docs", 35 } 36 37 var docsYamlCmd = &cobra.Command{ 38 Use: "yaml", 39 Short: "generates yaml docs skeleton file and merges it into existing definition", 40 Long: `parse an LR file and generates a yaml file structure for additional documentation.`, 41 Args: cobra.MinimumNArgs(1), 42 Run: func(cmd *cobra.Command, args []string) { 43 raw, err := os.ReadFile(args[0]) 44 if err != nil { 45 log.Error().Msg(err.Error()) 46 return 47 } 48 49 res, err := lr.Parse(string(raw)) 50 if err != nil { 51 log.Error().Msg(err.Error()) 52 return 53 } 54 55 // to ensure we generate the same markdown, we sort the resources first 56 sort.SliceStable(res.Resources, func(i, j int) bool { 57 return res.Resources[i].ID < res.Resources[j].ID 58 }) 59 60 filepath, err := cmd.Flags().GetString("docs-file") 61 if err != nil { 62 log.Fatal().Err(err).Msg("invalid argument for `file`") 63 } 64 65 version, err := cmd.Flags().GetString("version") 66 if err != nil { 67 log.Fatal().Err(err).Msg("invalid argument for `version`") 68 } 69 70 d := docs.LrDocs{ 71 Resources: map[string]*docs.LrDocsEntry{}, 72 } 73 74 fields := map[string][]*lr.BasicField{} 75 isPrivate := map[string]bool{} 76 for i := range res.Resources { 77 id := res.Resources[i].ID 78 isPrivate[id] = res.Resources[i].IsPrivate 79 d.Resources[id] = nil 80 if res.Resources[i].Body != nil { 81 basicFields := []*lr.BasicField{} 82 for _, f := range res.Resources[i].Body.Fields { 83 if f.BasicField != nil { 84 basicFields = append(basicFields, f.BasicField) 85 } 86 } 87 fields[id] = basicFields 88 } 89 } 90 91 // default behaviour is to output the result on cli 92 if filepath == "" { 93 data, err := yaml.Marshal(d) 94 if err != nil { 95 log.Fatal().Err(err).Msg("could not marshal docs") 96 } 97 98 fmt.Println(string(data)) 99 return 100 } 101 102 // if an file was provided, we check if the file exist and merge existing content with the new resources 103 // to ensure that existing documentation stays available 104 var existingData docs.LrDocs 105 _, err = os.Stat(filepath) 106 if err == nil { 107 log.Info().Msg("load existing data") 108 content, err := os.ReadFile(filepath) 109 if err != nil { 110 log.Fatal().Err(err).Msg("could not read file " + filepath) 111 } 112 err = yaml.Unmarshal(content, &existingData) 113 if err != nil { 114 log.Fatal().Err(err).Msg("could not load yaml data") 115 } 116 117 log.Info().Msg("merge content") 118 for k := range existingData.Resources { 119 v := existingData.Resources[k] 120 d.Resources[k] = v 121 } 122 } 123 124 // ensure default values and fields are set 125 for k := range d.Resources { 126 d.Resources[k] = ensureDefaults(k, d.Resources[k], version) 127 mergeFields(version, d.Resources[k], fields[k]) 128 // Merge in other doc fields from core.lr 129 d.Resources[k].IsPrivate = isPrivate[k] 130 } 131 132 // generate content 133 data, err := yaml.Marshal(d) 134 if err != nil { 135 log.Fatal().Err(err).Msg("could not marshal docs") 136 } 137 138 // add license header 139 data = append([]byte("# Copyright (c) Mondoo, Inc.\n# SPDX-License-Identifier: BUSL-1.1\n\n"), data...) 140 141 log.Info().Str("file", filepath).Msg("write file") 142 err = os.WriteFile(filepath, data, 0o700) 143 if err != nil { 144 log.Fatal().Err(err).Msg("could not write docs file") 145 } 146 }, 147 } 148 149 var platformMapping = map[string][]string{ 150 "aws": {"aws"}, 151 "gcp": {"gcloud"}, 152 "k8s": {"kubernetes"}, 153 "azure": {"azure"}, 154 "azurerm": {"azure"}, 155 "arista": {"arista-eos"}, 156 "equinix": {"equinix"}, 157 "ms365": {"microsoft365"}, 158 "msgraph": {"microsoft365"}, 159 "vsphere": {"vmware-esxi", "vmware-vsphere"}, 160 "esxi": {"vmware-esxi", "vmware-vsphere"}, 161 "terraform": {"terraform"}, 162 } 163 164 func ensureDefaults(id string, entry *docs.LrDocsEntry, version string) *docs.LrDocsEntry { 165 for k := range platformMapping { 166 if entry == nil { 167 entry = &docs.LrDocsEntry{} 168 } 169 if entry.MinMondooVersion == "" { 170 entry.MinMondooVersion = version 171 } else if entry.MinMondooVersion == defaultVersion && version != defaultVersion { 172 // Update to specified version if previously set to default 173 entry.MinMondooVersion = version 174 } 175 if strings.HasPrefix(id, k) { 176 entry.Platform = &docs.LrDocsPlatform{ 177 Name: platformMapping[k], 178 } 179 } 180 } 181 return entry 182 } 183 184 func mergeFields(version string, entry *docs.LrDocsEntry, fields []*lr.BasicField) { 185 if entry == nil && len(fields) > 0 { 186 entry = &docs.LrDocsEntry{} 187 entry.Fields = map[string]*docs.LrDocsField{} 188 } else if entry == nil { 189 return 190 } else if entry.Fields == nil { 191 entry.Fields = map[string]*docs.LrDocsField{} 192 } 193 docFields := entry.Fields 194 for _, f := range fields { 195 if docFields[f.ID] == nil { 196 fDoc := &docs.LrDocsField{ 197 MinMondooVersion: version, 198 } 199 entry.Fields[f.ID] = fDoc 200 } else if entry.Fields[f.ID].MinMondooVersion == "latest" && version != "latest" { 201 entry.Fields[f.ID].MinMondooVersion = version 202 } 203 // Scrub field version if same as resource 204 if entry.Fields[f.ID].MinMondooVersion == entry.MinMondooVersion { 205 entry.Fields[f.ID].MinMondooVersion = "" 206 } 207 } 208 } 209 210 func extractComments(raw []string) (string, string) { 211 if len(raw) == 0 { 212 return "", "" 213 } 214 215 for i := range raw { 216 raw[i] = strings.Trim(raw[i][2:], " \t\n") 217 } 218 219 title, rest := raw[0], raw[1:] 220 desc := strings.Join(rest, " ") 221 222 return title, desc 223 } 224 225 var docsJSONCmd = &cobra.Command{ 226 Use: "json", 227 Short: "convert yaml docs manifest into json", 228 Long: `convert a yaml-based docs manifest into its json description, ready for loading`, 229 Args: cobra.MinimumNArgs(1), 230 Run: func(cmd *cobra.Command, args []string) { 231 file := args[0] 232 233 dist, err := cmd.Flags().GetString("dist") 234 if err != nil { 235 log.Fatal().Err(err).Msg("failed to get dist flag") 236 } 237 238 // without dist we want the file to be put alongside the original 239 if dist == "" { 240 src, err := filepath.Abs(file) 241 if err != nil { 242 log.Fatal().Err(err).Msg("cannot figure out the absolute path for the source file") 243 } 244 dist = filepath.Dir(src) 245 } 246 247 raw, err := os.ReadFile(file) 248 if err != nil { 249 log.Fatal().Err(err) 250 } 251 252 var lrDocsData docs.LrDocs 253 err = yaml.Unmarshal(raw, &lrDocsData) 254 if err != nil { 255 log.Fatal().Err(err).Msg("could not load yaml data") 256 } 257 258 out, err := json.Marshal(&lrDocsData) 259 if err != nil { 260 log.Fatal().Err(err).Msg("failed to convert yaml to json") 261 } 262 263 if err = os.MkdirAll(dist, 0o755); err != nil { 264 log.Fatal().Err(err).Msg("failed to create dist folder") 265 } 266 infoFile := path.Join(dist, strings.TrimSuffix(path.Base(args[0]), ".yaml")+".json") 267 err = os.WriteFile(infoFile, []byte(out), 0o644) 268 if err != nil { 269 log.Fatal().Err(err).Str("path", infoFile).Msg("failed to write to json file") 270 } 271 }, 272 }