diff --git a/README.md b/README.md index 5ee8312..952f9ad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,23 @@ Generate code from launch YML +1. [Running](#running) +2. [Migrating to use in a Golang repo](#migrating-to-use-in-a-golang-repo) +3. [Dependency overrides](#dependency-overrides) + +```sh +$ launch-gen --help +Usage of ./bin/launch-gen: + -d string + Dependency name to override. You can provide multiple dependencies in the format dep1:replacementDep1,dep2:replacementDep2,... + -o string + optional output to file. Default is stdout + -p string + optional package name (default "main") + -skip-dependency value + Dependency to skip generating wag clients. Can be added mulitple times e.g. -skip-dependency a -skip-dependency b +``` + ## Running Build it @@ -74,3 +91,44 @@ generate: 6. Run `make generate`. `launch.go` should be within the specified directory. 7. Call `InitLaunchConfig()` during startup of your program, and use it when needed. + +## Dependencies + +By default, each dependency listed in `dependencies` will generate the following in `launch.go`: + +1. An import of the service's client package – see below +2. The addition of a struct member for the service's `Client` type in the `Dependencies` struct +3. Constructing the client via discovery in `InitLaunchConfig()` +4. Returning the constructed client in the `LaunchConfig` + +By default, `launch-gen` will map a service name specified in `dependencies` to a package of the form `github.com/Clever/[SERVICE_NAME]/gen-go/client`, matching the client package generated by WAG. For example, a dependency on `my-service` generates an import of package `github.com/Clever/my-service/gen-go/client`. You can customize the generated package name using the features documented below. + +### Versions + +If your service's module name and correspondingly its package names include the major version in the format `v2`, `v3`, and so on, you can specify the dependency in the format `my-service@vN`, where `my-service` is the service name and `N` is a whole number representing the major version. + +This mapping assumes that the major version `vN` is appended to the default package name as a suffix: + +``` +my-service@v2 -> github.com/Clever/my-service/gen-go/client/v2 +``` + +This mapping matches the convention used by WAG for services with a major version greater than 1. If your service dependency uses a different format, continue reading to [`Overrides`](#overrides) for greater control over the package name. + +### Overrides + +You can optionally override the package name for a service client dependency with the `-d` arg. Specifying `-d` will change the import statement in the generated `launch.go`. Originally, this option was also used to support services with a major version specified in the package and module names; this use case is now supported directly with the [Versions](#versions) syntax, although you can still achieve the same output with `-d`. + +Dependency mappings are defined as a colon-separated pair of strings, as in `configuredDependency:replacementDependency`. You can specify multiple overrides with a comma-separated string of these mappings: `dependency1:replacementDependency1,dependency2:replacementDependency2`. + +Since `launch.go` depends on the `client` package within each module, `launch-gen` appends a `/gen-go/client` suffix to the module name to specific the service's client package. If you override the dependency, you **must** specify the whole package path yourself; `launch-gen` will not automatically insert the `/gen-go/client` path anywhere in the override package name, since it cannot assume any particular format. + +```sh +$ launch-gen -d "servica-a:service-a/gen-go/client/v4,service-b:api-b-client/special/unique/client" +# service-a -> github.com/Clever/service-a/gen-go/client/v4 +# service-b -> github.com/Clever/api-b-client/special/unique/client +``` + +### Skipping + +If you don't want to generate the package client at all, you can prevent that behavior by specifying `-skip-dependency SERVICE_NAME`. This argument can be specified multiple times. diff --git a/dependencies.go b/dependencies.go new file mode 100644 index 0000000..8c396fe --- /dev/null +++ b/dependencies.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "log" + "regexp" + "strings" + + . "github.com/dave/jennifer/jen" +) + +type ServiceDependency struct { + name string + version string + override string +} + +func parseOverrideDependencies(overrideDependenciesString *string, dependencies []string) map[string]string { + + // parsing through the list of overrides to make an original:new string map + + overrideDependenciesMap := make(map[string]string) + + if overrideDependenciesString == nil || *overrideDependenciesString == "" { + return overrideDependenciesMap + } + + overrideDependenciesList := strings.Split(*overrideDependenciesString, ",") + for _, overrideRule := range overrideDependenciesList { + depReplacementArr := strings.Split(overrideRule, ":") + + if len(depReplacementArr) != 2 || depReplacementArr[1] == "" { + log.Fatal("usage: invalid formatting for the -d flag") + } + + flag := 0 + for _, d := range dependencies { + if d == depReplacementArr[0] { + flag = 1 + break + } + } + + if flag == 0 { + log.Fatal(depReplacementArr[0], " is not a dependency specified in the provided yaml file") + } + + overrideDependenciesMap[depReplacementArr[0]] = depReplacementArr[1] + } + + return overrideDependenciesMap +} + +func parseDependencies( + launchConfig *LaunchYML, + skipDependencies flagsSet, + overridesMap map[string]string, +) []ServiceDependency { + versionedModuleRegex := regexp.MustCompile("(?i)^([a-z][a-z0-9-_]+)@(v[0-9]+)$") + parsed := []ServiceDependency{} + for _, d := range launchConfig.Dependencies { + // skip + if _, ok := skipDependencies[d]; ok { + continue + } + + // check for override + overridePath, hasOverride := overridesMap[d] + if hasOverride { + parsed = append(parsed, ServiceDependency{name: d, override: overridePath}) + continue + } + + // check for configured version + submatches := versionedModuleRegex.FindStringSubmatch(d) + if len(submatches) == 3 { + parsed = append(parsed, ServiceDependency{name: submatches[1], version: submatches[2]}) + continue + } + + // default: just the name + parsed = append(parsed, ServiceDependency{name: d}) + } + + return parsed +} + +func (d ServiceDependency) packageName() string { + // with wagv9 onwards /gen-go/client is after the service name in the package path + importPackage := fmt.Sprintf("%s/gen-go/client", d.name) + if d.override != "" { + importPackage = d.override + } else if d.version != "" { + importPackage = fmt.Sprintf("%s/%s", importPackage, d.version) + } + + return fmt.Sprintf("github.com/Clever/%s", importPackage) +} + +func mapDependenciesToCode(dependencies []ServiceDependency) []Code { + imports := []Code{} + for _, d := range dependencies { + identifier := Id(strings.Title(toPublicVar(d.name))) + + // // with wagv9 onwards /gen-go/client is after the service name in the package path + // importPackage := fmt.Sprintf("%s/gen-go/client", d.name) + // if d.override != "" { + // importPackage = d.override + // } else if d.version != "" { + // importPackage = fmt.Sprintf("%s/%s", importPackage, d.version) + // } + + statement := identifier.Qual(d.packageName(), "Client") + imports = append(imports, statement) + } + + return imports +} diff --git a/dependencies_test.go b/dependencies_test.go new file mode 100644 index 0000000..fa3e5c4 --- /dev/null +++ b/dependencies_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseDependencies(t *testing.T) { + fakeConfig := &LaunchYML{ + Dependencies: []string{ + "service-a", + "service-b", + "service-c@v4", + "service-d", + }, + } + + skip := flagsSet{} + skip.Set("service-b") + result := parseDependencies(fakeConfig, skip, map[string]string{ + "service-d": "service-d/custom/package/path", + }) + + assert.Equal(t, []ServiceDependency{ + { + name: "service-a", + }, + { + name: "service-c", + version: "v4", + }, + { + name: "service-d", + override: "service-d/custom/package/path", + }, + }, result) +} diff --git a/fixtures/launch1.expected b/fixtures/launch1.expected index 6d6df3c..cff6e23 100644 --- a/fixtures/launch1.expected +++ b/fixtures/launch1.expected @@ -2,6 +2,7 @@ package packagename import ( client1 "github.com/Clever/dapple/gen-go/client" + v2 "github.com/Clever/il-config-service/gen-go/client/v2" v9 "github.com/Clever/wag/clientconfig/v9" client "github.com/Clever/workflow-manager/gen-go/client" trace "go.opentelemetry.io/otel/sdk/trace" @@ -23,6 +24,7 @@ type LaunchConfig struct { type Dependencies struct { WorkflowManager client.Client Dapple client1.Client + IlConfigService v2.Client } // Environment has environment variables and their values @@ -55,6 +57,10 @@ func InitLaunchConfig(exp *trace.SpanExporter) LaunchConfig { if err != nil { log.Fatalf("discovery error: %s", err) } + ilConfigService, err := v2.NewFromDiscovery(v9.WithTracing("il-config-service", exporter)) + if err != nil { + log.Fatalf("discovery error: %s", err) + } return LaunchConfig{ AwsResources: AwsResources{ S3ReadAndWriteMe: getS3NameByEnv("read-and-write-me"), @@ -63,6 +69,7 @@ func InitLaunchConfig(exp *trace.SpanExporter) LaunchConfig { }, Deps: Dependencies{ Dapple: dapple, + IlConfigService: ilConfigService, WorkflowManager: workflowManager, }, Env: Environment{ diff --git a/fixtures/launch1.yml b/fixtures/launch1.yml index 6cc2e11..885e362 100644 --- a/fixtures/launch1.yml +++ b/fixtures/launch1.yml @@ -6,6 +6,7 @@ dependencies: - workflow-manager - dapple - dependency-to-skip + - il-config-service@v2 aws: s3: read: diff --git a/fixtures/launch2.expected b/fixtures/launch2.expected index f0cd3f4..093a0c9 100644 --- a/fixtures/launch2.expected +++ b/fixtures/launch2.expected @@ -2,6 +2,7 @@ package packagename import ( v5 "github.com/Clever/dapple/gen-go/client/v5" + v2 "github.com/Clever/il-config-service/gen-go/client/v2" v9 "github.com/Clever/wag/clientconfig/v9" client "github.com/Clever/workflow-manager/gen-go/client" trace "go.opentelemetry.io/otel/sdk/trace" @@ -23,6 +24,7 @@ type LaunchConfig struct { type Dependencies struct { WorkflowManager client.Client Dapple v5.Client + IlConfigService v2.Client } // Environment has environment variables and their values @@ -55,6 +57,10 @@ func InitLaunchConfig(exp *trace.SpanExporter) LaunchConfig { if err != nil { log.Fatalf("discovery error: %s", err) } + ilConfigService, err := v2.NewFromDiscovery(v9.WithTracing("il-config-service", exporter)) + if err != nil { + log.Fatalf("discovery error: %s", err) + } return LaunchConfig{ AwsResources: AwsResources{ S3ReadAndWriteMe: getS3NameByEnv("read-and-write-me"), @@ -63,6 +69,7 @@ func InitLaunchConfig(exp *trace.SpanExporter) LaunchConfig { }, Deps: Dependencies{ Dapple: dapple, + IlConfigService: ilConfigService, WorkflowManager: workflowManager, }, Env: Environment{ diff --git a/fixtures/launch2.yml b/fixtures/launch2.yml index 6cc2e11..885e362 100644 --- a/fixtures/launch2.yml +++ b/fixtures/launch2.yml @@ -6,6 +6,7 @@ dependencies: - workflow-manager - dapple - dependency-to-skip + - il-config-service@v2 aws: s3: read: diff --git a/main.go b/main.go index 0ed8fcc..5f4e404 100644 --- a/main.go +++ b/main.go @@ -60,42 +60,6 @@ func sortedKeys(m map[string]struct{}) []string { return keys } -func parseOverrideDependencies(overrideDependenciesString *string, dependencies []string) map[string]string { - - // parsing through the list of overrides to make an original:new string map - - overrideDependenciesMap := make(map[string]string) - - if overrideDependenciesString == nil || *overrideDependenciesString == "" { - return overrideDependenciesMap - } - - overrideDependenciesList := strings.Split(*overrideDependenciesString, ",") - for _, overrideRule := range overrideDependenciesList { - depReplacementArr := strings.Split(overrideRule, ":") - - if len(depReplacementArr) != 2 || depReplacementArr[1] == "" { - log.Fatal("usage: invalid formatting for the -d flag") - } - - flag := 0 - for _, d := range dependencies { - if d == depReplacementArr[0] { - flag = 1 - break - } - } - - if flag == 0 { - log.Fatal(depReplacementArr[0], " is not a dependency specified in the provided yaml file") - } - - overrideDependenciesMap[depReplacementArr[0]] = depReplacementArr[1] - } - - return overrideDependenciesMap -} - func main() { t := LaunchYML{} packageName := flag.String("p", "main", "optional package name") @@ -142,29 +106,15 @@ func main() { // parseOverrideDependencies parses the list of dependencies to be overwritten into a map overrideDependenciesMap := parseOverrideDependencies(overrideDependenciesString, t.Dependencies) + parsedDependencies := parseDependencies(&t, skipDependencies, overrideDependenciesMap) + depsStruct := mapDependenciesToCode(parsedDependencies) // Dependencies - depsStruct := []Code{} depsInitDict := Dict{} - for _, d := range t.Dependencies { - if _, ok := skipDependencies[d]; ok { - continue - } - - // with wagv9 onwards the following string is after the service name in the path - depPathSuffix := "/gen-go/client" - - importPackage, hasOverride := overrideDependenciesMap[d] - if hasOverride { - // Clients have the version after '/client', so the '/gen-go/client' must be part of the path override already - depPathSuffix = "" - } else { - importPackage = d - } - - depsStruct = append(depsStruct, Id(strings.Title(toPublicVar(d))).Qual(fmt.Sprintf("github.com/Clever/%s%s", importPackage, depPathSuffix), "Client")) - depsInitDict[Id(strings.Title(toPublicVar(d)))] = Id(toPrivateVar(d)) + for _, d := range parsedDependencies { + depsInitDict[Id(strings.Title(toPublicVar(d.name)))] = Id(toPrivateVar(d.name)) } + f.Comment("Dependencies has clients for the service's dependencies") f.Type().Id("Dependencies").Struct(depsStruct...) @@ -211,15 +161,8 @@ func main() { //////////////////// // InitLaunchConfig() function //////////////////// - atLeastOneDep := false - for _, d := range t.Dependencies { - if _, skipped := skipDependencies[d]; !skipped { - atLeastOneDep = true - break - } - } lines := []Code{} - if atLeastOneDep { + if len(parsedDependencies) > 0 { lines = append(lines, []Code{ Id("var exporter ").Qual("go.opentelemetry.io/otel/sdk/trace", "SpanExporter"), If(Id("exp").Op("==").Nil().Block( @@ -230,27 +173,11 @@ func main() { }...) } // Setup a wag client for each dependency - for _, d := range t.Dependencies { - if _, ok := skipDependencies[d]; ok { - continue - } - - // with wagv9 onwards the following string is after the service name in the path - depPathSuffix := "/gen-go/client" - - // checking to see if the dependency name has to be overwritten - depName, hasOverride := overrideDependenciesMap[d] - if hasOverride { - // Clients have the version after '/client', so the '/gen-go/client' must be part of the path override already - depPathSuffix = "" - } else { - depName = d - } - + for _, d := range parsedDependencies { c := []Code{ - List(Id(toPrivateVar(d)), Err()).Op(":="). - Qual(fmt.Sprintf("github.com/Clever/%s%s", depName, depPathSuffix), "NewFromDiscovery"). - Call(Qual("github.com/Clever/wag/clientconfig/v9", "WithTracing").Call(Lit(d), Id("exporter"))), + List(Id(toPrivateVar(d.name)), Err()).Op(":="). + Qual(d.packageName(), "NewFromDiscovery"). + Call(Qual("github.com/Clever/wag/clientconfig/v9", "WithTracing").Call(Lit(d.name), Id("exporter"))), If(Err().Op("!=").Nil()).Block( Qual("log", "Fatalf").Call(List(Lit("discovery error: %s"), Err())), ),