Skip to content

Commit f9dc003

Browse files
authored
Merge pull request #125 from devtron-labs/env-gen-common-lib-move
chore: env gen common lib
2 parents 90a2b2a + 365a00c commit f9dc003

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed

common-lib/fetchAllEnv/fetchAllEnv.go

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fetchAllEnv
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"go/ast"
23+
"go/parser"
24+
"go/token"
25+
"log"
26+
"os"
27+
"path/filepath"
28+
"reflect"
29+
"sort"
30+
"strings"
31+
"text/template"
32+
)
33+
34+
type EnvField struct {
35+
Env string
36+
EnvType string
37+
EnvValue string
38+
EnvDescription string
39+
Example string
40+
Deprecated string
41+
}
42+
43+
type CategoryField struct {
44+
Category string
45+
Fields []EnvField
46+
}
47+
48+
const (
49+
categoryCommentStructPrefix = "CATEGORY="
50+
defaultCategory = "DEVTRON"
51+
deprecatedDefaultValue = "false"
52+
53+
envFieldTypeTag = "env"
54+
envDefaultFieldTypeTag = "envDefault"
55+
envDescriptionFieldTypeTag = "description"
56+
envPossibleValuesFieldTypeTag = "example"
57+
envDeprecatedFieldTypeTag = "deprecated"
58+
MARKDOWN_FILENAME = "env_gen.md"
59+
MARKDOWN_JSON_FILENAME = "env_gen.json"
60+
)
61+
62+
const MarkdownTemplate = `
63+
{{range . }}
64+
## {{ .Category }} Related Environment Variables
65+
| Key | Type | Default Value | Description | Example | Deprecated |
66+
|-------|----------|-------------------|-------------------|-----------------------|------------------|
67+
{{range .Fields }} | {{ .Env }} | {{ .EnvType }} |{{ .EnvValue }} | {{ .EnvDescription }} | {{ .Example }} | {{ .Deprecated }} |
68+
{{end}}
69+
{{end}}`
70+
71+
func FetchEnvAndWriteToFile() {
72+
WalkThroughProject()
73+
return
74+
}
75+
76+
func WalkThroughProject() {
77+
categoryFieldsMap := make(map[string][]EnvField)
78+
uniqueKeys := make(map[string]bool)
79+
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
80+
if err != nil {
81+
return err
82+
}
83+
if !info.IsDir() && strings.HasSuffix(path, ".go") {
84+
err = processGoFile(path, categoryFieldsMap, uniqueKeys)
85+
if err != nil {
86+
log.Println("error in processing go file", err)
87+
return err
88+
}
89+
}
90+
return nil
91+
})
92+
if err != nil {
93+
return
94+
}
95+
writeToFile(categoryFieldsMap)
96+
}
97+
98+
func processGoFile(filePath string, categoryFieldsMap map[string][]EnvField, uniqueKeys map[string]bool) error {
99+
fset := token.NewFileSet()
100+
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
101+
if err != nil {
102+
log.Println("error parsing file:", err)
103+
return err
104+
}
105+
ast.Inspect(node, func(n ast.Node) bool {
106+
if genDecl, ok := n.(*ast.GenDecl); ok {
107+
// checking if type declaration, one of [func, map, struct, array, channel, interface]
108+
if genDecl.Tok == token.TYPE {
109+
for _, spec := range genDecl.Specs {
110+
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
111+
// only checking struct type declarations
112+
if structType, ok2 := typeSpec.Type.(*ast.StructType); ok2 {
113+
allFields := make([]EnvField, 0, len(structType.Fields.List))
114+
for _, field := range structType.Fields.List {
115+
if field.Tag != nil {
116+
envField := getEnvKeyAndValue(field)
117+
envKey := envField.Env
118+
if len(envKey) == 0 || uniqueKeys[envKey] {
119+
continue
120+
}
121+
allFields = append(allFields, envField)
122+
uniqueKeys[envKey] = true
123+
}
124+
}
125+
if len(allFields) > 0 {
126+
category := getCategoryForAStruct(genDecl)
127+
categoryFieldsMap[category] = append(categoryFieldsMap[category], allFields...)
128+
}
129+
}
130+
}
131+
}
132+
}
133+
}
134+
return true
135+
})
136+
return nil
137+
}
138+
139+
func getEnvKeyAndValue(field *ast.Field) EnvField {
140+
tag := reflect.StructTag(strings.Trim(field.Tag.Value, "`")) // remove surrounding backticks
141+
142+
envKey := addReadmeTableDelimiterEscapeChar(tag.Get(envFieldTypeTag))
143+
envValue := addReadmeTableDelimiterEscapeChar(tag.Get(envDefaultFieldTypeTag))
144+
envDescription := addReadmeTableDelimiterEscapeChar(tag.Get(envDescriptionFieldTypeTag))
145+
envPossibleValues := addReadmeTableDelimiterEscapeChar(tag.Get(envPossibleValuesFieldTypeTag))
146+
envDeprecated := addReadmeTableDelimiterEscapeChar(tag.Get(envDeprecatedFieldTypeTag))
147+
// check if there exist any value provided in env for this field
148+
if value, ok := os.LookupEnv(envKey); ok {
149+
envValue = value
150+
}
151+
env := EnvField{
152+
Env: envKey,
153+
EnvValue: envValue,
154+
EnvDescription: envDescription,
155+
Example: envPossibleValues,
156+
Deprecated: envDeprecated,
157+
}
158+
if indent, ok := field.Type.(*ast.Ident); ok && indent != nil {
159+
env.EnvType = indent.Name
160+
}
161+
if len(envDeprecated) == 0 {
162+
env.Deprecated = deprecatedDefaultValue
163+
}
164+
return env
165+
}
166+
167+
func getCategoryForAStruct(genDecl *ast.GenDecl) string {
168+
category := defaultCategory
169+
if genDecl.Doc != nil {
170+
commentTexts := strings.Split(genDecl.Doc.Text(), "\n")
171+
for _, comment := range commentTexts {
172+
commentText := strings.TrimPrefix(strings.ReplaceAll(comment, " ", ""), "//") // this can happen if comment group is in /* */
173+
if strings.HasPrefix(commentText, categoryCommentStructPrefix) {
174+
categories := strings.Split(strings.TrimPrefix(commentText, categoryCommentStructPrefix), ",")
175+
if len(categories) > 0 && len(categories[0]) > 0 { //only supporting one category as of now
176+
category = categories[0] //overriding category
177+
break
178+
}
179+
}
180+
}
181+
}
182+
return category
183+
}
184+
185+
func addReadmeTableDelimiterEscapeChar(s string) string {
186+
return strings.ReplaceAll(s, "|", `\|`)
187+
}
188+
189+
func writeToFile(categoryFieldsMap map[string][]EnvField) {
190+
cfs := make([]CategoryField, 0, len(categoryFieldsMap))
191+
for category, allFields := range categoryFieldsMap {
192+
sort.Slice(allFields, func(i, j int) bool {
193+
return allFields[i].Env < allFields[j].Env
194+
})
195+
196+
cfs = append(cfs, CategoryField{
197+
Category: category,
198+
Fields: allFields,
199+
})
200+
}
201+
sort.Slice(cfs, func(i, j int) bool {
202+
return cfs[i].Category < cfs[j].Category
203+
})
204+
file, err := os.Create(MARKDOWN_FILENAME)
205+
if err != nil && !errors.Is(err, os.ErrExist) {
206+
panic(err)
207+
}
208+
defer file.Close()
209+
tmpl, err := template.New("markdown").Parse(MarkdownTemplate)
210+
if err != nil {
211+
panic(err)
212+
}
213+
err = tmpl.Execute(file, cfs)
214+
if err != nil {
215+
panic(err)
216+
}
217+
cfsMarshaled, err := json.Marshal(cfs)
218+
if err != nil {
219+
log.Println("error marshalling category fields:", err)
220+
panic(err)
221+
}
222+
err = os.WriteFile(MARKDOWN_JSON_FILENAME, cfsMarshaled, 0644)
223+
if err != nil {
224+
panic(err)
225+
}
226+
}

0 commit comments

Comments
 (0)