同步操作将从 mirrors_back/docker-cli 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16://go:build go1.22package loaderimport ("fmt""path""path/filepath""reflect""sort""strconv""strings""time"interp "github.com/docker/cli/cli/compose/interpolation""github.com/docker/cli/cli/compose/schema""github.com/docker/cli/cli/compose/template""github.com/docker/cli/cli/compose/types""github.com/docker/cli/opts""github.com/docker/docker/api/types/versions""github.com/docker/go-connections/nat"units "github.com/docker/go-units""github.com/go-viper/mapstructure/v2""github.com/google/shlex""github.com/pkg/errors""github.com/sirupsen/logrus"yaml "gopkg.in/yaml.v2")// Options supported by Loadtype Options struct {// Skip schema validationSkipValidation bool// Skip interpolationSkipInterpolation bool// Interpolation optionsInterpolate *interp.Options// Discard 'env_file' entries after resolving to 'environment' sectiondiscardEnvFiles bool}// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to// the `environment` sectionfunc WithDiscardEnvFiles(options *Options) {options.discardEnvFiles = true}// ParseYAML reads the bytes from a file, parses the bytes into a mapping// structure, and returns it.func ParseYAML(source []byte) (map[string]any, error) {var cfg anyif err := yaml.Unmarshal(source, &cfg); err != nil {return nil, err}cfgMap, ok := cfg.(map[any]any)if !ok {return nil, errors.Errorf("top-level object must be a mapping")}converted, err := convertToStringKeysRecursive(cfgMap, "")if err != nil {return nil, err}return converted.(map[string]any), nil}// Load reads a ConfigDetails and returns a fully loaded configurationfunc Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Config, error) {if len(configDetails.ConfigFiles) < 1 {return nil, errors.Errorf("No files specified")}options := &Options{Interpolate: &interp.Options{Substitute: template.Substitute,LookupValue: configDetails.LookupEnv,TypeCastMapping: interpolateTypeCastMapping,},}for _, op := range opt {op(options)}configs := []*types.Config{}var err errorfor _, file := range configDetails.ConfigFiles {configDict := file.Configversion := schema.Version(configDict)if configDetails.Version == "" {configDetails.Version = version}if configDetails.Version != version {return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)}if err := validateForbidden(configDict); err != nil {return nil, err}if !options.SkipInterpolation {configDict, err = interpolateConfig(configDict, *options.Interpolate)if err != nil {return nil, err}}if !options.SkipValidation {if err := schema.Validate(configDict, configDetails.Version); err != nil {return nil, err}}cfg, err := loadSections(configDict, configDetails)if err != nil {return nil, err}cfg.Filename = file.Filenameif options.discardEnvFiles {for i := range cfg.Services {cfg.Services[i].EnvFile = nil}}configs = append(configs, cfg)}return merge(configs)}func validateForbidden(configDict map[string]any) error {servicesDict, ok := configDict["services"].(map[string]any)if !ok {return nil}forbidden := getProperties(servicesDict, types.ForbiddenProperties)if len(forbidden) > 0 {return &ForbiddenPropertiesError{Properties: forbidden}}return nil}func loadSections(config map[string]any, configDetails types.ConfigDetails) (*types.Config, error) {var err errorcfg := types.Config{Version: schema.Version(config),}loaders := []struct {key stringfnc func(config map[string]any) error}{{key: "services",fnc: func(config map[string]any) error {cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)return err},},{key: "networks",fnc: func(config map[string]any) error {cfg.Networks, err = LoadNetworks(config, configDetails.Version)return err},},{key: "volumes",fnc: func(config map[string]any) error {cfg.Volumes, err = LoadVolumes(config, configDetails.Version)return err},},{key: "secrets",fnc: func(config map[string]any) error {cfg.Secrets, err = LoadSecrets(config, configDetails)return err},},{key: "configs",fnc: func(config map[string]any) error {cfg.Configs, err = LoadConfigObjs(config, configDetails)return err},},}for _, loader := range loaders {if err := loader.fnc(getSection(config, loader.key)); err != nil {return nil, err}}cfg.Extras = getExtras(config)return &cfg, nil}func getSection(config map[string]any, key string) map[string]any {section, ok := config[key]if !ok {return make(map[string]any)}return section.(map[string]any)}// GetUnsupportedProperties returns the list of any unsupported properties that are// used in the Compose files.func GetUnsupportedProperties(configDicts ...map[string]any) []string {unsupported := map[string]bool{}for _, configDict := range configDicts {for _, service := range getServices(configDict) {serviceDict := service.(map[string]any)for _, property := range types.UnsupportedProperties {if _, isSet := serviceDict[property]; isSet {unsupported[property] = true}}}}return sortedKeys(unsupported)}func sortedKeys(set map[string]bool) []string {keys := make([]string, 0, len(set))for key := range set {keys = append(keys, key)}sort.Strings(keys)return keys}// GetDeprecatedProperties returns the list of any deprecated properties that// are used in the compose files.func GetDeprecatedProperties(configDicts ...map[string]any) map[string]string {deprecated := map[string]string{}for _, configDict := range configDicts {deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)for key, value := range deprecatedProperties {deprecated[key] = value}}return deprecated}func getProperties(services map[string]any, propertyMap map[string]string) map[string]string {output := map[string]string{}for _, service := range services {if serviceDict, ok := service.(map[string]any); ok {for property, description := range propertyMap {if _, isSet := serviceDict[property]; isSet {output[property] = description}}}}return output}// ForbiddenPropertiesError is returned when there are properties in the Compose// file that are forbidden.type ForbiddenPropertiesError struct {Properties map[string]string}func (e *ForbiddenPropertiesError) Error() string {return "Configuration contains forbidden properties"}func getServices(configDict map[string]any) map[string]any {if services, ok := configDict["services"]; ok {if servicesDict, ok := services.(map[string]any); ok {return servicesDict}}return map[string]any{}}// Transform converts the source into the target struct with compose types transformer// and the specified transformers if any.func Transform(source any, target any, additionalTransformers ...Transformer) error {data := mapstructure.Metadata{}config := &mapstructure.DecoderConfig{DecodeHook: mapstructure.ComposeDecodeHookFunc(createTransformHook(additionalTransformers...),mapstructure.StringToTimeDurationHookFunc()),Result: target,Metadata: &data,}decoder, err := mapstructure.NewDecoder(config)if err != nil {return err}return decoder.Decode(source)}// TransformerFunc defines a function to perform the actual transformationtype TransformerFunc func(any) (any, error)// Transformer defines a map to type transformertype Transformer struct {TypeOf reflect.TypeFunc TransformerFunc}func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {transforms := map[reflect.Type]func(any) (any, error){reflect.TypeOf(types.External{}): transformExternal,reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest,reflect.TypeOf(types.ShellCommand{}): transformShellCommand,reflect.TypeOf(types.StringList{}): transformStringList,reflect.TypeOf(map[string]string{}): transformMapStringString,reflect.TypeOf(types.UlimitsConfig{}): transformUlimits,reflect.TypeOf(types.UnitBytes(0)): transformSize,reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort,reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap,reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap,reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList,reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false),reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),reflect.TypeOf(types.HostsList{}): transformHostsList,reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,reflect.TypeOf(types.Duration(0)): transformStringToDuration,}for _, transformer := range additionalTransformers {transforms[transformer.TypeOf] = transformer.Func}return func(_ reflect.Type, target reflect.Type, data any) (any, error) {transform, ok := transforms[target]if !ok {return data, nil}return transform(data)}}// keys needs to be converted to strings for jsonschemafunc convertToStringKeysRecursive(value any, keyPrefix string) (any, error) {if mapping, ok := value.(map[any]any); ok {dict := make(map[string]any)for key, entry := range mapping {str, ok := key.(string)if !ok {return nil, formatInvalidKeyError(keyPrefix, key)}var newKeyPrefix stringif keyPrefix == "" {newKeyPrefix = str} else {newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)}convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)if err != nil {return nil, err}dict[str] = convertedEntry}return dict, nil}if list, ok := value.([]any); ok {var convertedList []anyfor index, entry := range list {newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)if err != nil {return nil, err}convertedList = append(convertedList, convertedEntry)}return convertedList, nil}return value, nil}func formatInvalidKeyError(keyPrefix string, key any) error {var location stringif keyPrefix == "" {location = "at top level"} else {location = "in " + keyPrefix}return errors.Errorf("non-string key %s: %#v", location, key)}// LoadServices produces a ServiceConfig map from a compose file Dict// the servicesDict is not validated if directly used. Use Load() to enable validationfunc LoadServices(servicesDict map[string]any, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {services := make([]types.ServiceConfig, 0, len(servicesDict))for name, serviceDef := range servicesDict {serviceConfig, err := LoadService(name, serviceDef.(map[string]any), workingDir, lookupEnv)if err != nil {return nil, err}services = append(services, *serviceConfig)}return services, nil}// LoadService produces a single ServiceConfig from a compose file Dict// the serviceDict is not validated if directly used. Use Load() to enable validationfunc LoadService(name string, serviceDict map[string]any, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {serviceConfig := &types.ServiceConfig{}if err := Transform(serviceDict, serviceConfig); err != nil {return nil, err}serviceConfig.Name = nameif err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {return nil, err}if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {return nil, err}serviceConfig.Extras = getExtras(serviceDict)return serviceConfig, nil}func loadExtras(name string, source map[string]any) map[string]any {if dict, ok := source[name].(map[string]any); ok {return getExtras(dict)}return nil}func getExtras(dict map[string]any) map[string]any {extras := map[string]any{}for key, value := range dict {if strings.HasPrefix(key, "x-") {extras[key] = value}}if len(extras) == 0 {return nil}return extras}func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {for k, v := range vars {interpolatedV, ok := lookupEnv(k)if (v == nil || *v == "") && ok {// lookupEnv is prioritized over varsenvironment[k] = &interpolatedV} else {environment[k] = v}}}func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {environment := make(map[string]*string)if len(serviceConfig.EnvFile) > 0 {var envVars []stringfor _, file := range serviceConfig.EnvFile {filePath := absPath(workingDir, file)fileVars, err := opts.ParseEnvFile(filePath)if err != nil {return err}envVars = append(envVars, fileVars...)}updateEnvironment(environment,opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)}updateEnvironment(environment, serviceConfig.Environment, lookupEnv)serviceConfig.Environment = environmentreturn nil}func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {for i, volume := range volumes {if volume.Type != "bind" {continue}if volume.Source == "" {return errors.New(`invalid mount config for type "bind": field Source must not be empty`)}filePath := expandUser(volume.Source, lookupEnv)// Check if source is an absolute path (either Unix or Windows), to// handle a Windows client with a Unix daemon or vice-versa.//// Note that this is not required for Docker for Windows when specifying// a local Windows path, because Docker for Windows translates the Windows// path into a valid path within the VM.if !path.IsAbs(filePath) && !isAbs(filePath) {filePath = absPath(workingDir, filePath)}volume.Source = filePathvolumes[i] = volume}return nil}// TODO: make this more robustfunc expandUser(srcPath string, lookupEnv template.Mapping) string {if strings.HasPrefix(srcPath, "~") {home, ok := lookupEnv("HOME")if !ok {logrus.Warn("cannot expand '~', because the environment lacks HOME")return srcPath}return strings.Replace(srcPath, "~", home, 1)}return srcPath}func transformUlimits(data any) (any, error) {switch value := data.(type) {case int:return types.UlimitsConfig{Single: value}, nilcase map[string]any:ulimit := types.UlimitsConfig{}ulimit.Soft = value["soft"].(int)ulimit.Hard = value["hard"].(int)return ulimit, nildefault:return data, errors.Errorf("invalid type %T for ulimits", value)}}// LoadNetworks produces a NetworkConfig map from a compose file Dict// the source Dict is not validated if directly used. Use Load() to enable validationfunc LoadNetworks(source map[string]any, version string) (map[string]types.NetworkConfig, error) {networks := make(map[string]types.NetworkConfig)err := Transform(source, &networks)if err != nil {return networks, err}for name, network := range networks {if !network.External.External {continue}switch {case network.External.Name != "":if network.Name != "" {return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)}if versions.GreaterThanOrEqualTo(version, "3.5") {logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name)}network.Name = network.External.Namenetwork.External.Name = ""case network.Name == "":network.Name = name}network.Extras = loadExtras(name, source)networks[name] = network}return networks, nil}func externalVolumeError(volume, key string) error {return errors.Errorf("conflicting parameters \"external\" and %q specified for volume %q",key, volume)}// LoadVolumes produces a VolumeConfig map from a compose file Dict// the source Dict is not validated if directly used. Use Load() to enable validationfunc LoadVolumes(source map[string]any, version string) (map[string]types.VolumeConfig, error) {volumes := make(map[string]types.VolumeConfig)if err := Transform(source, &volumes); err != nil {return volumes, err}for name, volume := range volumes {if !volume.External.External {continue}switch {case volume.Driver != "":return nil, externalVolumeError(name, "driver")case len(volume.DriverOpts) > 0:return nil, externalVolumeError(name, "driver_opts")case len(volume.Labels) > 0:return nil, externalVolumeError(name, "labels")case volume.External.Name != "":if volume.Name != "" {return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)}if versions.GreaterThanOrEqualTo(version, "3.4") {logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)}volume.Name = volume.External.Namevolume.External.Name = ""case volume.Name == "":volume.Name = name}volume.Extras = loadExtras(name, source)volumes[name] = volume}return volumes, nil}// LoadSecrets produces a SecretConfig map from a compose file Dict// the source Dict is not validated if directly used. Use Load() to enable validationfunc LoadSecrets(source map[string]any, details types.ConfigDetails) (map[string]types.SecretConfig, error) {secrets := make(map[string]types.SecretConfig)if err := Transform(source, &secrets); err != nil {return secrets, err}for name, secret := range secrets {obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)if err != nil {return nil, err}secretConfig := types.SecretConfig(obj)secretConfig.Extras = loadExtras(name, source)secrets[name] = secretConfig}return secrets, nil}// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict// the source Dict is not validated if directly used. Use Load() to enable validationfunc LoadConfigObjs(source map[string]any, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {configs := make(map[string]types.ConfigObjConfig)if err := Transform(source, &configs); err != nil {return configs, err}for name, config := range configs {obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)if err != nil {return nil, err}configConfig := types.ConfigObjConfig(obj)configConfig.Extras = loadExtras(name, source)configs[name] = configConfig}return configs, nil}func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {// if "external: true"switch {case obj.External.External:// handle deprecated external.nameif obj.External.Name != "" {if obj.Name != "" {return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)}if versions.GreaterThanOrEqualTo(details.Version, "3.5") {logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)}obj.Name = obj.External.Nameobj.External.Name = ""} else if obj.Name == "" {obj.Name = name}// if not "external: true"case obj.Driver != "":if obj.File != "" {return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)}default:obj.File = absPath(details.WorkingDir, obj.File)}return obj, nil}func absPath(workingDir string, filePath string) string {if filepath.IsAbs(filePath) {return filePath}return filepath.Join(workingDir, filePath)}var transformMapStringString TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case map[string]any:return toMapStringString(value, false), nilcase map[string]string:return value, nildefault:return data, errors.Errorf("invalid type %T for map[string]string", value)}}var transformExternal TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case bool:return map[string]any{"external": value}, nilcase map[string]any:return map[string]any{"external": true, "name": value["name"]}, nildefault:return data, errors.Errorf("invalid type %T for external", value)}}var transformServicePort TransformerFunc = func(data any) (any, error) {switch entries := data.(type) {case []any:// We process the list instead of individual items here.// The reason is that one entry might be mapped to multiple ServicePortConfig.// Therefore we take an input of a list and return an output of a list.ports := []any{}for _, entry := range entries {switch value := entry.(type) {case int:v, err := toServicePortConfigs(strconv.Itoa(value))if err != nil {return data, err}ports = append(ports, v...)case string:v, err := toServicePortConfigs(value)if err != nil {return data, err}ports = append(ports, v...)case map[string]any:ports = append(ports, value)default:return data, errors.Errorf("invalid type %T for port", value)}}return ports, nildefault:return data, errors.Errorf("invalid type %T for port", entries)}}var transformStringSourceMap TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case string:return map[string]any{"source": value}, nilcase map[string]any:return data, nildefault:return data, errors.Errorf("invalid type %T for secret", value)}}var transformBuildConfig TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case string:return map[string]any{"context": value}, nilcase map[string]any:return data, nildefault:return data, errors.Errorf("invalid type %T for service build", value)}}var transformServiceVolumeConfig TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case string:return ParseVolume(value)case map[string]any:return data, nildefault:return data, errors.Errorf("invalid type %T for service volume", value)}}var transformServiceNetworkMap TransformerFunc = func(value any) (any, error) {if list, ok := value.([]any); ok {mapValue := map[any]any{}for _, name := range list {mapValue[name] = nil}return mapValue, nil}return value, nil}var transformStringOrNumberList TransformerFunc = func(value any) (any, error) {list := value.([]any)result := make([]string, len(list))for i, item := range list {result[i] = fmt.Sprint(item)}return result, nil}var transformStringList TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case string:return []string{value}, nilcase []any:return value, nildefault:return data, errors.Errorf("invalid type %T for string list", value)}}var transformHostsList TransformerFunc = func(data any) (any, error) {hl := transformListOrMapping(data, ":", false, []string{"=", ":"})// Remove brackets from IP addresses if present (for example "[::1]" -> "::1").result := make([]string, 0, len(hl))for _, hip := range hl {host, ip, _ := strings.Cut(hip, ":")if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {ip = ip[1 : len(ip)-1]}result = append(result, fmt.Sprintf("%s:%s", host, ip))}return result, nil}// transformListOrMapping transforms pairs of strings that may be represented as// a map, or a list of '=' or ':' separated strings, into a list of ':' separated// strings.func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string {switch value := listOrMapping.(type) {case map[string]any:return toStringList(value, sep, allowNil)case []any:result := make([]string, 0, len(value))for _, entry := range value {for i, allowSep := range allowSeps {entry := fmt.Sprint(entry)k, v, ok := strings.Cut(entry, allowSep)if ok {// Entry uses this allowed separator. Add it to the result, using// sep as a separator.result = append(result, fmt.Sprintf("%s%s%s", k, sep, v))break} else if i == len(allowSeps)-1 {// No more separators to try, keep the entry if allowNil.if allowNil {result = append(result, k)}}}}return result}panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))}func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {return func(data any) (any, error) {return transformMappingOrList(data, sep, allowNil), nil}}func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any {switch values := mappingOrList.(type) {case map[string]any:return toMapStringString(values, allowNil)case []any:result := make(map[string]any)for _, v := range values {key, val, hasValue := strings.Cut(v.(string), sep)switch {case !hasValue && allowNil:result[key] = nilcase !hasValue && !allowNil:result[key] = ""default:result[key] = val}}return result}panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))}var transformShellCommand TransformerFunc = func(value any) (any, error) {if str, ok := value.(string); ok {return shlex.Split(str)}return value, nil}var transformHealthCheckTest TransformerFunc = func(data any) (any, error) {switch value := data.(type) {case string:return append([]string{"CMD-SHELL"}, value), nilcase []any:return value, nildefault:return value, errors.Errorf("invalid type %T for healthcheck.test", value)}}var transformSize TransformerFunc = func(value any) (any, error) {switch value := value.(type) {case int:return int64(value), nilcase string:return units.RAMInBytes(value)}panic(errors.Errorf("invalid type for size %T", value))}var transformStringToDuration TransformerFunc = func(value any) (any, error) {switch value := value.(type) {case string:d, err := time.ParseDuration(value)if err != nil {return value, err}return types.Duration(d), nildefault:return value, errors.Errorf("invalid type %T for duration", value)}}func toServicePortConfigs(value string) ([]any, error) {var portConfigs []anyports, portBindings, err := nat.ParsePortSpecs([]string{value})if err != nil {return nil, err}// We need to sort the key of the ports to make sure it is consistentkeys := []string{}for port := range ports {keys = append(keys, string(port))}sort.Strings(keys)for _, key := range keys {// Reuse ConvertPortToPortConfig so that it is consistentportConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)if err != nil {return nil, err}for _, p := range portConfig {portConfigs = append(portConfigs, types.ServicePortConfig{Protocol: string(p.Protocol),Target: p.TargetPort,Published: p.PublishedPort,Mode: string(p.PublishMode),})}}return portConfigs, nil}func toMapStringString(value map[string]any, allowNil bool) map[string]any {output := make(map[string]any)for key, value := range value {output[key] = toString(value, allowNil)}return output}func toString(value any, allowNil bool) any {switch {case value != nil:return fmt.Sprint(value)case allowNil:return nildefault:return ""}}func toStringList(value map[string]any, separator string, allowNil bool) []string {output := []string{}for key, value := range value {if value == nil && !allowNil {continue}output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))}sort.Strings(output)return output}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。