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 }