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 }