github.com/simonferquel/app@v0.6.1-0.20181012141724-68b7cccf26ac/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  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/docker/app/internal"
    15  	"github.com/docker/app/internal/compose"
    16  	"github.com/docker/app/internal/yaml"
    17  	"github.com/docker/app/loader"
    18  	"github.com/docker/app/render"
    19  	"github.com/docker/app/types"
    20  	"github.com/docker/app/types/metadata"
    21  	composeloader "github.com/docker/cli/cli/compose/loader"
    22  	"github.com/docker/cli/cli/compose/schema"
    23  	"github.com/docker/cli/opts"
    24  	"github.com/pkg/errors"
    25  	log "github.com/sirupsen/logrus"
    26  )
    27  
    28  func prependToFile(filename, text string) error {
    29  	content, _ := ioutil.ReadFile(filename)
    30  	content = append([]byte(text), content...)
    31  	return ioutil.WriteFile(filename, content, 0644)
    32  }
    33  
    34  // Init is the entrypoint initialization function.
    35  // It generates a new application package based on the provided parameters.
    36  func Init(name string, composeFile string, description string, maintainers []string, singleFile bool) error {
    37  	if err := internal.ValidateAppName(name); err != nil {
    38  		return err
    39  	}
    40  	dirName := internal.DirNameFromAppName(name)
    41  	if err := os.Mkdir(dirName, 0755); err != nil {
    42  		return errors.Wrap(err, "failed to create application directory")
    43  	}
    44  	var err error
    45  	defer func() {
    46  		if err != nil {
    47  			os.RemoveAll(dirName)
    48  		}
    49  	}()
    50  	if err = writeMetadataFile(name, dirName, description, maintainers); err != nil {
    51  		return err
    52  	}
    53  
    54  	if composeFile == "" {
    55  		if _, err := os.Stat(internal.ComposeFileName); err == nil {
    56  			composeFile = internal.ComposeFileName
    57  		}
    58  	}
    59  	if composeFile == "" {
    60  		err = initFromScratch(name)
    61  	} else {
    62  		err = initFromComposeFile(name, composeFile)
    63  	}
    64  	if err != nil {
    65  		return err
    66  	}
    67  	if !singleFile {
    68  		return nil
    69  	}
    70  	// Merge as a single file
    71  	// Add some helfpful comments to distinguish the sections
    72  	if err := prependToFile(filepath.Join(dirName, internal.ComposeFileName), "# This section contains the Compose file that describes your application services.\n"); err != nil {
    73  		return err
    74  	}
    75  	if err := prependToFile(filepath.Join(dirName, internal.SettingsFileName), "# This section contains the default values for your application settings.\n"); err != nil {
    76  		return err
    77  	}
    78  	if err := prependToFile(filepath.Join(dirName, internal.MetadataFileName), "# This section contains your application metadata.\n"); err != nil {
    79  		return err
    80  	}
    81  
    82  	temp := "_temp_dockerapp__.dockerapp"
    83  	err = os.Rename(dirName, temp)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	defer os.RemoveAll(temp)
    88  	var target io.Writer
    89  	target, err = os.Create(dirName)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	defer target.(io.WriteCloser).Close()
    94  	app, err := loader.LoadFromDirectory(temp)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	return Merge(app, target)
    99  }
   100  
   101  func initFromScratch(name string) error {
   102  	log.Debug("init from scratch")
   103  	composeData, err := composeFileFromScratch()
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	dirName := internal.DirNameFromAppName(name)
   109  
   110  	if err := ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeData, 0644); err != nil {
   111  		return err
   112  	}
   113  	return ioutil.WriteFile(filepath.Join(dirName, internal.SettingsFileName), []byte{'\n'}, 0644)
   114  }
   115  
   116  func checkComposeFileVersion(compose map[string]interface{}) error {
   117  	version, ok := compose["version"]
   118  	if !ok {
   119  		return fmt.Errorf("unsupported Compose file version: version 1 is too low")
   120  	}
   121  	return schema.Validate(compose, fmt.Sprintf("%v", version))
   122  }
   123  
   124  func initFromComposeFile(name string, composeFile string) error {
   125  	log.Debug("init from compose")
   126  
   127  	dirName := internal.DirNameFromAppName(name)
   128  
   129  	composeRaw, err := ioutil.ReadFile(composeFile)
   130  	if err != nil {
   131  		return errors.Wrap(err, "failed to read compose file")
   132  	}
   133  	cfgMap, err := composeloader.ParseYAML(composeRaw)
   134  	if err != nil {
   135  		return errors.Wrap(err, "failed to parse compose file")
   136  	}
   137  	if err := checkComposeFileVersion(cfgMap); err != nil {
   138  		return err
   139  	}
   140  	settings := make(map[string]string)
   141  	envs, err := opts.ParseEnvFile(filepath.Join(filepath.Dir(composeFile), ".env"))
   142  	if err == nil {
   143  		for _, v := range envs {
   144  			kv := strings.SplitN(v, "=", 2)
   145  			if len(kv) == 2 {
   146  				settings[kv[0]] = kv[1]
   147  			}
   148  		}
   149  	}
   150  	vars, err := compose.ExtractVariables(composeRaw, render.Pattern)
   151  	if err != nil {
   152  		return errors.Wrap(err, "failed to parse compose file")
   153  	}
   154  	needsFilling := false
   155  	for k, v := range vars {
   156  		if _, ok := settings[k]; !ok {
   157  			if v != "" {
   158  				settings[k] = v
   159  			} else {
   160  				settings[k] = "FILL ME"
   161  				needsFilling = true
   162  			}
   163  		}
   164  	}
   165  	settingsYAML, err := yaml.Marshal(settings)
   166  	if err != nil {
   167  		return errors.Wrap(err, "failed to marshal settings")
   168  	}
   169  	err = ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeRaw, 0644)
   170  	if err != nil {
   171  		return errors.Wrap(err, "failed to write docker-compose.yml")
   172  	}
   173  	err = ioutil.WriteFile(filepath.Join(dirName, internal.SettingsFileName), settingsYAML, 0644)
   174  	if err != nil {
   175  		return errors.Wrap(err, "failed to write settings.yml")
   176  	}
   177  	if needsFilling {
   178  		fmt.Println("You will need to edit settings.yml to fill in default values.")
   179  	}
   180  	return nil
   181  }
   182  
   183  func composeFileFromScratch() ([]byte, error) {
   184  	fileStruct := types.NewInitialComposeFile()
   185  	return yaml.Marshal(fileStruct)
   186  }
   187  
   188  const metaTemplate = `# Version of the application
   189  version: {{ .Version }}
   190  # Name of the application
   191  name: {{ .Name }}
   192  # A short description of the application
   193  description: {{ .Description }}
   194  # Namespace to use when pushing to a registry. This is typically your Hub username.
   195  {{ if len .Namespace}}namespace: {{ .Namespace }} {{ else }}#namespace: myHubUsername{{ end }}
   196  # List of application maintainers with name and email for each
   197  {{ if len .Maintainers }}maintainers:
   198  {{ range .Maintainers }}  - name: {{ .Name  }}
   199      email: {{ .Email }}
   200  {{ end }}{{ else }}#maintainers:
   201  #  - name: John Doe
   202  #    email: john@doe.com
   203  {{ end }}`
   204  
   205  func writeMetadataFile(name, dirName string, description string, maintainers []string) error {
   206  	meta := newMetadata(name, description, maintainers)
   207  	tmpl, err := template.New("metadata").Parse(metaTemplate)
   208  	if err != nil {
   209  		return errors.Wrap(err, "internal error parsing metadata template")
   210  	}
   211  	resBuf := &bytes.Buffer{}
   212  	if err := tmpl.Execute(resBuf, meta); err != nil {
   213  		return errors.Wrap(err, "error generating metadata")
   214  	}
   215  	return ioutil.WriteFile(filepath.Join(dirName, internal.MetadataFileName), resBuf.Bytes(), 0644)
   216  }
   217  
   218  // parseMaintainersData parses user-provided data through the maintainers flag and returns
   219  // a slice of Maintainer instances
   220  func parseMaintainersData(maintainers []string) []metadata.Maintainer {
   221  	var res []metadata.Maintainer
   222  	for _, m := range maintainers {
   223  		ne := strings.SplitN(m, ":", 2)
   224  		var email string
   225  		if len(ne) > 1 {
   226  			email = ne[1]
   227  		}
   228  		res = append(res, metadata.Maintainer{Name: ne[0], Email: email})
   229  	}
   230  	return res
   231  }
   232  
   233  func newMetadata(name string, description string, maintainers []string) metadata.AppMetadata {
   234  	res := metadata.AppMetadata{
   235  		Version:     "0.1.0",
   236  		Name:        name,
   237  		Description: description,
   238  	}
   239  	if len(maintainers) == 0 {
   240  		userData, _ := user.Current()
   241  		if userData != nil && userData.Username != "" {
   242  			res.Maintainers = []metadata.Maintainer{{Name: userData.Username}}
   243  		}
   244  	} else {
   245  		res.Maintainers = parseMaintainersData(maintainers)
   246  	}
   247  	return res
   248  }