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  }