tools/lambda-compat/cmd/lambda-build/main.go (314 lines of code) (raw):

package main /* Copyright 2022 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import ( "embed" "encoding/json" "errors" "flag" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "strings" "text/template" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" ) type SamTemplate struct { TemplateFormatVersion string `yaml:"AWSTemplateFormatVersion"` Transform string `yaml:"Transform"` Resources map[string]*SamResource `yaml:"Resources"` } type SamResource struct { Type string `yaml:"Type"` Properties map[string]interface{} `yaml:"Properties"` } type arrayFlags []string var templateDir string = "templates/" var generateDockerfile bool = true var dockerfilePath string = "Dockerfile" var generateTerraform bool = true var terraformPath string = "./" var projectId string = "" var containerRegistry string = "" var containerTag string = "latest" var additionalVars arrayFlags var additionalVarsJson arrayFlags var targetDir string = "." //go:embed templates/*.tpl var fs embed.FS func (i *arrayFlags) String() string { // change this, this is just can example to satisfy the interface return fmt.Sprintf("%v", *i) } func (i *arrayFlags) Set(value string) error { *i = append(*i, strings.TrimSpace(value)) return nil } func (r SamResource) getStringProperty(properties map[string]interface{}, key string, mustHave bool) string { if _, ok := properties[key]; ok { return properties[key].(string) } else { if mustHave { log.Fatal().Str("property", key).Msg("Missing required property") } } return "" } func (r SamResource) getInt64Property(properties map[string]interface{}, key string, mustHave bool) int64 { if _, ok := properties[key]; ok { return properties[key].(int64) } else { if mustHave { log.Fatal().Str("property", key).Msg("Missing required property") } } return 0 } func makeMap(args ...string) map[string]string { ret := make(map[string]string, len(args)/2) for i := 0; i < len(args); i++ { ret[args[i]] = args[i+1] i += 1 } return ret } func addToMap(add map[string]string, key string, val string) map[string]string { add[key] = val return add } func getTemplateFunctions() *template.FuncMap { funcMap := template.FuncMap{ "MakeMap": makeMap, "AddToMap": addToMap, "HasSuffix": strings.HasSuffix, "ToLower": strings.ToLower, } return &funcMap } func ensureDirectory(dir string) { dirStat, err := os.Stat(dir) if os.IsNotExist(err) { log.Info().Str("directory", dir).Msg("Creating subdirectory") err = os.MkdirAll(dir, 0770) if err != nil { log.Fatal().Err(err).Str("directory", dir).Msg("Failed to create subdirectory") } } else if !dirStat.IsDir() { log.Fatal().Err(err).Str("directory", dir).Msg("Subdirectory existing and is not a directory") } } func (resource SamResource) getTemplateVariables(name string) *map[string]interface{} { handler := resource.getStringProperty(resource.Properties, "Handler", true) runtime := resource.getStringProperty(resource.Properties, "Runtime", true) codeURI := resource.getStringProperty(resource.Properties, "CodeUri", true) if !strings.HasSuffix(codeURI, "/") { codeURI = fmt.Sprintf("%s/", codeURI) } packageType := resource.getStringProperty(resource.Properties, "PackageType", false) if packageType == "" { packageType = "Zip" } templateVars := map[string]interface{}{ "Name": name, "Runtime": runtime, "CodeUri": codeURI, "Handler": handler, "PackageType": packageType, "Registry": containerRegistry, "ProjectId": projectId, "Tag": containerTag, "Environment": map[string]interface{}{ "Variables": map[string]interface{}{}, }, } if _, ok := resource.Properties["Environment"]; ok { environment := resource.Properties["Environment"].(map[string]interface{}) if _, ok = environment["Variables"]; ok { (templateVars["Environment"].(map[string]interface{}))["Variables"] = environment["Variables"] } } for _, tVar := range additionalVars { s := strings.SplitN(tVar, "=", 2) templateVars[s[0]] = s[1] } for _, tVar := range additionalVarsJson { var v interface{} s := strings.SplitN(tVar, "=", 2) if err := json.Unmarshal([]byte(s[1]), &v); err != nil { log.Fatal().Err(err).Msg("Failed to parse JSON parameter") } templateVars[s[0]] = v } return &templateVars } func (resource SamResource) renderDockerfile(name string, dockerfile string) error { templateVars := resource.getTemplateVariables(name) runtimeRE := regexp.MustCompile(`[0-9].*$`) runtimeTemplate := runtimeRE.ReplaceAllString((*templateVars)["Runtime"].(string), "") templateFile := fmt.Sprintf("%s%s.tpl", templateDir, runtimeTemplate) commonTemplateFile := fmt.Sprintf("%scommon.tpl", templateDir) tmpl, err := template.New(fmt.Sprintf("%s.tpl", runtimeTemplate)). Funcs(*getTemplateFunctions()). ParseFiles(commonTemplateFile, templateFile) if err != nil { return err } log.Info().Str("file", dockerfile).Str("template", filepath.Base(templateFile)).Msg("Creating Dockerfile file") f, err := os.OpenFile(dockerfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640) if err != nil { return err } defer f.Close() err = tmpl.Execute(f, templateVars) if err != nil { return err } f.Close() return nil } func (resource SamResource) renderTerraform(name string, dir string) error { terraformTemplateGlob := fmt.Sprintf("%s/terraform_*.tpl", templateDir) terraformTemplates, err := filepath.Glob(terraformTemplateGlob) if err != nil { return err } for _, templateFile := range terraformTemplates { templateBasename := strings.ReplaceAll(filepath.Base(templateFile), "terraform_", "") templateBasename = strings.ReplaceAll(templateBasename, ".tpl", "") tmpl, err := template.New(filepath.Base(templateFile)). Funcs(*getTemplateFunctions()). ParseFiles(templateFile) if err != nil { return err } resultPath := fmt.Sprintf("%s/%s", filepath.Clean(dir), templateBasename) log.Info().Str("file", resultPath).Str("template", filepath.Base(templateFile)).Msg("Creating Terraform file") f, err := os.OpenFile(resultPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640) if err != nil { return err } defer f.Close() err = tmpl.Execute(f, resource.getTemplateVariables(name)) if err != nil { return err } f.Close() } return nil } func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) exe, err := os.Executable() if err != nil { log.Fatal().Err(err).Msg("unable to determine binary location") } templateDir = fmt.Sprintf("%s/templates/", filepath.Dir(exe)) log.Info().Msg("Lambda compatibility tool for Cloud Run") flag.StringVar(&templateDir, "template-dir", templateDir, "location of Dockerfile and Terraform templates") flag.StringVar(&targetDir, "target-dir", targetDir, "directory to create Dockerfile and Terraform files in") flag.BoolVar(&generateDockerfile, "dockerfile", true, "generate Dockerfile") flag.StringVar(&dockerfilePath, "dockerfile-path", dockerfilePath, "path and filename for Dockerfile") flag.BoolVar(&generateTerraform, "terraform", true, "generate Terraform files") flag.StringVar(&terraformPath, "terraform-path", terraformPath, "path for Terraform files") flag.StringVar(&projectId, "project-id", projectId, "GCP project ID") flag.StringVar(&containerRegistry, "registry", containerRegistry, "Container registry to use (eg. docker.pkg.dev/project-id/registry)") flag.StringVar(&containerTag, "tag", containerTag, "Container image tag") flag.Var(&additionalVars, "var", "Additional template variable (key=value)") flag.Var(&additionalVarsJson, "var-json", "Additional template variable in JSON (key={\"foo\":\"bar\"})") flag.Parse() if !strings.HasSuffix(templateDir, "/") { templateDir = templateDir + "/" } if _, err := os.Stat(templateDir + "common.tpl"); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(templateDir + "common.tpl"); errors.Is(err, os.ErrNotExist) { log.Info().Str("directory", templateDir).Msg("Creating template directory") err = os.MkdirAll(templateDir, 0770) if err != nil { log.Fatal().Err(err).Str("directory", templateDir).Msg("Failed to create template directory") } } templates := []string{ "common.tpl", "dotnetcore.tpl", "go.tpl", "java.tpl", "nodejs.tpl", "python.tpl", "ruby.tpl", "terraform_main.tf.tpl", "terraform_outputs.tf.tpl", "terraform_variables.tf.tpl", "terraform_versions.tf.tpl", } log.Info().Str("directory", templateDir).Msg("Writing embedded templates to disk") for _, tpl := range templates { contents, err := fs.ReadFile(fmt.Sprintf("templates/%s", tpl)) if err != nil { log.Fatal().Err(err).Msg("Failed to retrieve embedded template") } err = ioutil.WriteFile(fmt.Sprintf("%s/%s", templateDir, tpl), contents, 0664) if err != nil { log.Fatal().Err(err).Msg("Failed to write embedded template") } } } var containerBuilders []string = []string{ "buildah", "podman", "nerdctl", } var containerBuilder string = "docker" for _, bin := range containerBuilders { _, err := exec.LookPath(bin) if err == nil { containerBuilder = bin break } } if !strings.HasSuffix(containerRegistry, "/") { containerRegistry = strings.TrimSuffix(containerRegistry, "/") } if len(flag.Args()) == 0 { log.Fatal().Msg("Specify at least one template to process") } for _, fileName := range flag.Args() { var template SamTemplate yamlFile, err := ioutil.ReadFile(fileName) if err != nil { log.Fatal().Str("file", fileName).Err(err).Msg("Failed to load configuration file") } err = yaml.Unmarshal(yamlFile, &template) if err != nil { log.Fatal().Str("file", fileName).Err(err).Msg("Failed to decode YAML") } log.Info().Str("file", fileName).Msg("Processing SAM template file") for name, resource := range template.Resources { if resource.Type == "AWS::Serverless::Function" { subdir := filepath.Clean(fmt.Sprintf("%s/%s", filepath.Clean(targetDir), name)) + "/" if generateDockerfile { ensureDirectory(subdir) dockerfile := fmt.Sprintf("%s%s", subdir, dockerfilePath) err := resource.renderDockerfile(name, dockerfile) if err != nil { log.Fatal().Str("file", fileName).Str("resource", name).Err(err).Msg("Failed to render Dockerfile for resource") } if containerRegistry != "" { log.Info().Msg("To build container, run:") log.Info().Msg(fmt.Sprintf(" %s build -t %s/%s:%s -f %s/Dockerfile .", containerBuilder, containerRegistry, strings.ToLower(name), containerTag, name)) } } if generateTerraform { terraformSubdir := fmt.Sprintf("%s%s", subdir, terraformPath) ensureDirectory(terraformSubdir) err := resource.renderTerraform(name, terraformSubdir) if err != nil { log.Fatal().Str("file", fileName).Str("resource", name).Err(err).Msg("Failed to render Terraform for resource") } } } else { log.Debug().Str("type", resource.Type).Msg("Unknown resource type ignored") } } } }