github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/stack/kubernetes/convert.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"io"
     5  	"io/ioutil"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/docker/cli/cli/compose/loader"
    11  	"github.com/docker/cli/cli/compose/schema"
    12  	composeTypes "github.com/docker/cli/cli/compose/types"
    13  	composetypes "github.com/docker/cli/cli/compose/types"
    14  	latest "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3"
    15  	"github.com/docker/compose-on-kubernetes/api/compose/v1beta1"
    16  	"github.com/docker/compose-on-kubernetes/api/compose/v1beta2"
    17  	"github.com/docker/go-connections/nat"
    18  	"github.com/mitchellh/mapstructure"
    19  	"github.com/pkg/errors"
    20  	yaml "gopkg.in/yaml.v2"
    21  	v1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  )
    24  
    25  const (
    26  	// kubernatesExtraField is an extra field on ServiceConfigs containing kubernetes-specific extensions to compose format
    27  	kubernatesExtraField = "x-kubernetes"
    28  )
    29  
    30  // NewStackConverter returns a converter from types.Config (compose) to the specified
    31  // stack version or error out if the version is not supported or existent.
    32  func NewStackConverter(version string) (StackConverter, error) {
    33  	switch version {
    34  	case "v1beta1":
    35  		return stackV1Beta1Converter{}, nil
    36  	case "v1beta2":
    37  		return stackV1Beta2Converter{}, nil
    38  	case "v1alpha3":
    39  		return stackV1Alpha3Converter{}, nil
    40  	default:
    41  		return nil, errors.Errorf("stack version %s unsupported", version)
    42  	}
    43  }
    44  
    45  // StackConverter converts a compose types.Config to a Stack
    46  type StackConverter interface {
    47  	FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error)
    48  }
    49  
    50  type stackV1Beta1Converter struct{}
    51  
    52  func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
    53  	cfg.Version = v1beta1.MaxComposeVersion
    54  	st, err := fromCompose(stderr, name, cfg, v1beta1Capabilities)
    55  	if err != nil {
    56  		return Stack{}, err
    57  	}
    58  	res, err := yaml.Marshal(cfg)
    59  	if err != nil {
    60  		return Stack{}, err
    61  	}
    62  	// reload the result to check that it produced a valid 3.5 compose file
    63  	resparsedConfig, err := loader.ParseYAML(res)
    64  	if err != nil {
    65  		return Stack{}, err
    66  	}
    67  	if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil {
    68  		return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion)
    69  	}
    70  
    71  	st.ComposeFile = string(res)
    72  	return st, nil
    73  }
    74  
    75  type stackV1Beta2Converter struct{}
    76  
    77  func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
    78  	return fromCompose(stderr, name, cfg, v1beta2Capabilities)
    79  }
    80  
    81  type stackV1Alpha3Converter struct{}
    82  
    83  func (s stackV1Alpha3Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
    84  	return fromCompose(stderr, name, cfg, v1alpha3Capabilities)
    85  }
    86  
    87  func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config, capabilities composeCapabilities) (Stack, error) {
    88  	spec, err := fromComposeConfig(stderr, cfg, capabilities)
    89  	if err != nil {
    90  		return Stack{}, err
    91  	}
    92  	return Stack{
    93  		Name: name,
    94  		Spec: spec,
    95  	}, nil
    96  }
    97  
    98  func loadStackData(composefile string) (*composetypes.Config, error) {
    99  	parsed, err := loader.ParseYAML([]byte(composefile))
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	return loader.Load(composetypes.ConfigDetails{
   104  		ConfigFiles: []composetypes.ConfigFile{
   105  			{
   106  				Config: parsed,
   107  			},
   108  		},
   109  	})
   110  }
   111  
   112  // Conversions from internal stack to different stack compose component versions.
   113  func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
   114  	cfg, err := loadStackData(in.Spec.ComposeFile)
   115  	if err != nil {
   116  		return Stack{}, err
   117  	}
   118  	spec, err := fromComposeConfig(ioutil.Discard, cfg, v1beta1Capabilities)
   119  	if err != nil {
   120  		return Stack{}, err
   121  	}
   122  	return Stack{
   123  		Name:        in.ObjectMeta.Name,
   124  		Namespace:   in.ObjectMeta.Namespace,
   125  		ComposeFile: in.Spec.ComposeFile,
   126  		Spec:        spec,
   127  	}, nil
   128  }
   129  
   130  func stackToV1beta1(s Stack) *v1beta1.Stack {
   131  	return &v1beta1.Stack{
   132  		ObjectMeta: metav1.ObjectMeta{
   133  			Name: s.Name,
   134  		},
   135  		Spec: v1beta1.StackSpec{
   136  			ComposeFile: s.ComposeFile,
   137  		},
   138  	}
   139  }
   140  
   141  func stackFromV1beta2(in *v1beta2.Stack) (Stack, error) {
   142  	var spec *latest.StackSpec
   143  	if in.Spec != nil {
   144  		spec = &latest.StackSpec{}
   145  		if err := latest.Convert_v1beta2_StackSpec_To_v1alpha3_StackSpec(in.Spec, spec, nil); err != nil {
   146  			return Stack{}, err
   147  		}
   148  	}
   149  	return Stack{
   150  		Name:      in.ObjectMeta.Name,
   151  		Namespace: in.ObjectMeta.Namespace,
   152  		Spec:      spec,
   153  	}, nil
   154  }
   155  
   156  func stackToV1beta2(s Stack) (*v1beta2.Stack, error) {
   157  	var spec *v1beta2.StackSpec
   158  	if s.Spec != nil {
   159  		spec = &v1beta2.StackSpec{}
   160  		if err := latest.Convert_v1alpha3_StackSpec_To_v1beta2_StackSpec(s.Spec, spec, nil); err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  	return &v1beta2.Stack{
   165  		ObjectMeta: metav1.ObjectMeta{
   166  			Name: s.Name,
   167  		},
   168  		Spec: spec,
   169  	}, nil
   170  }
   171  
   172  func stackFromV1alpha3(in *latest.Stack) Stack {
   173  	return Stack{
   174  		Name:      in.ObjectMeta.Name,
   175  		Namespace: in.ObjectMeta.Namespace,
   176  		Spec:      in.Spec,
   177  	}
   178  }
   179  
   180  func stackToV1alpha3(s Stack) *latest.Stack {
   181  	return &latest.Stack{
   182  		ObjectMeta: metav1.ObjectMeta{
   183  			Name: s.Name,
   184  		},
   185  		Spec: s.Spec,
   186  	}
   187  }
   188  
   189  func fromComposeConfig(stderr io.Writer, c *composeTypes.Config, capabilities composeCapabilities) (*latest.StackSpec, error) {
   190  	if c == nil {
   191  		return nil, nil
   192  	}
   193  	warnUnsupportedFeatures(stderr, c)
   194  	serviceConfigs := make([]latest.ServiceConfig, len(c.Services))
   195  	for i, s := range c.Services {
   196  		svc, err := fromComposeServiceConfig(s, capabilities)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  		serviceConfigs[i] = svc
   201  	}
   202  	return &latest.StackSpec{
   203  		Services: serviceConfigs,
   204  		Secrets:  fromComposeSecrets(c.Secrets),
   205  		Configs:  fromComposeConfigs(c.Configs),
   206  	}, nil
   207  }
   208  
   209  func fromComposeSecrets(s map[string]composeTypes.SecretConfig) map[string]latest.SecretConfig {
   210  	if s == nil {
   211  		return nil
   212  	}
   213  	m := map[string]latest.SecretConfig{}
   214  	for key, value := range s {
   215  		m[key] = latest.SecretConfig{
   216  			Name: value.Name,
   217  			File: value.File,
   218  			External: latest.External{
   219  				Name:     value.External.Name,
   220  				External: value.External.External,
   221  			},
   222  			Labels: value.Labels,
   223  		}
   224  	}
   225  	return m
   226  }
   227  
   228  func fromComposeConfigs(s map[string]composeTypes.ConfigObjConfig) map[string]latest.ConfigObjConfig {
   229  	if s == nil {
   230  		return nil
   231  	}
   232  	m := map[string]latest.ConfigObjConfig{}
   233  	for key, value := range s {
   234  		m[key] = latest.ConfigObjConfig{
   235  			Name: value.Name,
   236  			File: value.File,
   237  			External: latest.External{
   238  				Name:     value.External.Name,
   239  				External: value.External.External,
   240  			},
   241  			Labels: value.Labels,
   242  		}
   243  	}
   244  	return m
   245  }
   246  
   247  func fromComposeServiceConfig(s composeTypes.ServiceConfig, capabilities composeCapabilities) (latest.ServiceConfig, error) {
   248  	var (
   249  		userID *int64
   250  		err    error
   251  	)
   252  	if s.User != "" {
   253  		numerical, err := strconv.Atoi(s.User)
   254  		if err == nil {
   255  			unixUserID := int64(numerical)
   256  			userID = &unixUserID
   257  		}
   258  	}
   259  	kubeExtra, err := resolveServiceExtra(s)
   260  	if err != nil {
   261  		return latest.ServiceConfig{}, err
   262  	}
   263  	if kubeExtra.PullSecret != "" && !capabilities.hasPullSecrets {
   264  		return latest.ServiceConfig{}, errors.Errorf(`stack API version %s does not support pull secrets (field "x-kubernetes.pull_secret"), please use version v1alpha3 or higher`, capabilities.apiVersion)
   265  	}
   266  	if kubeExtra.PullPolicy != "" && !capabilities.hasPullPolicies {
   267  		return latest.ServiceConfig{}, errors.Errorf(`stack API version %s does not support pull policies (field "x-kubernetes.pull_policy"), please use version v1alpha3 or higher`, capabilities.apiVersion)
   268  	}
   269  
   270  	internalPorts, err := setupIntraStackNetworking(s, kubeExtra, capabilities)
   271  	if err != nil {
   272  		return latest.ServiceConfig{}, err
   273  	}
   274  
   275  	return latest.ServiceConfig{
   276  		Name:    s.Name,
   277  		CapAdd:  s.CapAdd,
   278  		CapDrop: s.CapDrop,
   279  		Command: s.Command,
   280  		Configs: fromComposeServiceConfigs(s.Configs),
   281  		Deploy: latest.DeployConfig{
   282  			Mode:          s.Deploy.Mode,
   283  			Replicas:      s.Deploy.Replicas,
   284  			Labels:        s.Deploy.Labels,
   285  			UpdateConfig:  fromComposeUpdateConfig(s.Deploy.UpdateConfig),
   286  			Resources:     fromComposeResources(s.Deploy.Resources),
   287  			RestartPolicy: fromComposeRestartPolicy(s.Deploy.RestartPolicy),
   288  			Placement:     fromComposePlacement(s.Deploy.Placement),
   289  		},
   290  		Entrypoint:          s.Entrypoint,
   291  		Environment:         s.Environment,
   292  		ExtraHosts:          s.ExtraHosts,
   293  		Hostname:            s.Hostname,
   294  		HealthCheck:         fromComposeHealthcheck(s.HealthCheck),
   295  		Image:               s.Image,
   296  		Ipc:                 s.Ipc,
   297  		Labels:              s.Labels,
   298  		Pid:                 s.Pid,
   299  		Ports:               fromComposePorts(s.Ports),
   300  		Privileged:          s.Privileged,
   301  		ReadOnly:            s.ReadOnly,
   302  		Secrets:             fromComposeServiceSecrets(s.Secrets),
   303  		StdinOpen:           s.StdinOpen,
   304  		StopGracePeriod:     composetypes.ConvertDurationPtr(s.StopGracePeriod),
   305  		Tmpfs:               s.Tmpfs,
   306  		Tty:                 s.Tty,
   307  		User:                userID,
   308  		Volumes:             fromComposeServiceVolumeConfig(s.Volumes),
   309  		WorkingDir:          s.WorkingDir,
   310  		PullSecret:          kubeExtra.PullSecret,
   311  		PullPolicy:          kubeExtra.PullPolicy,
   312  		InternalServiceType: kubeExtra.InternalServiceType,
   313  		InternalPorts:       internalPorts,
   314  	}, nil
   315  }
   316  
   317  func setupIntraStackNetworking(s composeTypes.ServiceConfig, kubeExtra kubernetesExtra, capabilities composeCapabilities) ([]latest.InternalPort, error) {
   318  	if kubeExtra.InternalServiceType != latest.InternalServiceTypeAuto && !capabilities.hasIntraStackLoadBalancing {
   319  		return nil,
   320  			errors.Errorf(`stack API version %s does not support intra-stack load balancing (field "x-kubernetes.internal_service_type"), please use version v1alpha3 or higher`,
   321  				capabilities.apiVersion)
   322  	}
   323  	if !capabilities.hasIntraStackLoadBalancing {
   324  		return nil, nil
   325  	}
   326  	if err := validateInternalServiceType(kubeExtra.InternalServiceType); err != nil {
   327  		return nil, err
   328  	}
   329  	internalPorts, err := toInternalPorts(s.Expose)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	return internalPorts, nil
   334  }
   335  
   336  func validateInternalServiceType(internalServiceType latest.InternalServiceType) error {
   337  	switch internalServiceType {
   338  	case latest.InternalServiceTypeAuto, latest.InternalServiceTypeClusterIP, latest.InternalServiceTypeHeadless:
   339  	default:
   340  		return errors.Errorf(`invalid value %q for field "x-kubernetes.internal_service_type", valid values are %q or %q`, internalServiceType,
   341  			latest.InternalServiceTypeClusterIP,
   342  			latest.InternalServiceTypeHeadless)
   343  	}
   344  	return nil
   345  }
   346  
   347  func toInternalPorts(expose []string) ([]latest.InternalPort, error) {
   348  	var internalPorts []latest.InternalPort
   349  	for _, sourcePort := range expose {
   350  		proto, port := nat.SplitProtoPort(sourcePort)
   351  		start, end, err := nat.ParsePortRange(port)
   352  		if err != nil {
   353  			return nil, errors.Errorf("invalid format for expose: %q, error: %s", sourcePort, err)
   354  		}
   355  		for i := start; i <= end; i++ {
   356  			k8sProto := v1.Protocol(strings.ToUpper(proto))
   357  			switch k8sProto {
   358  			case v1.ProtocolSCTP, v1.ProtocolTCP, v1.ProtocolUDP:
   359  			default:
   360  				return nil, errors.Errorf("invalid protocol for expose: %q, supported values are %q, %q and %q", sourcePort, v1.ProtocolSCTP, v1.ProtocolTCP, v1.ProtocolUDP)
   361  			}
   362  			internalPorts = append(internalPorts, latest.InternalPort{
   363  				Port:     int32(i),
   364  				Protocol: k8sProto,
   365  			})
   366  		}
   367  	}
   368  	return internalPorts, nil
   369  }
   370  
   371  func resolveServiceExtra(s composeTypes.ServiceConfig) (kubernetesExtra, error) {
   372  	if iface, ok := s.Extras[kubernatesExtraField]; ok {
   373  		var result kubernetesExtra
   374  		if err := mapstructure.Decode(iface, &result); err != nil {
   375  			return kubernetesExtra{}, err
   376  		}
   377  		return result, nil
   378  	}
   379  	return kubernetesExtra{}, nil
   380  }
   381  
   382  func fromComposePorts(ports []composeTypes.ServicePortConfig) []latest.ServicePortConfig {
   383  	if ports == nil {
   384  		return nil
   385  	}
   386  	p := make([]latest.ServicePortConfig, len(ports))
   387  	for i, port := range ports {
   388  		p[i] = latest.ServicePortConfig{
   389  			Mode:      port.Mode,
   390  			Target:    port.Target,
   391  			Published: port.Published,
   392  			Protocol:  port.Protocol,
   393  		}
   394  	}
   395  	return p
   396  }
   397  
   398  func fromComposeServiceSecrets(secrets []composeTypes.ServiceSecretConfig) []latest.ServiceSecretConfig {
   399  	if secrets == nil {
   400  		return nil
   401  	}
   402  	c := make([]latest.ServiceSecretConfig, len(secrets))
   403  	for i, secret := range secrets {
   404  		c[i] = latest.ServiceSecretConfig{
   405  			Source: secret.Source,
   406  			Target: secret.Target,
   407  			UID:    secret.UID,
   408  			Mode:   secret.Mode,
   409  		}
   410  	}
   411  	return c
   412  }
   413  
   414  func fromComposeServiceConfigs(configs []composeTypes.ServiceConfigObjConfig) []latest.ServiceConfigObjConfig {
   415  	if configs == nil {
   416  		return nil
   417  	}
   418  	c := make([]latest.ServiceConfigObjConfig, len(configs))
   419  	for i, config := range configs {
   420  		c[i] = latest.ServiceConfigObjConfig{
   421  			Source: config.Source,
   422  			Target: config.Target,
   423  			UID:    config.UID,
   424  			Mode:   config.Mode,
   425  		}
   426  	}
   427  	return c
   428  }
   429  
   430  func fromComposeHealthcheck(h *composeTypes.HealthCheckConfig) *latest.HealthCheckConfig {
   431  	if h == nil {
   432  		return nil
   433  	}
   434  	return &latest.HealthCheckConfig{
   435  		Test:     h.Test,
   436  		Timeout:  composetypes.ConvertDurationPtr(h.Timeout),
   437  		Interval: composetypes.ConvertDurationPtr(h.Interval),
   438  		Retries:  h.Retries,
   439  	}
   440  }
   441  
   442  func fromComposePlacement(p composeTypes.Placement) latest.Placement {
   443  	return latest.Placement{
   444  		Constraints: fromComposeConstraints(p.Constraints),
   445  	}
   446  }
   447  
   448  var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`)
   449  
   450  const (
   451  	swarmOs          = "node.platform.os"
   452  	swarmArch        = "node.platform.arch"
   453  	swarmHostname    = "node.hostname"
   454  	swarmLabelPrefix = "node.labels."
   455  )
   456  
   457  func fromComposeConstraints(s []string) *latest.Constraints {
   458  	if len(s) == 0 {
   459  		return nil
   460  	}
   461  	constraints := &latest.Constraints{}
   462  	for _, constraint := range s {
   463  		matches := constraintEquals.FindStringSubmatch(constraint)
   464  		if len(matches) == 4 {
   465  			key := matches[1]
   466  			operator := matches[2]
   467  			value := matches[3]
   468  			constraint := &latest.Constraint{
   469  				Operator: operator,
   470  				Value:    value,
   471  			}
   472  			switch {
   473  			case key == swarmOs:
   474  				constraints.OperatingSystem = constraint
   475  			case key == swarmArch:
   476  				constraints.Architecture = constraint
   477  			case key == swarmHostname:
   478  				constraints.Hostname = constraint
   479  			case strings.HasPrefix(key, swarmLabelPrefix):
   480  				if constraints.MatchLabels == nil {
   481  					constraints.MatchLabels = map[string]latest.Constraint{}
   482  				}
   483  				constraints.MatchLabels[strings.TrimPrefix(key, swarmLabelPrefix)] = *constraint
   484  			}
   485  		}
   486  	}
   487  	return constraints
   488  }
   489  
   490  func fromComposeResources(r composeTypes.Resources) latest.Resources {
   491  	return latest.Resources{
   492  		Limits:       fromComposeResourcesResourceLimit(r.Limits),
   493  		Reservations: fromComposeResourcesResource(r.Reservations),
   494  	}
   495  }
   496  
   497  // TODO create ResourceLimit type and support for limiting Pids on k8s
   498  func fromComposeResourcesResourceLimit(r *composeTypes.ResourceLimit) *latest.Resource {
   499  	if r == nil {
   500  		return nil
   501  	}
   502  	return &latest.Resource{
   503  		MemoryBytes: int64(r.MemoryBytes),
   504  		NanoCPUs:    r.NanoCPUs,
   505  	}
   506  }
   507  
   508  func fromComposeResourcesResource(r *composeTypes.Resource) *latest.Resource {
   509  	if r == nil {
   510  		return nil
   511  	}
   512  	return &latest.Resource{
   513  		MemoryBytes: int64(r.MemoryBytes),
   514  		NanoCPUs:    r.NanoCPUs,
   515  	}
   516  }
   517  
   518  func fromComposeUpdateConfig(u *composeTypes.UpdateConfig) *latest.UpdateConfig {
   519  	if u == nil {
   520  		return nil
   521  	}
   522  	return &latest.UpdateConfig{
   523  		Parallelism: u.Parallelism,
   524  	}
   525  }
   526  
   527  func fromComposeRestartPolicy(r *composeTypes.RestartPolicy) *latest.RestartPolicy {
   528  	if r == nil {
   529  		return nil
   530  	}
   531  	return &latest.RestartPolicy{
   532  		Condition: r.Condition,
   533  	}
   534  }
   535  
   536  func fromComposeServiceVolumeConfig(vs []composeTypes.ServiceVolumeConfig) []latest.ServiceVolumeConfig {
   537  	if vs == nil {
   538  		return nil
   539  	}
   540  	volumes := []latest.ServiceVolumeConfig{}
   541  	for _, v := range vs {
   542  		volumes = append(volumes, latest.ServiceVolumeConfig{
   543  			Type:     v.Type,
   544  			Source:   v.Source,
   545  			Target:   v.Target,
   546  			ReadOnly: v.ReadOnly,
   547  		})
   548  	}
   549  	return volumes
   550  }
   551  
   552  var (
   553  	v1beta1Capabilities = composeCapabilities{
   554  		apiVersion: "v1beta1",
   555  	}
   556  	v1beta2Capabilities = composeCapabilities{
   557  		apiVersion: "v1beta2",
   558  	}
   559  	v1alpha3Capabilities = composeCapabilities{
   560  		apiVersion:                 "v1alpha3",
   561  		hasPullSecrets:             true,
   562  		hasPullPolicies:            true,
   563  		hasIntraStackLoadBalancing: true,
   564  	}
   565  )
   566  
   567  type composeCapabilities struct {
   568  	apiVersion                 string
   569  	hasPullSecrets             bool
   570  	hasPullPolicies            bool
   571  	hasIntraStackLoadBalancing bool
   572  }
   573  
   574  type kubernetesExtra struct {
   575  	PullSecret          string                     `mapstructure:"pull_secret"`
   576  	PullPolicy          string                     `mapstructure:"pull_policy"`
   577  	InternalServiceType latest.InternalServiceType `mapstructure:"internal_service_type"`
   578  }