github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/stack/loader/loader.go (about)

     1  package loader
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/command/stack/options"
    13  	"github.com/docker/cli/cli/compose/loader"
    14  	"github.com/docker/cli/cli/compose/schema"
    15  	composetypes "github.com/docker/cli/cli/compose/types"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // LoadComposefile parse the composefile specified in the cli and returns its Config and version.
    20  func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) {
    21  	configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In())
    22  	if err != nil {
    23  		return nil, err
    24  	}
    25  
    26  	dicts := getDictsFrom(configDetails.ConfigFiles)
    27  	config, err := loader.Load(configDetails)
    28  	if err != nil {
    29  		if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
    30  			//nolint:revive // ignore capitalization error; this error is intentionally formatted multi-line
    31  			return nil, errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
    32  				propertyWarnings(fpe.Properties))
    33  		}
    34  
    35  		return nil, err
    36  	}
    37  
    38  	unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
    39  	if len(unsupportedProperties) > 0 {
    40  		fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
    41  			strings.Join(unsupportedProperties, ", "))
    42  	}
    43  
    44  	deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
    45  	if len(deprecatedProperties) > 0 {
    46  		fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
    47  			propertyWarnings(deprecatedProperties))
    48  	}
    49  	return config, nil
    50  }
    51  
    52  func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
    53  	dicts := []map[string]interface{}{}
    54  
    55  	for _, configFile := range configFiles {
    56  		dicts = append(dicts, configFile.Config)
    57  	}
    58  
    59  	return dicts
    60  }
    61  
    62  func propertyWarnings(properties map[string]string) string {
    63  	var msgs []string
    64  	for name, description := range properties {
    65  		msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
    66  	}
    67  	sort.Strings(msgs)
    68  	return strings.Join(msgs, "\n\n")
    69  }
    70  
    71  // GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails
    72  func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
    73  	var details composetypes.ConfigDetails
    74  
    75  	if len(composefiles) == 0 {
    76  		return details, errors.New("Please specify a Compose file (with --compose-file)")
    77  	}
    78  
    79  	if composefiles[0] == "-" && len(composefiles) == 1 {
    80  		workingDir, err := os.Getwd()
    81  		if err != nil {
    82  			return details, err
    83  		}
    84  		details.WorkingDir = workingDir
    85  	} else {
    86  		absPath, err := filepath.Abs(composefiles[0])
    87  		if err != nil {
    88  			return details, err
    89  		}
    90  		details.WorkingDir = filepath.Dir(absPath)
    91  	}
    92  
    93  	var err error
    94  	details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
    95  	if err != nil {
    96  		return details, err
    97  	}
    98  	// Take the first file version (2 files can't have different version)
    99  	details.Version = schema.Version(details.ConfigFiles[0].Config)
   100  	details.Environment, err = buildEnvironment(os.Environ())
   101  	return details, err
   102  }
   103  
   104  func buildEnvironment(env []string) (map[string]string, error) {
   105  	result := make(map[string]string, len(env))
   106  	for _, s := range env {
   107  		k, v, ok := strings.Cut(s, "=")
   108  		if !ok || k == "" {
   109  			return result, errors.Errorf("unexpected environment %q", s)
   110  		}
   111  		// value may be set, but empty if "s" is like "K=", not "K".
   112  		result[k] = v
   113  	}
   114  	return result, nil
   115  }
   116  
   117  func loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
   118  	var configFiles []composetypes.ConfigFile
   119  
   120  	for _, filename := range filenames {
   121  		configFile, err := loadConfigFile(filename, stdin)
   122  		if err != nil {
   123  			return configFiles, err
   124  		}
   125  		configFiles = append(configFiles, *configFile)
   126  	}
   127  
   128  	return configFiles, nil
   129  }
   130  
   131  func loadConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
   132  	var bytes []byte
   133  	var err error
   134  
   135  	if filename == "-" {
   136  		bytes, err = io.ReadAll(stdin)
   137  	} else {
   138  		bytes, err = os.ReadFile(filename)
   139  	}
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	config, err := loader.ParseYAML(bytes)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	return &composetypes.ConfigFile{
   150  		Filename: filename,
   151  		Config:   config,
   152  	}, nil
   153  }