github.com/ijc/docker-app@v0.6.1-0.20181012090447-c7ca8bc483fb/render/render.go (about) 1 package render 2 3 import ( 4 "fmt" 5 "os" 6 "regexp" 7 "strings" 8 9 "github.com/docker/app/internal/compose" 10 "github.com/docker/app/internal/renderer" 11 "github.com/docker/app/internal/slices" 12 "github.com/docker/app/types" 13 "github.com/docker/app/types/settings" 14 "github.com/docker/cli/cli/compose/loader" 15 composetemplate "github.com/docker/cli/cli/compose/template" 16 composetypes "github.com/docker/cli/cli/compose/types" 17 "github.com/pkg/errors" 18 19 // Register gotemplate renderer 20 _ "github.com/docker/app/internal/renderer/gotemplate" 21 // Register mustache renderer 22 _ "github.com/docker/app/internal/renderer/mustache" 23 // Register yatee renderer 24 _ "github.com/docker/app/internal/renderer/yatee" 25 26 // Register json formatter 27 _ "github.com/docker/app/internal/formatter/json" 28 // Register yaml formatter 29 _ "github.com/docker/app/internal/formatter/yaml" 30 ) 31 32 var ( 33 delimiter = "\\$" 34 substitution = "[_a-z][._a-z0-9]*(?::?[-?][^}]*)?" 35 36 patternString = fmt.Sprintf( 37 "%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))", 38 delimiter, delimiter, substitution, substitution, 39 ) 40 41 // Pattern is the variable regexp pattern used to interpolate or extract variables when rendering 42 Pattern = regexp.MustCompile(patternString) 43 ) 44 45 // Render renders the Compose file for this app, merging in settings files, other compose files, and env 46 // appname string, composeFiles []string, settingsFiles []string 47 func Render(app *types.App, env map[string]string) (*composetypes.Config, error) { 48 // prepend the app settings to the argument settings 49 // load the settings into a struct 50 fileSettings := app.Settings() 51 // inject our metadata 52 metaPrefixed, err := settings.Load(app.MetadataRaw(), settings.WithPrefix("app")) 53 if err != nil { 54 return nil, err 55 } 56 envSettings, err := settings.FromFlatten(env) 57 if err != nil { 58 return nil, err 59 } 60 allSettings, err := settings.Merge(fileSettings, metaPrefixed, envSettings) 61 if err != nil { 62 return nil, errors.Wrap(err, "failed to merge settings") 63 } 64 // prepend our app compose file to the list 65 renderers := renderer.Drivers() 66 if r, ok := os.LookupEnv("DOCKERAPP_RENDERERS"); ok { 67 rl := strings.Split(r, ",") 68 for _, r := range rl { 69 if !slices.ContainsString(renderer.Drivers(), r) { 70 return nil, fmt.Errorf("renderer '%s' not found", r) 71 } 72 } 73 renderers = rl 74 } 75 configFiles, err := compose.Load(app.Composes(), func(data string) (string, error) { 76 return renderer.Apply(data, allSettings, renderers...) 77 }) 78 if err != nil { 79 return nil, errors.Wrap(err, "failed to load composefiles") 80 } 81 return render(configFiles, allSettings.Flatten()) 82 } 83 84 func render(configFiles []composetypes.ConfigFile, finalEnv map[string]string) (*composetypes.Config, error) { 85 rendered, err := loader.Load(composetypes.ConfigDetails{ 86 WorkingDir: ".", 87 ConfigFiles: configFiles, 88 Environment: finalEnv, 89 }, func(opts *loader.Options) { 90 opts.Interpolate.Substitute = substitute 91 }) 92 if err != nil { 93 return nil, errors.Wrap(err, "failed to load Compose file") 94 } 95 if err := processEnabled(rendered); err != nil { 96 return nil, err 97 } 98 return rendered, nil 99 } 100 101 func substitute(template string, mapping composetemplate.Mapping) (string, error) { 102 return composetemplate.SubstituteWith(template, mapping, Pattern, errorIfMissing) 103 } 104 105 func errorIfMissing(substitution string, mapping composetemplate.Mapping) (string, bool, error) { 106 value, found := mapping(substitution) 107 if !found { 108 return "", true, &composetemplate.InvalidTemplateError{ 109 Template: "required variable " + substitution + " is missing a value", 110 } 111 } 112 return value, true, nil 113 } 114 115 func processEnabled(config *composetypes.Config) error { 116 services := []composetypes.ServiceConfig{} 117 for _, service := range config.Services { 118 if service.Extras != nil { 119 if xEnabled, ok := service.Extras["x-enabled"]; ok { 120 enabled, err := isEnabled(xEnabled) 121 if err != nil { 122 return err 123 } 124 if !enabled { 125 continue 126 } 127 } 128 } 129 services = append(services, service) 130 } 131 config.Services = services 132 return nil 133 } 134 135 func isEnabled(e interface{}) (bool, error) { 136 switch v := e.(type) { 137 case string: 138 v = strings.ToLower(strings.TrimSpace(v)) 139 switch { 140 case v == "1", v == "true": 141 return true, nil 142 case v == "", v == "0", v == "false": 143 return false, nil 144 case strings.HasPrefix(v, "!"): 145 nv, err := isEnabled(v[1:]) 146 if err != nil { 147 return false, err 148 } 149 return !nv, nil 150 default: 151 return false, errors.Errorf("%s is not a valid value for x-enabled", e) 152 } 153 case bool: 154 return v, nil 155 } 156 return false, errors.Errorf("invalid type (%T) for x-enabled", e) 157 }