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 }