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  }