github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/internal/packager/init.go (about)

     1  package packager
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/user"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/docker/app/internal"
    16  	"github.com/docker/app/internal/compose"
    17  	"github.com/docker/app/internal/validator"
    18  	"github.com/docker/app/internal/yaml"
    19  	"github.com/docker/app/types"
    20  	"github.com/docker/app/types/metadata"
    21  	"github.com/docker/app/types/parameters"
    22  	composeloader "github.com/docker/cli/cli/compose/loader"
    23  	"github.com/docker/cli/cli/compose/schema"
    24  	"github.com/docker/cli/opts"
    25  	"github.com/pkg/errors"
    26  	"github.com/sirupsen/logrus"
    27  )
    28  
    29  // Init is the entrypoint initialization function.
    30  // It generates a new application definition based on the provided parameters
    31  // and returns the path to the created application definition.
    32  func Init(errWriter io.Writer, name string, composeFile string) (string, error) {
    33  	if err := internal.ValidateAppName(name); err != nil {
    34  		return "", err
    35  	}
    36  	dirName := internal.DirNameFromAppName(name)
    37  	if err := os.Mkdir(dirName, 0755); err != nil {
    38  		return "", errors.Wrap(err, "failed to create application directory")
    39  	}
    40  	var err error
    41  	defer func() {
    42  		if err != nil {
    43  			os.RemoveAll(dirName)
    44  		}
    45  	}()
    46  	if err = writeMetadataFile(name, dirName); err != nil {
    47  		return "", err
    48  	}
    49  
    50  	if composeFile == "" {
    51  		err = initFromScratch(name)
    52  	} else {
    53  		v := validator.NewValidatorWithDefaults()
    54  		err = v.Validate(composeFile)
    55  		if err != nil {
    56  			return "", err
    57  		}
    58  		err = initFromComposeFile(errWriter, name, composeFile)
    59  	}
    60  	if err != nil {
    61  		return "", err
    62  	}
    63  	return dirName, nil
    64  }
    65  
    66  func initFromScratch(name string) error {
    67  	logrus.Debug("Initializing from scratch")
    68  	composeData, err := composeFileFromScratch()
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	dirName := internal.DirNameFromAppName(name)
    74  
    75  	if err := ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeData, 0644); err != nil {
    76  		return err
    77  	}
    78  	return ioutil.WriteFile(filepath.Join(dirName, internal.ParametersFileName), []byte{'\n'}, 0644)
    79  }
    80  
    81  func checkComposeFileVersion(compose map[string]interface{}) error {
    82  	version, ok := compose["version"]
    83  	if !ok {
    84  		return fmt.Errorf("unsupported Compose file version: version 1 is too low")
    85  	}
    86  	return schema.Validate(compose, fmt.Sprintf("%v", version))
    87  }
    88  
    89  func getEnvFiles(svcName string, envFileEntry interface{}) ([]string, error) {
    90  	var envFiles []string
    91  	switch envFileEntry := envFileEntry.(type) {
    92  	case string:
    93  		envFiles = append(envFiles, envFileEntry)
    94  	case []interface{}:
    95  		for _, env := range envFileEntry {
    96  			envFiles = append(envFiles, env.(string))
    97  		}
    98  	default:
    99  		return nil, fmt.Errorf("unknown entries in 'env_file' for service %s -> %v",
   100  			svcName, envFileEntry)
   101  	}
   102  	return envFiles, nil
   103  }
   104  
   105  func checkEnvFiles(errWriter io.Writer, appName string, cfgMap map[string]interface{}) error {
   106  	services := cfgMap["services"]
   107  	servicesMap, ok := services.(map[string]interface{})
   108  	if !ok {
   109  		return fmt.Errorf("invalid Compose file")
   110  	}
   111  	for svcName, svc := range servicesMap {
   112  		svcContent, ok := svc.(map[string]interface{})
   113  		if !ok {
   114  			return fmt.Errorf("invalid service %q", svcName)
   115  		}
   116  		envFileEntry, ok := svcContent["env_file"]
   117  		if !ok {
   118  			continue
   119  		}
   120  		envFiles, err := getEnvFiles(svcName, envFileEntry)
   121  		if err != nil {
   122  			return errors.Wrap(err, "invalid Compose file")
   123  		}
   124  		for _, envFilePath := range envFiles {
   125  			fmt.Fprintf(errWriter,
   126  				"%s.env_file %q will not be copied into %s.dockerapp. "+
   127  					"Please copy it manually and update the path accordingly in the compose file.\n",
   128  				svcName, envFilePath, appName)
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  func getParamsFromDefaultEnvFile(composeFile string, composeRaw []byte) (map[string]string, bool, error) {
   135  	params := make(map[string]string)
   136  	envs, err := opts.ParseEnvFile(filepath.Join(filepath.Dir(composeFile), ".env"))
   137  	if err == nil {
   138  		for _, v := range envs {
   139  			kv := strings.SplitN(v, "=", 2)
   140  			if len(kv) == 2 {
   141  				params[kv[0]] = kv[1]
   142  			}
   143  		}
   144  	}
   145  	vars, err := compose.ExtractVariables(composeRaw, compose.ExtrapolationPattern)
   146  	if err != nil {
   147  		return nil, false, errors.Wrap(err, "failed to parse compose file")
   148  	}
   149  	needsFilling := false
   150  	for k, v := range vars {
   151  		if _, ok := params[k]; !ok {
   152  			if v != "" {
   153  				params[k] = v
   154  			} else {
   155  				params[k] = "FILL ME"
   156  				needsFilling = true
   157  			}
   158  		}
   159  	}
   160  	return params, needsFilling, nil
   161  }
   162  
   163  func initFromComposeFile(errWriter io.Writer, name string, composeFile string) error {
   164  	logrus.Debugf("Initializing from compose file %s", composeFile)
   165  
   166  	dirName := internal.DirNameFromAppName(name)
   167  
   168  	composeRaw, err := ioutil.ReadFile(composeFile)
   169  	if err != nil {
   170  		return errors.Wrapf(err, "failed to read compose file %q", composeFile)
   171  	}
   172  	cfgMap, err := composeloader.ParseYAML(composeRaw)
   173  	if err != nil {
   174  		return errors.Wrap(err, "failed to parse compose file")
   175  	}
   176  	if err := checkComposeFileVersion(cfgMap); err != nil {
   177  		return err
   178  	}
   179  	if err := checkEnvFiles(errWriter, name, cfgMap); err != nil {
   180  		return err
   181  	}
   182  	params, needsFilling, err := getParamsFromDefaultEnvFile(composeFile, composeRaw)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	expandedParams, err := parameters.FromFlatten(params)
   187  	if err != nil {
   188  		return errors.Wrap(err, "failed to expand parameters")
   189  	}
   190  	parametersYAML, err := yaml.Marshal(expandedParams)
   191  	if err != nil {
   192  		return errors.Wrap(err, "failed to marshal parameters")
   193  	}
   194  	// remove parameter default values from compose before saving
   195  	composeRaw = removeDefaultValuesFromCompose(composeRaw)
   196  	err = ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeRaw, 0644)
   197  	if err != nil {
   198  		return errors.Wrap(err, "failed to write docker-compose.yml")
   199  	}
   200  	err = ioutil.WriteFile(filepath.Join(dirName, internal.ParametersFileName), parametersYAML, 0644)
   201  	if err != nil {
   202  		return errors.Wrap(err, "failed to write parameters.yml")
   203  	}
   204  	if needsFilling {
   205  		fmt.Fprintln(os.Stderr, "You will need to edit parameters.yml to fill in default values.")
   206  	}
   207  	return nil
   208  }
   209  
   210  func removeDefaultValuesFromCompose(compose []byte) []byte {
   211  	// find variable names followed by default values/error messages with ':-', '-', ':?' and '?' as separators.
   212  	rePattern := regexp.MustCompile(`\$\{[a-zA-Z_]+[a-zA-Z0-9_.]*((:-)|(\-)|(:\?)|(\?))(.*)\}`)
   213  	matches := rePattern.FindAllSubmatch(compose, -1)
   214  	//remove default value from compose content
   215  	for _, groups := range matches {
   216  		variable := groups[0]
   217  		separator := groups[1]
   218  		variableName := bytes.SplitN(variable, separator, 2)[0]
   219  		compose = bytes.ReplaceAll(compose, variable, []byte(fmt.Sprintf("%s}", variableName)))
   220  	}
   221  	return compose
   222  }
   223  
   224  func composeFileFromScratch() ([]byte, error) {
   225  	fileStruct := types.NewInitialComposeFile()
   226  	return yaml.Marshal(fileStruct)
   227  }
   228  
   229  const metaTemplate = `# Version of the application
   230  version: {{ .Version }}
   231  # Name of the application
   232  name: {{ .Name }}
   233  # A short description of the application
   234  description: {{ .Description }}
   235  # List of application maintainers with name and email for each
   236  {{ if len .Maintainers }}maintainers:
   237  {{ range .Maintainers }}  - name: {{ .Name  }}
   238      email: {{ .Email }}
   239  {{ end }}{{ else }}#maintainers:
   240  #  - name: John Doe
   241  #    email: john@doe.com
   242  {{ end }}`
   243  
   244  func writeMetadataFile(name, dirName string) error {
   245  	meta := newMetadata(name)
   246  	tmpl, err := template.New("metadata").Parse(metaTemplate)
   247  	if err != nil {
   248  		return errors.Wrap(err, "internal error parsing metadata template")
   249  	}
   250  	resBuf := &bytes.Buffer{}
   251  	if err := tmpl.Execute(resBuf, meta); err != nil {
   252  		return errors.Wrap(err, "error generating metadata")
   253  	}
   254  	return ioutil.WriteFile(filepath.Join(dirName, internal.MetadataFileName), resBuf.Bytes(), 0644)
   255  }
   256  
   257  func newMetadata(name string) metadata.AppMetadata {
   258  	res := metadata.AppMetadata{
   259  		Version: "0.1.0",
   260  		Name:    name,
   261  	}
   262  	userData, _ := user.Current()
   263  	if userData != nil && userData.Username != "" {
   264  		res.Maintainers = []metadata.Maintainer{{Name: userData.Username}}
   265  	}
   266  	return res
   267  }