|
| 1 | +// Copyright 2022 Lekko Technologies, Inc. |
| 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 main |
| 16 | + |
| 17 | +import ( |
| 18 | + "bytes" |
| 19 | + "fmt" |
| 20 | + "os" |
| 21 | + "os/exec" |
| 22 | + "regexp" |
| 23 | + "sort" |
| 24 | + "strings" |
| 25 | + "text/template" |
| 26 | + |
| 27 | + featurev1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/feature/v1beta1" |
| 28 | + "github.com/lekkodev/cli/pkg/repo" |
| 29 | + "github.com/lekkodev/cli/pkg/secrets" |
| 30 | + "github.com/pkg/errors" |
| 31 | + "github.com/spf13/cobra" |
| 32 | + "golang.org/x/mod/modfile" |
| 33 | + "google.golang.org/protobuf/proto" |
| 34 | +) |
| 35 | + |
| 36 | +func genGoCmd() *cobra.Command { |
| 37 | + var ns string |
| 38 | + var wd string |
| 39 | + var of string |
| 40 | + cmd := &cobra.Command{ |
| 41 | + Use: "go", |
| 42 | + Short: "generate Go library code from configs", |
| 43 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 44 | + b, err := os.ReadFile("go.mod") |
| 45 | + if err != nil { |
| 46 | + return err |
| 47 | + } |
| 48 | + mf, err := modfile.ParseLax("go.mod", b, nil) |
| 49 | + if err != nil { |
| 50 | + return err |
| 51 | + } |
| 52 | + moduleRoot := mf.Module.Mod.Path |
| 53 | + |
| 54 | + rs := secrets.NewSecretsOrFail() |
| 55 | + r, err := repo.NewLocal(wd, rs) |
| 56 | + if err != nil { |
| 57 | + return errors.Wrap(err, "new repo") |
| 58 | + } |
| 59 | + ffs, err := r.GetFeatureFiles(cmd.Context(), ns) |
| 60 | + if err != nil { |
| 61 | + return err |
| 62 | + } |
| 63 | + sort.SliceStable(ffs, func(i, j int) bool { |
| 64 | + return ffs[i].CompiledProtoBinFileName < ffs[j].CompiledProtoBinFileName |
| 65 | + }) |
| 66 | + var protoAsByteStrings []string |
| 67 | + var codeStrings []string |
| 68 | + protoImportSet := make(map[string]*protoImport) |
| 69 | + for _, ff := range ffs { |
| 70 | + fff, err := os.ReadFile(wd + "/" + ns + "/" + ff.CompiledProtoBinFileName) |
| 71 | + if err != nil { |
| 72 | + return err |
| 73 | + } |
| 74 | + f := &featurev1beta1.Feature{} |
| 75 | + if err := proto.Unmarshal(fff, f); err != nil { |
| 76 | + return err |
| 77 | + } |
| 78 | + codeString, err := genGoForFeature(f, ns) |
| 79 | + if err != nil { |
| 80 | + return err |
| 81 | + } |
| 82 | + if f.Type == featurev1beta1.FeatureType_FEATURE_TYPE_PROTO { |
| 83 | + protoImport := unpackProtoType(moduleRoot, f.Tree.Default.TypeUrl) |
| 84 | + protoImportSet[protoImport.ImportPath] = &protoImport |
| 85 | + } |
| 86 | + protoAsBytes := fmt.Sprintf("\t\t\"%s\": []byte{", f.Key) |
| 87 | + for idx, b := range fff { |
| 88 | + if idx%16 == 0 { |
| 89 | + protoAsBytes += "\n\t\t\t" |
| 90 | + } else { |
| 91 | + protoAsBytes += " " |
| 92 | + } |
| 93 | + protoAsBytes += fmt.Sprintf("0x%02x,", b) |
| 94 | + } |
| 95 | + protoAsBytes += "\n\t\t},\n" |
| 96 | + protoAsByteStrings = append(protoAsByteStrings, protoAsBytes) |
| 97 | + codeStrings = append(codeStrings, codeString) |
| 98 | + } |
| 99 | + const templateBody = `package lekko{{$.Namespace}} |
| 100 | +
|
| 101 | +import ( |
| 102 | +{{range $.ProtoImports}} |
| 103 | + {{ . }}{{end}} |
| 104 | + "context" |
| 105 | + client "github.com/lekkodev/go-sdk/client" |
| 106 | +) |
| 107 | +
|
| 108 | +type LekkoClient struct { |
| 109 | + client.Client |
| 110 | + Close client.CloseFunc |
| 111 | +} |
| 112 | +
|
| 113 | +type SafeLekkoClient struct { |
| 114 | + client.Client |
| 115 | + Close client.CloseFunc |
| 116 | +} |
| 117 | +
|
| 118 | +func (c *SafeLekkoClient) GetBool(ctx context.Context, namespace string, key string) bool { |
| 119 | + res, err := c.Client.GetBool(ctx, namespace, key) |
| 120 | + if err != nil { |
| 121 | + panic(err) |
| 122 | + } |
| 123 | + return res |
| 124 | +} |
| 125 | +func (c *SafeLekkoClient) GetString(ctx context.Context, namespace string, key string) string { |
| 126 | + res, err := c.Client.GetString(ctx, namespace, key) |
| 127 | + if err != nil { |
| 128 | + panic(err) |
| 129 | + } |
| 130 | + return res |
| 131 | +} |
| 132 | +
|
| 133 | +func (c *SafeLekkoClient) GetFloat(ctx context.Context, namespace string, key string) float64 { |
| 134 | + res, err := c.Client.GetFloat(ctx, namespace, key) |
| 135 | + if err != nil { |
| 136 | + panic(err) |
| 137 | + } |
| 138 | + return res |
| 139 | +} |
| 140 | +
|
| 141 | +func (c *SafeLekkoClient) GetInt(ctx context.Context, namespace string, key string) int64 { |
| 142 | + res, err := c.Client.GetInt(ctx, namespace, key) |
| 143 | + if err != nil { |
| 144 | + panic(err) |
| 145 | + } |
| 146 | + return res |
| 147 | +} |
| 148 | +
|
| 149 | +var StaticConfig = map[string]map[string][]byte{ |
| 150 | + "{{$.Namespace}}": { |
| 151 | +{{range $.ProtoAsByteStrings}}{{ . }}{{end}} }, |
| 152 | +} |
| 153 | +{{range $.CodeStrings}} |
| 154 | +{{ . }}{{end}}` |
| 155 | + |
| 156 | + // buf generate --template '{"version":"v1","plugins":[{"plugin":"go","out":"gen/go"}]}' |
| 157 | + // |
| 158 | + // This generates the code for the config repo, assuming it has a buf.gen.yml in that repo. |
| 159 | + // In OUR repos, and maybe some of our customers, they may already have a buf.gen.yml, so if |
| 160 | + // that is the case, we should identify that, not run code gen (maybe?) and instead need to |
| 161 | + // take the prefix by parsing the buf.gen.yml to understand where the go code goes. |
| 162 | + pCmd := exec.Command( |
| 163 | + "buf", |
| 164 | + "generate", |
| 165 | + fmt.Sprintf(`--template={"managed": {"enabled": true, "go_package_prefix": {"default": "%s/internal/lekko/proto"}}, "version":"v1","plugins":[{"plugin":"go","out":"internal/lekko/proto", "opt": "paths=source_relative"}]}`, moduleRoot), |
| 166 | + "--include-imports", |
| 167 | + wd) // #nosec G204 |
| 168 | + pCmd.Dir = "." |
| 169 | + fmt.Println("executing in wd: " + wd + " command: " + pCmd.String()) |
| 170 | + if out, err := pCmd.CombinedOutput(); err != nil { |
| 171 | + fmt.Printf("Error when generating code with buf: %s\n %e\n", out, err) |
| 172 | + return err |
| 173 | + } |
| 174 | + if err := os.MkdirAll("./internal/lekko/"+ns, 0770); err != nil { |
| 175 | + return err |
| 176 | + } |
| 177 | + f, err := os.Create("./internal/lekko/" + ns + "/" + of) |
| 178 | + if err != nil { |
| 179 | + return err |
| 180 | + } |
| 181 | + var protoImports []string |
| 182 | + for _, imp := range protoImportSet { |
| 183 | + protoImports = append(protoImports, fmt.Sprintf(`%s "%s"`, imp.PackageAlias, imp.ImportPath)) |
| 184 | + } |
| 185 | + data := struct { |
| 186 | + ProtoImports []string |
| 187 | + Namespace string |
| 188 | + ProtoAsByteStrings []string |
| 189 | + CodeStrings []string |
| 190 | + }{ |
| 191 | + protoImports, |
| 192 | + ns, |
| 193 | + protoAsByteStrings, |
| 194 | + codeStrings, |
| 195 | + } |
| 196 | + templ := template.Must(template.New("").Parse(templateBody)) |
| 197 | + return templ.Execute(f, data) |
| 198 | + }, |
| 199 | + } |
| 200 | + cmd.Flags().StringVarP(&ns, "namespace", "n", "default", "namespace to generate code from") |
| 201 | + cmd.Flags().StringVarP(&wd, "config-path", "c", ".", "path to configuration repository") |
| 202 | + cmd.Flags().StringVarP(&of, "output", "o", "lekko.go", "output file") |
| 203 | + return cmd |
| 204 | +} |
| 205 | + |
| 206 | +var genCmd = &cobra.Command{ |
| 207 | + Use: "gen", |
| 208 | + Short: "generate library code from configs", |
| 209 | +} |
| 210 | + |
| 211 | +func genGoForFeature(f *featurev1beta1.Feature, ns string) (string, error) { |
| 212 | + const defaultTemplateBody = `// {{$.Description}} |
| 213 | +func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) ({{$.RetType}}, error) { |
| 214 | + return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}") |
| 215 | +} |
| 216 | +
|
| 217 | +// {{$.Description}} |
| 218 | +func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) {{$.RetType}} { |
| 219 | + return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}") |
| 220 | +} |
| 221 | +` |
| 222 | + |
| 223 | + const protoTemplateBody = `// {{$.Description}} |
| 224 | +func (c *LekkoClient) {{$.FuncName}}(ctx context.Context) (*{{$.RetType}}, error) { |
| 225 | + result := &{{$.RetType}}{} |
| 226 | + err := c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result) |
| 227 | + return result, err |
| 228 | +} |
| 229 | +
|
| 230 | +// {{$.Description}} |
| 231 | +func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context) *{{$.RetType}} { |
| 232 | + result := &{{$.RetType}}{} |
| 233 | + c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result) |
| 234 | + return result |
| 235 | +} |
| 236 | +` |
| 237 | + const jsonTemplateBody = `// {{$.Description}} |
| 238 | +func (c *LekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}) error { |
| 239 | + return c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result) |
| 240 | +} |
| 241 | +
|
| 242 | +// {{$.Description}} |
| 243 | +func (c *SafeLekkoClient) {{$.FuncName}}(ctx context.Context, result interface{}) { |
| 244 | + c.{{$.GetFunction}}(ctx, "{{$.Namespace}}", "{{$.Key}}", result) |
| 245 | +} |
| 246 | +` |
| 247 | + var funcNameBuilder strings.Builder |
| 248 | + funcNameBuilder.WriteString("Get") |
| 249 | + for _, word := range regexp.MustCompile("[_-]+").Split(f.Key, -1) { |
| 250 | + funcNameBuilder.WriteString(strings.ToUpper(word[:1]) + word[1:]) |
| 251 | + } |
| 252 | + funcName := funcNameBuilder.String() |
| 253 | + var retType string |
| 254 | + var getFunction string |
| 255 | + templateBody := defaultTemplateBody |
| 256 | + switch f.Type { |
| 257 | + case 1: |
| 258 | + retType = "bool" |
| 259 | + getFunction = "GetBool" |
| 260 | + case 2: |
| 261 | + retType = "int64" |
| 262 | + getFunction = "GetInt" |
| 263 | + case 3: |
| 264 | + retType = "float64" |
| 265 | + getFunction = "GetFloat" |
| 266 | + case 4: |
| 267 | + retType = "string" |
| 268 | + getFunction = "GetString" |
| 269 | + case 5: |
| 270 | + getFunction = "GetJSON" |
| 271 | + templateBody = jsonTemplateBody |
| 272 | + case 6: |
| 273 | + getFunction = "GetProto" |
| 274 | + templateBody = protoTemplateBody |
| 275 | + // we don't need the import path so sending in empty string |
| 276 | + protoType := unpackProtoType("", f.Tree.Default.TypeUrl) |
| 277 | + // creates configv1beta1.DBConfig |
| 278 | + retType = fmt.Sprintf("%s.%s", protoType.PackageAlias, protoType.Type) |
| 279 | + } |
| 280 | + |
| 281 | + data := struct { |
| 282 | + Description string |
| 283 | + FuncName string |
| 284 | + GetFunction string |
| 285 | + RetType string |
| 286 | + Namespace string |
| 287 | + Key string |
| 288 | + }{ |
| 289 | + f.Description, |
| 290 | + funcName, |
| 291 | + getFunction, |
| 292 | + retType, |
| 293 | + ns, |
| 294 | + f.Key, |
| 295 | + } |
| 296 | + templ, err := template.New("go func").Parse(templateBody) |
| 297 | + if err != nil { |
| 298 | + return "", err |
| 299 | + } |
| 300 | + var ret bytes.Buffer |
| 301 | + err = templ.Execute(&ret, data) |
| 302 | + return ret.String(), err |
| 303 | +} |
| 304 | + |
| 305 | +type protoImport struct { |
| 306 | + PackageAlias string |
| 307 | + ImportPath string |
| 308 | + Type string |
| 309 | +} |
| 310 | + |
| 311 | +func unpackProtoType(moduleRoot string, typeURL string) protoImport { |
| 312 | + anyURLSplit := strings.Split(typeURL, "/") |
| 313 | + if anyURLSplit[0] != "type.googleapis.com" { |
| 314 | + panic("invalid any type url: " + typeURL) |
| 315 | + } |
| 316 | + // turn default.config.v1beta1.DBConfig into: |
| 317 | + // moduleRoot/internal/lekko/proto/default/config/v1beta1 |
| 318 | + typeParts := strings.Split(anyURLSplit[1], ".") |
| 319 | + |
| 320 | + importPath := strings.Join(append([]string{moduleRoot + "/internal/lekko/proto"}, typeParts[:len(typeParts)-1]...), "/") |
| 321 | + |
| 322 | + prefix := fmt.Sprintf(`%s%s`, typeParts[len(typeParts)-3], typeParts[len(typeParts)-2]) |
| 323 | + |
| 324 | + // TODO do google.protobuf.X |
| 325 | + switch anyURLSplit[1] { |
| 326 | + case "google.protobuf.Duration": |
| 327 | + importPath = "google.golang.org/protobuf/types/known/durationpb" |
| 328 | + prefix = "durationpb" |
| 329 | + default: |
| 330 | + } |
| 331 | + |
| 332 | + return protoImport{PackageAlias: prefix, ImportPath: importPath, Type: typeParts[len(typeParts)-1]} |
| 333 | +} |
0 commit comments