github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/stack/loader/loader.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package loader 5 6 import ( 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "runtime" 12 "sort" 13 "strings" 14 15 "github.com/khulnasoft/cli/cli/command" 16 "github.com/khulnasoft/cli/cli/command/stack/options" 17 "github.com/khulnasoft/cli/cli/compose/loader" 18 "github.com/khulnasoft/cli/cli/compose/schema" 19 composetypes "github.com/khulnasoft/cli/cli/compose/types" 20 "github.com/pkg/errors" 21 ) 22 23 // LoadComposefile parse the composefile specified in the cli and returns its Config and version. 24 func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) { 25 configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In()) 26 if err != nil { 27 return nil, err 28 } 29 30 dicts := getDictsFrom(configDetails.ConfigFiles) 31 config, err := loader.Load(configDetails) 32 if err != nil { 33 if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { 34 // this error is intentionally formatted multi-line 35 return nil, errors.Errorf("Compose file contains unsupported options:\n\n%s\n", propertyWarnings(fpe.Properties)) 36 } 37 38 return nil, err 39 } 40 41 unsupportedProperties := loader.GetUnsupportedProperties(dicts...) 42 if len(unsupportedProperties) > 0 { 43 fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", 44 strings.Join(unsupportedProperties, ", ")) 45 } 46 47 deprecatedProperties := loader.GetDeprecatedProperties(dicts...) 48 if len(deprecatedProperties) > 0 { 49 fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", 50 propertyWarnings(deprecatedProperties)) 51 } 52 return config, nil 53 } 54 55 func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]any { 56 dicts := []map[string]any{} 57 58 for _, configFile := range configFiles { 59 dicts = append(dicts, configFile.Config) 60 } 61 62 return dicts 63 } 64 65 func propertyWarnings(properties map[string]string) string { 66 msgs := make([]string, 0, len(properties)) 67 for name, description := range properties { 68 msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) 69 } 70 sort.Strings(msgs) 71 return strings.Join(msgs, "\n\n") 72 } 73 74 // GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails 75 func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { 76 var details composetypes.ConfigDetails 77 78 if len(composefiles) == 0 { 79 return details, errors.New("Please specify a Compose file (with --compose-file)") 80 } 81 82 if composefiles[0] == "-" && len(composefiles) == 1 { 83 workingDir, err := os.Getwd() 84 if err != nil { 85 return details, err 86 } 87 details.WorkingDir = workingDir 88 } else { 89 absPath, err := filepath.Abs(composefiles[0]) 90 if err != nil { 91 return details, err 92 } 93 details.WorkingDir = filepath.Dir(absPath) 94 } 95 96 var err error 97 details.ConfigFiles, err = loadConfigFiles(composefiles, stdin) 98 if err != nil { 99 return details, err 100 } 101 // Take the first file version (2 files can't have different version) 102 details.Version = schema.Version(details.ConfigFiles[0].Config) 103 details.Environment, err = buildEnvironment(os.Environ()) 104 return details, err 105 } 106 107 func buildEnvironment(env []string) (map[string]string, error) { 108 result := make(map[string]string, len(env)) 109 for _, s := range env { 110 if runtime.GOOS == "windows" && len(s) > 0 { 111 // cmd.exe can have special environment variables which names start with "=". 112 // They are only there for MS-DOS compatibility and we should ignore them. 113 // See TestBuildEnvironment for examples. 114 // 115 // https://ss64.com/nt/syntax-variables.html 116 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133 117 // https://github.com/khulnasoft/cli/issues/4078 118 if s[0] == '=' { 119 continue 120 } 121 } 122 123 k, v, ok := strings.Cut(s, "=") 124 if !ok || k == "" { 125 return result, errors.Errorf("unexpected environment variable '%s'", s) 126 } 127 // value may be set, but empty if "s" is like "K=", not "K". 128 result[k] = v 129 } 130 return result, nil 131 } 132 133 func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) { 134 configFiles := make([]composetypes.ConfigFile, 0, len(filenames)) 135 136 for _, filename := range filenames { 137 configFile, err := loadConfigFile(filename, stdin) 138 if err != nil { 139 return configFiles, err 140 } 141 configFiles = append(configFiles, *configFile) 142 } 143 144 return configFiles, nil 145 } 146 147 func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) { 148 var bytes []byte 149 var err error 150 151 if filename == "-" { 152 bytes, err = io.ReadAll(stdin) 153 } else { 154 bytes, err = os.ReadFile(filename) 155 } 156 if err != nil { 157 return nil, err 158 } 159 160 config, err := loader.ParseYAML(bytes) 161 if err != nil { 162 return nil, err 163 } 164 165 return &composetypes.ConfigFile{ 166 Filename: filename, 167 Config: config, 168 }, nil 169 }