From ba43317862790e861a64fdc1c202274c95dc0c76 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 19 Feb 2020 12:17:16 +0100 Subject: [PATCH 01/24] Replicate docker-compose file loading Skeletton to produce helm chart by internal conversion Signed-off-by: Nicolas De Loof --- compose/docker.go | 23 +++++++++++ compose/errors.go | 38 ++++++++++++++++++ compose/labels.go | 17 ++++++++ compose/project.go | 26 +++++++++++++ helm/output.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++ transform/kube.go | 72 ++++++++++++++++++++++++++++++++++ 6 files changed, 272 insertions(+) create mode 100644 compose/docker.go create mode 100644 compose/errors.go create mode 100644 compose/labels.go create mode 100644 compose/project.go create mode 100644 helm/output.go create mode 100644 transform/kube.go diff --git a/compose/docker.go b/compose/docker.go new file mode 100644 index 000000000..ada6e3b89 --- /dev/null +++ b/compose/docker.go @@ -0,0 +1,23 @@ +package compose + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + registry "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/client" +) + +var ( + Client client.APIClient + RegistryClient registry.RegistryClient + ConfigFile *configfile.ConfigFile + Stdout *streams.Out +) + +func WithDockerCli(cli command.Cli) { + Client = cli.Client() + RegistryClient = cli.RegistryClient(false) + ConfigFile = cli.ConfigFile() + Stdout = cli.Out() +} diff --git a/compose/errors.go b/compose/errors.go new file mode 100644 index 000000000..3f9e290c4 --- /dev/null +++ b/compose/errors.go @@ -0,0 +1,38 @@ +package compose + +import ( + "fmt" + "strings" +) + +func combine(errors []error) error { + if len(errors) == 0 { + return nil + } + if len(errors) == 1 { + return errors[0] + } + err := combinedError{} + for _, e := range errors { + if c, ok := e.(combinedError); ok { + err.errors = append(err.errors, c.errors...) + } else { + err.errors = append(err.errors, e) + } + } + return combinedError{errors} +} + +type combinedError struct { + errors []error +} + +func (c combinedError) Error() string { + points := make([]string, len(c.errors)) + for i, err := range c.errors { + points[i] = fmt.Sprintf("* %s", err.Error()) + } + return fmt.Sprintf( + "%d errors occurred:\n\t%s", + len(c.errors), strings.Join(points, "\n\t")) +} diff --git a/compose/labels.go b/compose/labels.go new file mode 100644 index 000000000..c230fca5d --- /dev/null +++ b/compose/labels.go @@ -0,0 +1,17 @@ +package compose + +const ( + LABEL_DOCKER_COMPOSE_PREFIX = "com.docker.compose" + LABEL_SERVICE = LABEL_DOCKER_COMPOSE_PREFIX + ".service" + LABEL_VERSION = LABEL_DOCKER_COMPOSE_PREFIX + ".version" + LABEL_CONTAINER_NUMBER = LABEL_DOCKER_COMPOSE_PREFIX + ".container-number" + LABEL_ONE_OFF = LABEL_DOCKER_COMPOSE_PREFIX + ".oneoff" + LABEL_NETWORK = LABEL_DOCKER_COMPOSE_PREFIX + ".network" + LABEL_SLUG = LABEL_DOCKER_COMPOSE_PREFIX + ".slug" + LABEL_VOLUME = LABEL_DOCKER_COMPOSE_PREFIX + ".volume" + LABEL_CONFIG_HASH = LABEL_DOCKER_COMPOSE_PREFIX + ".config-hash" + LABEL_PROJECT = LABEL_DOCKER_COMPOSE_PREFIX + ".project" + LABEL_WORKING_DIR = LABEL_DOCKER_COMPOSE_PREFIX + ".working_dir" + LABEL_CONFIG_FILES = LABEL_DOCKER_COMPOSE_PREFIX + ".config_files" + LABEL_ENVIRONMENT_FILE = LABEL_DOCKER_COMPOSE_PREFIX + ".environment_file" +) diff --git a/compose/project.go b/compose/project.go new file mode 100644 index 000000000..901b19bcb --- /dev/null +++ b/compose/project.go @@ -0,0 +1,26 @@ +package compose + +import ( + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" +) + +type Project struct { + types.Config + projectDir string + Name string `yaml:"-" json:"-"` +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: *model, + projectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} diff --git a/helm/output.go b/helm/output.go new file mode 100644 index 000000000..11cc78671 --- /dev/null +++ b/helm/output.go @@ -0,0 +1,96 @@ +package helm + +import ( + "bytes" + "encoding/json" + "gopkg.in/yaml.v3" + "html/template" + "io/ioutil" + "k8s.io/apimachinery/pkg/runtime" + "os" + "path/filepath" +) + +func Write(project string, objects map[string]runtime.Object, target string) error { + out := Outputer{ target } + + if err := out.Write("README.md", []byte("This chart was created by converting a Compose file")); err != nil { + return err + } + + chart := `name: {{.Name}} +description: A generated Helm Chart for {{.Name}} from Skippbox Kompose +version: 0.0.1 +apiVersion: v1 +keywords: + - {{.Name}} +sources: +home: +` + + t, err := template.New("ChartTmpl").Parse(chart) + if err != nil { + return err + } + type ChartDetails struct { + Name string + } + var chartData bytes.Buffer + _ = t.Execute(&chartData, ChartDetails{project}) + + + if err := out.Write("Chart.yaml", chartData.Bytes()); err != nil { + return err + } + + for name, o := range objects { + j, err := json.Marshal(o) + if err != nil { + return err + } + b, err := jsonToYaml(j, 2) + if err != nil { + return err + } + if err := out.Write(filepath.Join("templates", name), b); err != nil { + return err + } + } + return nil +} + +type Outputer struct { + Dir string +} + +func (o Outputer) Write(path string, content []byte) error { + out := filepath.Join(o.Dir, path) + os.MkdirAll(filepath.Dir(out), 0744) + return ioutil.WriteFile(out, content, 0644) +} + +// Convert JSON to YAML. +func jsonToYaml(j []byte, spaces int) ([]byte, error) { + // Convert the JSON to an object. + var jsonObj interface{} + // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the + // Go JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshling to interface{}, it just picks float64 + // universally. go-yaml does go through the effort of picking the right + // number type, so we can preserve number type throughout this process. + err := yaml.Unmarshal(j, &jsonObj) + if err != nil { + return nil, err + } + + var b bytes.Buffer + encoder := yaml.NewEncoder(&b) + encoder.SetIndent(spaces) + if err := encoder.Encode(jsonObj); err != nil { + return nil, err + } + return b.Bytes(), nil + + // Marshal this object into YAML. + // return yaml.Marshal(jsonObj) +} \ No newline at end of file diff --git a/transform/kube.go b/transform/kube.go new file mode 100644 index 000000000..2aa738354 --- /dev/null +++ b/transform/kube.go @@ -0,0 +1,72 @@ +package transform + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + core "k8s.io/api/core/v1" + apps "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { + objects := map[string]runtime.Object{} + for _, service := range model.Services { + objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(service) + objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = mapToDeployment(service) + for _, vol := range service.Volumes { + if vol.Type == "volume" { + objects[fmt.Sprintf("%s-persistentvolumeclain.yaml", service.Name)] = mapToPVC(service, vol) + } + } + } + return objects, nil +} + +func mapToService(service types.ServiceConfig) *core.Service { + return &core.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + }, + Spec: core.ServiceSpec{ + Selector: map[string]string{"com.docker.compose.service": service.Name}, + }, + } +} + +func mapToDeployment(service types.ServiceConfig) *apps.Deployment { + return &apps.Deployment{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: apps.DeploymentSpec{ + Template: core.PodTemplateSpec{ + ObjectMeta: meta.ObjectMeta{ + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PodSpec{ + Containers: []core.Container{ + { + Name: service.Name, + Image: service.Image, + }, + }, + }, + }, + }, + } +} + +func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { + return &core.PersistentVolumeClaim{ + ObjectMeta: meta.ObjectMeta{ + Name: vol.Source, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PersistentVolumeClaimSpec{ + VolumeName: vol.Source, + }, + } +} From f7c86a7d30766b81d010fcf48623ea47de7c5435 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 19 Feb 2020 16:07:58 +0100 Subject: [PATCH 02/24] Extend compose to kube conversion (Most of this code is copy/paste from https://github.com/docker/compose-on-kubernetes/tree/master/internal/convert) Signed-off-by: Nicolas De Loof --- convert/kube.go | 176 +++++++++++++++++++++++ convert/pod.go | 342 +++++++++++++++++++++++++++++++++++++++++++++ convert/volumes.go | 228 ++++++++++++++++++++++++++++++ transform/kube.go | 72 ---------- 4 files changed, 746 insertions(+), 72 deletions(-) create mode 100644 convert/kube.go create mode 100644 convert/pod.go create mode 100644 convert/volumes.go delete mode 100644 transform/kube.go diff --git a/convert/kube.go b/convert/kube.go new file mode 100644 index 000000000..23a34843f --- /dev/null +++ b/convert/kube.go @@ -0,0 +1,176 @@ +package convert + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + apps "k8s.io/api/apps/v1" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "strings" + "time" +) + +func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { + objects := map[string]runtime.Object{} + for _, service := range model.Services { + objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) + if service.Deploy != nil && service.Deploy.Mode == "global" { + daemonset, err := mapToDaemonset(service, model) + if err != nil { + return nil, err + } + objects[fmt.Sprintf("%s-daemonset.yaml", service.Name)] = daemonset + } else { + deployment, err := mapToDeployment(service, model) + if err != nil { + return nil, err + } + objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = deployment + } + for _, vol := range service.Volumes { + if vol.Type == "volume" { + objects[fmt.Sprintf("%s-persistentvolumeclain.yaml", service.Name)] = mapToPVC(service, vol) + } + } + } + return objects, nil +} + +func mapToService(model *compose.Project, service types.ServiceConfig) *core.Service { + ports := []core.ServicePort{} + for _, p := range service.Ports { + ports = append(ports, + core.ServicePort{ + Name: fmt.Sprintf("%d-%s", p.Target, strings.ToLower(string(p.Protocol))), + Port: int32(p.Target), + TargetPort: intstr.FromInt(int(p.Target)), + Protocol: toProtocol(p.Protocol), + }) + } + + return &core.Service{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + }, + Spec: core.ServiceSpec{ + Selector: map[string]string{"com.docker.compose.service": service.Name}, + Ports: ports, + Type: mapServiceToServiceType(service, model), + }, + } +} + +func mapServiceToServiceType(service types.ServiceConfig, model *compose.Project) core.ServiceType { + serviceType := core.ServiceTypeClusterIP + if len(service.Networks) == 0 { + // service is implicitly attached to "default" network + serviceType = core.ServiceTypeLoadBalancer + } + for name := range service.Networks { + if !model.Networks[name].Internal { + serviceType = core.ServiceTypeLoadBalancer + } + } + for _, port := range service.Ports { + if port.Published != 0 { + serviceType = core.ServiceTypeNodePort + } + } + return serviceType +} + +func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps.Deployment, error) { + labels := map[string]string{ + "com.docker.compose.service": service.Name, + "com.docker.compose.project": model.Name, + } + podTemplate, err := toPodTemplate(service, labels, model) + if err != nil { + return nil, err + } + + return &apps.Deployment{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: labels, + }, + Spec: apps.DeploymentSpec{ + Replicas: toReplicas(service.Deploy), + Strategy: toDeploymentStrategy(service.Deploy), + Template: podTemplate, + }, + }, nil +} + +func mapToDaemonset(service types.ServiceConfig, model *compose.Project) (*apps.DaemonSet, error) { + labels := map[string]string{ + "com.docker.compose.service": service.Name, + "com.docker.compose.project": model.Name, + } + podTemplate, err := toPodTemplate(service, labels, model) + if err != nil { + return nil, err + } + + return &apps.DaemonSet{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: labels, + }, + Spec: apps.DaemonSetSpec{ + Template: podTemplate, + }, + }, nil +} + + +func toReplicas(deploy *types.DeployConfig) *int32 { + v := int32(1) + if deploy != nil { + v = int32(*deploy.Replicas) + } + return &v +} + +func toDeploymentStrategy(deploy *types.DeployConfig) apps.DeploymentStrategy { + if deploy == nil || deploy.UpdateConfig == nil { + return apps.DeploymentStrategy{ + Type: apps.RecreateDeploymentStrategyType, + } + } + return apps.DeploymentStrategy{ + Type: apps.RollingUpdateDeploymentStrategyType, + RollingUpdate: &apps.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(*deploy.UpdateConfig.Parallelism), + }, + MaxSurge: nil, + }, + } +} + +func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { + return &core.PersistentVolumeClaim{ + ObjectMeta: meta.ObjectMeta{ + Name: vol.Source, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PersistentVolumeClaimSpec{ + VolumeName: vol.Source, + }, + } +} + +// toSecondsOrDefault converts a duration string in seconds and defaults to a +// given value if the duration is nil. +// The supported units are us, ms, s, m and h. +func toSecondsOrDefault(duration *types.Duration, defaultValue int32) int32 { //nolint: unparam + if duration == nil { + return defaultValue + } + return int32(time.Duration(*duration).Seconds()) +} diff --git a/convert/pod.go b/convert/pod.go new file mode 100644 index 000000000..29561c000 --- /dev/null +++ b/convert/pod.go @@ -0,0 +1,342 @@ +package convert + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/helm-prototype/pkg/compose" + "sort" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *compose.Project) (apiv1.PodTemplateSpec, error) { + tpl := apiv1.PodTemplateSpec{} + hostAliases, err := toHostAliases(serviceConfig.ExtraHosts) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + env, err := toEnv(serviceConfig.Environment) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + restartPolicy, err := toRestartPolicy(serviceConfig) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + limits, err := toResource(serviceConfig.Deploy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + requests, err := toResource(serviceConfig.Deploy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + volumes, err := toVolumes(serviceConfig, model) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + volumeMounts, err := toVolumeMounts(serviceConfig, model) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } +/* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } */ + tpl.ObjectMeta = metav1.ObjectMeta{ + Labels: labels, + Annotations: serviceConfig.Labels, + } + tpl.Spec.RestartPolicy = restartPolicy + tpl.Spec.Volumes = volumes + tpl.Spec.HostPID = toHostPID(serviceConfig.Pid) + tpl.Spec.HostIPC = toHostIPC(serviceConfig.Ipc) + tpl.Spec.Hostname = serviceConfig.Hostname + tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod) + tpl.Spec.HostAliases = hostAliases + // FIXME tpl.Spec.Affinity = nodeAffinity + // we dont want to remove all containers and recreate them because: + // an admission plugin can add sidecar containers + // we for sure want to keep the main container to be additive + if len(tpl.Spec.Containers) == 0 { + tpl.Spec.Containers = []apiv1.Container{{}} + } + + containerIX := 0 + for ix, c := range tpl.Spec.Containers { + if c.Name == serviceConfig.Name { + containerIX = ix + break + } + } + tpl.Spec.Containers[containerIX].Name = serviceConfig.Name + tpl.Spec.Containers[containerIX].Image = serviceConfig.Image + // FIXME tpl.Spec.Containers[containerIX].ImagePullPolicy = pullPolicy + tpl.Spec.Containers[containerIX].Command = serviceConfig.Entrypoint + tpl.Spec.Containers[containerIX].Args = serviceConfig.Command + tpl.Spec.Containers[containerIX].WorkingDir = serviceConfig.WorkingDir + tpl.Spec.Containers[containerIX].TTY = serviceConfig.Tty + tpl.Spec.Containers[containerIX].Stdin = serviceConfig.StdinOpen + tpl.Spec.Containers[containerIX].Ports = toPorts(serviceConfig.Ports) + tpl.Spec.Containers[containerIX].LivenessProbe = toLivenessProbe(serviceConfig.HealthCheck) + tpl.Spec.Containers[containerIX].Env = env + tpl.Spec.Containers[containerIX].VolumeMounts = volumeMounts + tpl.Spec.Containers[containerIX].SecurityContext = toSecurityContext(serviceConfig) + tpl.Spec.Containers[containerIX].Resources = apiv1.ResourceRequirements{ + Limits: limits, + Requests: requests, + } + + /* FIXME + if serviceConfig.PullSecret != "" { + pullSecrets := map[string]struct{}{} + for _, ps := range tpl.Spec.ImagePullSecrets { + pullSecrets[ps.Name] = struct{}{} + } + if _, ok := pullSecrets[serviceConfig.PullSecret]; !ok { + tpl.Spec.ImagePullSecrets = append(tpl.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: serviceConfig.PullSecret}) + } + } + */ + return tpl, nil +} + +func toImagePullPolicy(image string, specifiedPolicy string) (apiv1.PullPolicy, error) { + if specifiedPolicy == "" { + if strings.HasSuffix(image, ":latest") { + return apiv1.PullAlways, nil + } + return apiv1.PullIfNotPresent, nil + } + switch apiv1.PullPolicy(specifiedPolicy) { + case apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever: + return apiv1.PullPolicy(specifiedPolicy), nil + default: + return "", errors.Errorf("invalid pull policy %q, must be %q, %q or %q", specifiedPolicy, apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever) + } +} + +func toHostAliases(extraHosts []string) ([]apiv1.HostAlias, error) { + if extraHosts == nil { + return nil, nil + } + + byHostnames := map[string]string{} + for _, host := range extraHosts { + split := strings.SplitN(host, ":", 2) + if len(split) != 2 { + return nil, errors.Errorf("malformed host %s", host) + } + byHostnames[split[0]] = split[1] + } + + byIPs := map[string][]string{} + for k, v := range byHostnames { + byIPs[v] = append(byIPs[v], k) + } + + aliases := make([]apiv1.HostAlias, len(byIPs)) + i := 0 + for key, hosts := range byIPs { + sort.Strings(hosts) + aliases[i] = apiv1.HostAlias{ + IP: key, + Hostnames: hosts, + } + i++ + } + sort.Slice(aliases, func(i, j int) bool { return aliases[i].IP < aliases[j].IP }) + return aliases, nil +} + +func toHostPID(pid string) bool { + return "host" == pid +} + +func toHostIPC(ipc string) bool { + return "host" == ipc +} + +func toTerminationGracePeriodSeconds(duration *types.Duration) *int64 { + if duration == nil { + return nil + } + gracePeriod := int64(time.Duration(*duration).Seconds()) + return &gracePeriod +} + +func toLivenessProbe(hc *types.HealthCheckConfig) *apiv1.Probe { + if hc == nil || len(hc.Test) < 1 || hc.Test[0] == "NONE" { + return nil + } + + command := hc.Test[1:] + if hc.Test[0] == "CMD-SHELL" { + command = append([]string{"sh", "-c"}, command...) + } + + return &apiv1.Probe{ + TimeoutSeconds: toSecondsOrDefault(hc.Timeout, 1), + PeriodSeconds: toSecondsOrDefault(hc.Interval, 1), + FailureThreshold: int32(defaultUint64(hc.Retries, 3)), + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: command, + }, + }, + } +} + +func toEnv(env map[string]*string) ([]apiv1.EnvVar, error) { + var envVars []apiv1.EnvVar + + for k, v := range env { + if v == nil { + return nil, errors.Errorf("%s has no value, unsetting an environment variable is not supported", k) + } + envVars = append(envVars, toEnvVar(k, *v)) + } + sort.Slice(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) + return envVars, nil +} + +func toEnvVar(key, value string) apiv1.EnvVar { + return apiv1.EnvVar{ + Name: key, + Value: value, + } +} + +func toPorts(list []types.ServicePortConfig) []apiv1.ContainerPort { + var ports []apiv1.ContainerPort + + for _, v := range list { + ports = append(ports, apiv1.ContainerPort{ + ContainerPort: int32(v.Target), + Protocol: toProtocol(v.Protocol), + }) + } + + return ports +} + +func toProtocol(value string) apiv1.Protocol { + if value == "udp" { + return apiv1.ProtocolUDP + } + return apiv1.ProtocolTCP +} + +func toRestartPolicy(s types.ServiceConfig) (apiv1.RestartPolicy, error) { + if s.Deploy == nil || s.Deploy.RestartPolicy == nil { + return apiv1.RestartPolicyAlways, nil + } + policy := s.Deploy.RestartPolicy + + switch policy.Condition { + case string(swarm.RestartPolicyConditionAny): + return apiv1.RestartPolicyAlways, nil + case string(swarm.RestartPolicyConditionNone): + return apiv1.RestartPolicyNever, nil + case string(swarm.RestartPolicyConditionOnFailure): + return apiv1.RestartPolicyOnFailure, nil + default: + return "", errors.Errorf("unsupported restart policy %s", policy.Condition) + } +} + +func toResource(deploy *types.DeployConfig) (apiv1.ResourceList, error) { + if deploy == nil || deploy.Resources.Limits == nil { + return nil, nil + } + + res := deploy.Resources.Limits + list := make(apiv1.ResourceList) + if res.NanoCPUs != "" { + cpus, err := resource.ParseQuantity(res.NanoCPUs) + if err != nil { + return nil, err + } + list[apiv1.ResourceCPU] = cpus + } + if res.MemoryBytes != 0 { + memory, err := resource.ParseQuantity(fmt.Sprintf("%v", res.MemoryBytes)) + if err != nil { + return nil, err + } + list[apiv1.ResourceMemory] = memory + } + return list, nil +} + +func toSecurityContext(s types.ServiceConfig) *apiv1.SecurityContext { + isPrivileged := toBoolPointer(s.Privileged) + isReadOnly := toBoolPointer(s.ReadOnly) + + var capabilities *apiv1.Capabilities + if s.CapAdd != nil || s.CapDrop != nil { + capabilities = &apiv1.Capabilities{ + Add: toCapabilities(s.CapAdd), + Drop: toCapabilities(s.CapDrop), + } + } + + var userID *int64 + if s.User != "" { + numerical, err := strconv.Atoi(s.User) + if err == nil { + unixUserID := int64(numerical) + userID = &unixUserID + } + } + + if isPrivileged == nil && isReadOnly == nil && capabilities == nil && userID == nil { + return nil + } + + return &apiv1.SecurityContext{ + RunAsUser: userID, + Privileged: isPrivileged, + ReadOnlyRootFilesystem: isReadOnly, + Capabilities: capabilities, + } +} + +func toBoolPointer(value bool) *bool { + if value { + return &value + } + + return nil +} + +func defaultUint64(v *uint64, defaultValue uint64) uint64 { //nolint: unparam + if v == nil { + return defaultValue + } + + return *v +} + +func toCapabilities(list []string) (capabilities []apiv1.Capability) { + for _, c := range list { + capabilities = append(capabilities, apiv1.Capability(c)) + } + return +} + +//nolint: unparam +func forceRestartPolicy(podTemplate apiv1.PodTemplateSpec, forcedRestartPolicy apiv1.RestartPolicy) apiv1.PodTemplateSpec { + if podTemplate.Spec.RestartPolicy != "" { + podTemplate.Spec.RestartPolicy = forcedRestartPolicy + } + + return podTemplate +} diff --git a/convert/volumes.go b/convert/volumes.go new file mode 100644 index 000000000..d3828c2e7 --- /dev/null +++ b/convert/volumes.go @@ -0,0 +1,228 @@ +package convert + +import ( + "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + "path" + "path/filepath" + "strings" + + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" +) + +const dockerSock = "/var/run/docker.sock" + +type volumeSpec struct { + mount apiv1.VolumeMount + source *apiv1.VolumeSource +} + +func hasPersistentVolumes(s types.ServiceConfig) bool { + for _, volume := range s.Volumes { + if volume.Type == "volume" { + return true + } + } + + return false +} + +func toVolumeSpecs(s types.ServiceConfig, model *compose.Project) ([]volumeSpec, error) { + var specs []volumeSpec + for i, m := range s.Volumes { + var source *apiv1.VolumeSource + name := fmt.Sprintf("mount-%d", i) + subpath := "" + if m.Source == dockerSock && m.Target == dockerSock { + subpath = "docker.sock" + source = hostPathVolume("/var/run") + } else if strings.HasSuffix(m.Source, ".git") { + source = gitVolume(m.Source) + } else if m.Type == "volume" { + if m.Source != "" { + name = m.Source + } + } else { + // bind mount + if !filepath.IsAbs(m.Source) { + return nil, errors.Errorf("%s: only absolute paths can be specified in mount source", m.Source) + } + if m.Source == "/" { + source = hostPathVolume("/") + } else { + parent, file := filepath.Split(m.Source) + if parent != "/" { + parent = strings.TrimSuffix(parent, "/") + } + source = hostPathVolume(parent) + subpath = file + } + } + + specs = append(specs, volumeSpec{ + source: source, + mount: volumeMount(name, m.Target, m.ReadOnly, subpath), + }) + } + + for i, m := range s.Tmpfs { + name := fmt.Sprintf("tmp-%d", i) + + specs = append(specs, volumeSpec{ + source: emptyVolumeInMemory(), + mount: volumeMount(name, m, false, ""), + }) + } + + for i, s := range s.Secrets { + name := fmt.Sprintf("secret-%d", i) + + target := path.Join("/run/secrets", or(s.Target, s.Source)) + subPath := name + readOnly := true + + specs = append(specs, volumeSpec{ + source: secretVolume(s, model.Secrets[name], subPath), + mount: volumeMount(name, target, readOnly, subPath), + }) + } + + for i, c := range s.Configs { + name := fmt.Sprintf("config-%d", i) + + target := or(c.Target, "/"+c.Source) + subPath := name + readOnly := true + + specs = append(specs, volumeSpec{ + source: configVolume(c, model.Configs[name], subPath), + mount: volumeMount(name, target, readOnly, subPath), + }) + } + + return specs, nil +} + +func or(v string, defaultValue string) string { + if v != "" && v != "." { + return v + } + + return defaultValue +} + +func toVolumeMounts(s types.ServiceConfig, model *compose.Project) ([]apiv1.VolumeMount, error) { + var mounts []apiv1.VolumeMount + specs, err := toVolumeSpecs(s, model) + if err != nil { + return nil, err + } + for _, spec := range specs { + mounts = append(mounts, spec.mount) + } + return mounts, nil +} + +func toVolumes(s types.ServiceConfig, model *compose.Project) ([]apiv1.Volume, error) { + var volumes []apiv1.Volume + specs, err := toVolumeSpecs(s, model) + if err != nil { + return nil, err + } + for _, spec := range specs { + if spec.source == nil { + continue + } + volumes = append(volumes, apiv1.Volume{ + Name: spec.mount.Name, + VolumeSource: *spec.source, + }) + } + return volumes, nil +} + +func gitVolume(path string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + GitRepo: &apiv1.GitRepoVolumeSource{ + Repository: filepath.ToSlash(path), + }, + } +} + +func hostPathVolume(path string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + HostPath: &apiv1.HostPathVolumeSource{ + Path: path, + }, + } +} + +func defaultMode(mode *uint32) *int32 { + var defaultMode *int32 + + if mode != nil { + signedMode := int32(*mode) + defaultMode = &signedMode + } + + return defaultMode +} + +func secretVolume(config types.ServiceSecretConfig, topLevelSecret types.SecretConfig, subPath string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: config.Source, + Items: []apiv1.KeyToPath{ + { + Key: toKey(topLevelSecret.File), + Path: subPath, + Mode: defaultMode(config.Mode), + }, + }, + }, + } +} + +func volumeMount(name, path string, readOnly bool, subPath string) apiv1.VolumeMount { + return apiv1.VolumeMount{ + Name: name, + MountPath: path, + ReadOnly: readOnly, + SubPath: subPath, + } +} + +func configVolume(config types.ServiceConfigObjConfig, topLevelConfig types.ConfigObjConfig, subPath string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: config.Source, + }, + Items: []apiv1.KeyToPath{ + { + Key: toKey(topLevelConfig.File), + Path: subPath, + Mode: defaultMode(config.Mode), + }, + }, + }, + } +} + +func toKey(file string) string { + if file != "" { + return path.Base(file) + } + + return "file" // TODO: hard-coded key for external configs +} + +func emptyVolumeInMemory() *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + EmptyDir: &apiv1.EmptyDirVolumeSource{ + Medium: apiv1.StorageMediumMemory, + }, + } +} diff --git a/transform/kube.go b/transform/kube.go deleted file mode 100644 index 2aa738354..000000000 --- a/transform/kube.go +++ /dev/null @@ -1,72 +0,0 @@ -package transform - -import ( - "fmt" - "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - core "k8s.io/api/core/v1" - apps "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { - objects := map[string]runtime.Object{} - for _, service := range model.Services { - objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(service) - objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = mapToDeployment(service) - for _, vol := range service.Volumes { - if vol.Type == "volume" { - objects[fmt.Sprintf("%s-persistentvolumeclain.yaml", service.Name)] = mapToPVC(service, vol) - } - } - } - return objects, nil -} - -func mapToService(service types.ServiceConfig) *core.Service { - return &core.Service{ - ObjectMeta: meta.ObjectMeta{ - Name: service.Name, - }, - Spec: core.ServiceSpec{ - Selector: map[string]string{"com.docker.compose.service": service.Name}, - }, - } -} - -func mapToDeployment(service types.ServiceConfig) *apps.Deployment { - return &apps.Deployment{ - ObjectMeta: meta.ObjectMeta{ - Name: service.Name, - Labels: map[string]string{"com.docker.compose.service": service.Name}, - }, - Spec: apps.DeploymentSpec{ - Template: core.PodTemplateSpec{ - ObjectMeta: meta.ObjectMeta{ - Labels: map[string]string{"com.docker.compose.service": service.Name}, - }, - Spec: core.PodSpec{ - Containers: []core.Container{ - { - Name: service.Name, - Image: service.Image, - }, - }, - }, - }, - }, - } -} - -func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { - return &core.PersistentVolumeClaim{ - ObjectMeta: meta.ObjectMeta{ - Name: vol.Source, - Labels: map[string]string{"com.docker.compose.service": service.Name}, - }, - Spec: core.PersistentVolumeClaimSpec{ - VolumeName: vol.Source, - }, - } -} From f1976eca07d283b97c4389f88f4618574ba89c5f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 20 Feb 2020 16:43:06 +0100 Subject: [PATCH 03/24] podTemplate conversion test cases adapted from compose-on-kubernetes some test need to be fixed, marked as /*FIXME Test*/ Signed-off-by: Nicolas De Loof --- convert/pod.go | 27 +- convert/pod_test.go | 984 ++++++++++++++++++++++++++++++++++++++++++++ convert/volumes.go | 4 +- 3 files changed, 1001 insertions(+), 14 deletions(-) create mode 100644 convert/pod_test.go diff --git a/convert/pod.go b/convert/pod.go index 29561c000..7ce7dcb2c 100644 --- a/convert/pod.go +++ b/convert/pod.go @@ -30,14 +30,22 @@ func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, if err != nil { return apiv1.PodTemplateSpec{}, err } - limits, err := toResource(serviceConfig.Deploy) - if err != nil { - return apiv1.PodTemplateSpec{}, err + + var limits apiv1.ResourceList = nil + if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Limits != nil { + limits, err = toResource(serviceConfig.Deploy.Resources.Limits) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } } - requests, err := toResource(serviceConfig.Deploy) - if err != nil { - return apiv1.PodTemplateSpec{}, err + var requests apiv1.ResourceList = nil + if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Reservations != nil { + requests, err = toResource(serviceConfig.Deploy.Resources.Reservations) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } } + volumes, err := toVolumes(serviceConfig, model) if err != nil { return apiv1.PodTemplateSpec{}, err @@ -252,12 +260,7 @@ func toRestartPolicy(s types.ServiceConfig) (apiv1.RestartPolicy, error) { } } -func toResource(deploy *types.DeployConfig) (apiv1.ResourceList, error) { - if deploy == nil || deploy.Resources.Limits == nil { - return nil, nil - } - - res := deploy.Resources.Limits +func toResource(res *types.Resource) (apiv1.ResourceList, error) { list := make(apiv1.ResourceList) if res.NanoCPUs != "" { cpus, err := resource.ParseQuantity(res.NanoCPUs) diff --git a/convert/pod_test.go b/convert/pod_test.go new file mode 100644 index 000000000..491173a88 --- /dev/null +++ b/convert/pod_test.go @@ -0,0 +1,984 @@ +package convert + +import ( + "fmt" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + "github.com/stretchr/testify/assert" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "os" + "runtime" + "testing" +) + +func loadYAML(yaml string) (*compose.Project, error) { + dict, err := loader.ParseYAML([]byte(yaml)) + if err != nil { + return nil, err + } + workingDir, err := os.Getwd() + if err != nil { + panic(err) + } + return compose.NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: []types.ConfigFile{ + {Filename: "compose.yaml", Config: dict}, + }, + }, "test") +} + +func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { + res, err := podTemplateWithError(yaml) + assert.NoError(t, err) + return res +} + +func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { + project, err := loadYAML(yaml) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + return toPodTemplate(project.Services[0], nil, project) +} + +func TestToPodWithDockerSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" +`) + + expectedVolume := apiv1.Volume{ + Name: "mount-0", + VolumeSource: apiv1.VolumeSource{ + HostPath: &apiv1.HostPathVolumeSource{ + Path: "/var/run", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "mount-0", + MountPath: "/var/run/docker.sock", + SubPath: "docker.sock", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithFunkyCommand(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: basi/node-exporter + command: ["-collector.procfs", "/host/proc", "-collector.sysfs", "/host/sys"] +`) + + expectedArgs := []string{ + `-collector.procfs`, + `/host/proc`, // ? + `-collector.sysfs`, + `/host/sys`, // ? + } + assert.Equal(t, expectedArgs, podTemplate.Spec.Containers[0].Args) +} + +func TestToPodWithGlobalVolume(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + db: + image: "postgres:9.4" + volumes: + - dbdata:/var/lib/postgresql/data +`) + + expectedMount := apiv1.VolumeMount{ + Name: "dbdata", + MountPath: "/var/lib/postgresql/data", + } + assert.Len(t, podTemplate.Spec.Volumes, 0) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithResources(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + db: + image: "postgres:9.4" + deploy: + resources: + limits: + cpus: "0.001" + memory: 50Mb + reservations: + cpus: "0.0001" + memory: 20Mb +`) + + expectedResourceRequirements := apiv1.ResourceRequirements{ + Limits: map[apiv1.ResourceName]resource.Quantity{ + apiv1.ResourceCPU: resource.MustParse("0.001"), + apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 50*1024*1024)), + }, + Requests: map[apiv1.ResourceName]resource.Quantity{ + apiv1.ResourceCPU: resource.MustParse("0.0001"), + apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 20*1024*1024)), + }, + } + assert.Equal(t, expectedResourceRequirements, podTemplate.Spec.Containers[0].Resources) +} + +func TestToPodWithCapabilities(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + cap_add: + - ALL + cap_drop: + - NET_ADMIN + - SYS_ADMIN +`) + + expectedSecurityContext := &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{ + Add: []apiv1.Capability{"ALL"}, + Drop: []apiv1.Capability{"NET_ADMIN", "SYS_ADMIN"}, + }, + } + + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithReadOnly(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + read_only: true +`) + + yes := true + expectedSecurityContext := &apiv1.SecurityContext{ + ReadOnlyRootFilesystem: &yes, + } + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithPrivileged(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + privileged: true +`) + + yes := true + expectedSecurityContext := &apiv1.SecurityContext{ + Privileged: &yes, + } + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithEnvNilShouldErrorOut(t *testing.T) { + _, err := podTemplateWithError(` +version: "3" +services: + redis: + image: "redis:alpine" + environment: + - SESSION_SECRET +`) + assert.Error(t, err) +} + +func TestToPodWithEnv(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + environment: + - RACK_ENV=development + - SHOW=true +`) + + expectedEnv := []apiv1.EnvVar{ + { + Name: "RACK_ENV", + Value: "development", + }, + { + Name: "SHOW", + Value: "true", + }, + } + + assert.Equal(t, expectedEnv, podTemplate.Spec.Containers[0].Env) +} + +func TestToPodWithVolume(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + volumes: + - /ignore:/ignore + - /opt/data:/var/lib/mysql:ro +`) + + assert.Len(t, podTemplate.Spec.Volumes, 2) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 2) +} + +func /*FIXME Test*/ToPodWithRelativeVolumes(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + _, err := podTemplateWithError(` +version: "3" +services: + nginx: + image: nginx + volumes: + - ./fail:/ignore +`) + + assert.Error(t, err) +} + +func TestToPodWithHealthCheck(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 90s + timeout: 10s + retries: 3 +`) + + expectedLivenessProbe := &apiv1.Probe{ + TimeoutSeconds: 10, + PeriodSeconds: 90, + FailureThreshold: 3, + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: []string{"curl", "-f", "http://localhost"}, + }, + }, + } + + assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) +} + +func TestToPodWithShellHealthCheck(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost"] +`) + + expectedLivenessProbe := &apiv1.Probe{ + TimeoutSeconds: 1, + PeriodSeconds: 1, + FailureThreshold: 3, + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: []string{"sh", "-c", "curl -f http://localhost"}, + }, + }, + } + + assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) +} + +func TestToPodWithTargetlessExternalSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - my_secret +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external secrets use + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithExternalSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret + target: nginx_secret +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external secrets use + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/nginx_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ToPodWithFileBasedSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret +secrets: + my_secret: + file: ./secret.txt +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "secret.txt", + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ToPodWithTwoFileBasedSecrets(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret1 + - source: my_secret2 + target: secret2 +secrets: + my_secret1: + file: ./secret1.txt + my_secret2: + file: ./secret2.txt +`) + + expectedVolumes := []apiv1.Volume{ + { + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret1", + Items: []apiv1.KeyToPath{ + { + Key: "secret1.txt", + Path: "secret-0", + }, + }, + }, + }, + }, + { + Name: "secret-1", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret2", + Items: []apiv1.KeyToPath{ + { + Key: "secret2.txt", + Path: "secret-1", + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret1", + SubPath: "secret-0", + }, + { + Name: "secret-1", + ReadOnly: true, + MountPath: "/run/secrets/secret2", + SubPath: "secret-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func TestToPodWithTerminationGracePeriod(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + stop_grace_period: 100s +`) + + expected := int64(100) + assert.Equal(t, &expected, podTemplate.Spec.TerminationGracePeriodSeconds) +} + +func TestToPodWithTmpfs(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + tmpfs: + - /tmp +`) + + expectedVolume := apiv1.Volume{ + Name: "tmp-0", + VolumeSource: apiv1.VolumeSource{ + EmptyDir: &apiv1.EmptyDirVolumeSource{ + Medium: "Memory", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "tmp-0", + MountPath: "/tmp", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithNumericalUser(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + user: "1000" +`) + + userID := int64(1000) + + expectedSecurityContext := &apiv1.SecurityContext{ + RunAsUser: &userID, + } + + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithGitVolume(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + volumes: + - source: "git@github.com:moby/moby.git" + target: /sources + type: git +`) + + expectedVolume := apiv1.Volume{ + Name: "mount-0", + VolumeSource: apiv1.VolumeSource{ + GitRepo: &apiv1.GitRepoVolumeSource{ + Repository: "git@github.com:moby/moby.git", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "mount-0", + ReadOnly: false, + MountPath: "/sources", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ToPodWithFileBasedConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - source: my_config + target: /usr/share/nginx/html/index.html + uid: "103" + gid: "103" + mode: 0440 +configs: + my_config: + file: ./file.html +`) + + mode := int32(0440) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "my_config", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file.html", + Path: "config-0", + Mode: &mode, + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/usr/share/nginx/html/index.html", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ToPodWithTargetlessFileBasedConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - my_config +configs: + my_config: + file: ./file.html +`) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "myconfig", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file.html", + Path: "config-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/myconfig", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithExternalConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - source: my_config + target: /usr/share/nginx/html/index.html + uid: "103" + gid: "103" + mode: 0440 +configs: + my_config: + external: true +`) + + mode := int32(0440) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "my_config", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external config use + Path: "config-0", + Mode: &mode, + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/usr/share/nginx/html/index.html", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ToPodWithTwoConfigsSameMountPoint(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + configs: + - source: first + target: /data/first.json + mode: "0440" + - source: second + target: /data/second.json + mode: "0550" +configs: + first: + file: ./file1 + secondv: + file: ./file2 +`) + + mode0440 := int32(0440) + mode0550 := int32(0550) + + expectedVolumes := []apiv1.Volume{ + { + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "first", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file1", + Path: "config-0", + Mode: &mode0440, + }, + }, + }, + }, + }, + { + Name: "config-1", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "second", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file2", + Path: "config-1", + Mode: &mode0550, + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "config-0", + ReadOnly: true, + MountPath: "/data/first.json", + SubPath: "config-0", + }, + { + Name: "config-1", + ReadOnly: true, + MountPath: "/data/second.json", + SubPath: "config-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func TestToPodWithTwoExternalConfigsSameMountPoint(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + configs: + - source: first + target: /data/first.json + - source: second + target: /data/second.json +configs: + first: + file: ./file1 + second: + file: ./file2 +`) + + expectedVolumes := []apiv1.Volume{ + { + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "first", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", + Path: "config-0", + }, + }, + }, + }, + }, + { + Name: "config-1", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "second", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", + Path: "config-1", + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "config-0", + ReadOnly: true, + MountPath: "/data/first.json", + SubPath: "config-0", + }, + { + Name: "config-1", + ReadOnly: true, + MountPath: "/data/second.json", + SubPath: "config-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func /*FIXME Test*/ToPodWithPullSecret(t *testing.T) { + podTemplateWithSecret := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + x-kubernetes.pull-secret: test-pull-secret +`) + + assert.Equal(t, 1, len(podTemplateWithSecret.Spec.ImagePullSecrets)) + assert.Equal(t, "test-pull-secret", podTemplateWithSecret.Spec.ImagePullSecrets[0].Name) + + podTemplateNoSecret := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx +`) + + assert.Nil(t, podTemplateNoSecret.Spec.ImagePullSecrets) +} + +func /*FIXME Test*/ToPodWithPullPolicy(t *testing.T) { + cases := []struct { + name string + stack string + expectedPolicy apiv1.PullPolicy + expectedError string + }{ + { + name: "specific tag", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific +`, + expectedPolicy: apiv1.PullIfNotPresent, + }, + { + name: "latest tag", + stack: ` +version: "3" +services: + nginx: + image: nginx:latest +`, + expectedPolicy: apiv1.PullAlways, + }, + { + name: "explicit policy", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific + x-kubernetes.pull-policy: Never +`, + expectedPolicy: apiv1.PullNever, + }, + { + name: "invalid policy", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific + x-kubernetes.pull-policy: Invalid +`, + expectedError: `invalid pull policy "Invalid", must be "Always", "IfNotPresent" or "Never"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + pod, err := podTemplateWithError(c.stack) + if c.expectedError != "" { + assert.EqualError(t, err, c.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, c.expectedPolicy) + } + }) + } +} diff --git a/convert/volumes.go b/convert/volumes.go index d3828c2e7..a87968421 100644 --- a/convert/volumes.go +++ b/convert/volumes.go @@ -170,13 +170,13 @@ func defaultMode(mode *uint32) *int32 { return defaultMode } -func secretVolume(config types.ServiceSecretConfig, topLevelSecret types.SecretConfig, subPath string) *apiv1.VolumeSource { +func secretVolume(config types.ServiceSecretConfig, topLevelConfig types.SecretConfig, subPath string) *apiv1.VolumeSource { return &apiv1.VolumeSource{ Secret: &apiv1.SecretVolumeSource{ SecretName: config.Source, Items: []apiv1.KeyToPath{ { - Key: toKey(topLevelSecret.File), + Key: toKey(topLevelConfig.File), Path: subPath, Mode: defaultMode(config.Mode), }, From 5306f38f70206badd2b7a240b0623d2065c56551 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 21 Feb 2020 17:54:01 +0100 Subject: [PATCH 04/24] Fix linter Signed-off-by: Guillaume Tardif --- compose/labels.go | 26 +++++++++++++------------- convert/kube.go | 26 +++++++++++++------------- convert/pod.go | 19 ++++++++++--------- convert/pod_test.go | 23 ++++++++++++----------- convert/volumes.go | 5 +++-- helm/output.go | 10 +++++----- 6 files changed, 56 insertions(+), 53 deletions(-) diff --git a/compose/labels.go b/compose/labels.go index c230fca5d..8ef7d4985 100644 --- a/compose/labels.go +++ b/compose/labels.go @@ -1,17 +1,17 @@ package compose const ( - LABEL_DOCKER_COMPOSE_PREFIX = "com.docker.compose" - LABEL_SERVICE = LABEL_DOCKER_COMPOSE_PREFIX + ".service" - LABEL_VERSION = LABEL_DOCKER_COMPOSE_PREFIX + ".version" - LABEL_CONTAINER_NUMBER = LABEL_DOCKER_COMPOSE_PREFIX + ".container-number" - LABEL_ONE_OFF = LABEL_DOCKER_COMPOSE_PREFIX + ".oneoff" - LABEL_NETWORK = LABEL_DOCKER_COMPOSE_PREFIX + ".network" - LABEL_SLUG = LABEL_DOCKER_COMPOSE_PREFIX + ".slug" - LABEL_VOLUME = LABEL_DOCKER_COMPOSE_PREFIX + ".volume" - LABEL_CONFIG_HASH = LABEL_DOCKER_COMPOSE_PREFIX + ".config-hash" - LABEL_PROJECT = LABEL_DOCKER_COMPOSE_PREFIX + ".project" - LABEL_WORKING_DIR = LABEL_DOCKER_COMPOSE_PREFIX + ".working_dir" - LABEL_CONFIG_FILES = LABEL_DOCKER_COMPOSE_PREFIX + ".config_files" - LABEL_ENVIRONMENT_FILE = LABEL_DOCKER_COMPOSE_PREFIX + ".environment_file" + LabelDockerComposePrefix = "com.docker.compose" + LabelService = LabelDockerComposePrefix + ".service" + LabelVersion = LabelDockerComposePrefix + ".version" + LabelContainerNumber = LabelDockerComposePrefix + ".container-number" + LabelOneOff = LabelDockerComposePrefix + ".oneoff" + LabelNetwork = LabelDockerComposePrefix + ".network" + LabelSlug = LabelDockerComposePrefix + ".slug" + LabelVolume = LabelDockerComposePrefix + ".volume" + LabelConfigHash = LabelDockerComposePrefix + ".config-hash" + LabelProject = LabelDockerComposePrefix + ".project" + LabelWorkingDir = LabelDockerComposePrefix + ".working_dir" + LabelConfigFiles = LabelDockerComposePrefix + ".config_files" + LabelEnvironmentFile = LabelDockerComposePrefix + ".environment_file" ) diff --git a/convert/kube.go b/convert/kube.go index 23a34843f..c90fe45a9 100644 --- a/convert/kube.go +++ b/convert/kube.go @@ -2,6 +2,9 @@ package convert import ( "fmt" + "strings" + "time" + "github.com/compose-spec/compose-go/types" "github.com/docker/helm-prototype/pkg/compose" apps "k8s.io/api/apps/v1" @@ -9,13 +12,11 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" - "strings" - "time" ) func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { objects := map[string]runtime.Object{} - for _, service := range model.Services { + for _, service := range model.Services { objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) if service.Deploy != nil && service.Deploy.Mode == "global" { daemonset, err := mapToDaemonset(service, model) @@ -94,13 +95,13 @@ func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps return &apps.Deployment{ ObjectMeta: meta.ObjectMeta{ - Name: service.Name, + Name: service.Name, Labels: labels, }, - Spec: apps.DeploymentSpec{ - Replicas: toReplicas(service.Deploy), - Strategy: toDeploymentStrategy(service.Deploy), - Template: podTemplate, + Spec: apps.DeploymentSpec{ + Replicas: toReplicas(service.Deploy), + Strategy: toDeploymentStrategy(service.Deploy), + Template: podTemplate, }, }, nil } @@ -126,7 +127,6 @@ func mapToDaemonset(service types.ServiceConfig, model *compose.Project) (*apps. }, nil } - func toReplicas(deploy *types.DeployConfig) *int32 { v := int32(1) if deploy != nil { @@ -156,11 +156,11 @@ func toDeploymentStrategy(deploy *types.DeployConfig) apps.DeploymentStrategy { func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { return &core.PersistentVolumeClaim{ ObjectMeta: meta.ObjectMeta{ - Name: vol.Source, - Labels: map[string]string{"com.docker.compose.service": service.Name}, + Name: vol.Source, + Labels: map[string]string{"com.docker.compose.service": service.Name}, }, - Spec: core.PersistentVolumeClaimSpec{ - VolumeName: vol.Source, + Spec: core.PersistentVolumeClaimSpec{ + VolumeName: vol.Source, }, } } diff --git a/convert/pod.go b/convert/pod.go index 7ce7dcb2c..85036a031 100644 --- a/convert/pod.go +++ b/convert/pod.go @@ -2,14 +2,15 @@ package convert import ( "fmt" - "github.com/compose-spec/compose-go/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/helm-prototype/pkg/compose" "sort" "strconv" "strings" "time" + "github.com/compose-spec/compose-go/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/helm-prototype/pkg/compose" + "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -31,14 +32,14 @@ func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, return apiv1.PodTemplateSpec{}, err } - var limits apiv1.ResourceList = nil + var limits apiv1.ResourceList if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Limits != nil { limits, err = toResource(serviceConfig.Deploy.Resources.Limits) if err != nil { return apiv1.PodTemplateSpec{}, err } } - var requests apiv1.ResourceList = nil + var requests apiv1.ResourceList if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Reservations != nil { requests, err = toResource(serviceConfig.Deploy.Resources.Reservations) if err != nil { @@ -54,10 +55,10 @@ func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, if err != nil { return apiv1.PodTemplateSpec{}, err } -/* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } */ + /* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } */ tpl.ObjectMeta = metav1.ObjectMeta{ Labels: labels, Annotations: serviceConfig.Labels, diff --git a/convert/pod_test.go b/convert/pod_test.go index 491173a88..3e50dd0e5 100644 --- a/convert/pod_test.go +++ b/convert/pod_test.go @@ -2,15 +2,16 @@ package convert import ( "fmt" + "os" + "runtime" + "testing" + "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/helm-prototype/pkg/compose" "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - "os" - "runtime" - "testing" ) func loadYAML(yaml string) (*compose.Project, error) { @@ -256,7 +257,7 @@ services: assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 2) } -func /*FIXME Test*/ToPodWithRelativeVolumes(t *testing.T) { +func /*FIXME Test*/ ToPodWithRelativeVolumes(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") return @@ -401,7 +402,7 @@ services: assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } -func /*FIXME Test*/ToPodWithFileBasedSecret(t *testing.T) { +func /*FIXME Test*/ ToPodWithFileBasedSecret(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: @@ -442,7 +443,7 @@ secrets: assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } -func /*FIXME Test*/ToPodWithTwoFileBasedSecrets(t *testing.T) { +func /*FIXME Test*/ ToPodWithTwoFileBasedSecrets(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: @@ -603,7 +604,7 @@ services: assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } -func /*FIXME Test*/ToPodWithFileBasedConfig(t *testing.T) { +func /*FIXME Test*/ ToPodWithFileBasedConfig(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: @@ -653,7 +654,7 @@ configs: assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } -func /*FIXME Test*/ToPodWithTargetlessFileBasedConfig(t *testing.T) { +func /*FIXME Test*/ ToPodWithTargetlessFileBasedConfig(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: @@ -746,7 +747,7 @@ configs: assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) } -func /*FIXME Test*/ToPodWithTwoConfigsSameMountPoint(t *testing.T) { +func /*FIXME Test*/ ToPodWithTwoConfigsSameMountPoint(t *testing.T) { podTemplate := podTemplate(t, ` version: "3" services: @@ -897,7 +898,7 @@ configs: assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) } -func /*FIXME Test*/ToPodWithPullSecret(t *testing.T) { +func /*FIXME Test*/ ToPodWithPullSecret(t *testing.T) { podTemplateWithSecret := podTemplate(t, ` version: "3" services: @@ -919,7 +920,7 @@ services: assert.Nil(t, podTemplateNoSecret.Spec.ImagePullSecrets) } -func /*FIXME Test*/ToPodWithPullPolicy(t *testing.T) { +func /*FIXME Test*/ ToPodWithPullPolicy(t *testing.T) { cases := []struct { name string stack string diff --git a/convert/volumes.go b/convert/volumes.go index a87968421..57eb5bb70 100644 --- a/convert/volumes.go +++ b/convert/volumes.go @@ -2,12 +2,13 @@ package convert import ( "fmt" - "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" "path" "path/filepath" "strings" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose" + "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" ) diff --git a/helm/output.go b/helm/output.go index 11cc78671..edf7cdb1e 100644 --- a/helm/output.go +++ b/helm/output.go @@ -3,16 +3,17 @@ package helm import ( "bytes" "encoding/json" - "gopkg.in/yaml.v3" "html/template" "io/ioutil" - "k8s.io/apimachinery/pkg/runtime" "os" "path/filepath" + + "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/runtime" ) func Write(project string, objects map[string]runtime.Object, target string) error { - out := Outputer{ target } + out := Outputer{target} if err := out.Write("README.md", []byte("This chart was created by converting a Compose file")); err != nil { return err @@ -38,7 +39,6 @@ home: var chartData bytes.Buffer _ = t.Execute(&chartData, ChartDetails{project}) - if err := out.Write("Chart.yaml", chartData.Bytes()); err != nil { return err } @@ -93,4 +93,4 @@ func jsonToYaml(j []byte, spaces int) ([]byte, error) { // Marshal this object into YAML. // return yaml.Marshal(jsonObj) -} \ No newline at end of file +} From d1e40e9c36bb2b8ab506854fca7abfa9efdc6c13 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 2 Mar 2020 09:24:06 +0100 Subject: [PATCH 05/24] add support for placement constraints Signed-off-by: Nicolas De Loof --- convert/placement.go | 126 +++++++++++++++++++++++++++++ convert/placement_test.go | 163 ++++++++++++++++++++++++++++++++++++++ convert/pod.go | 6 +- 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 convert/placement.go create mode 100644 convert/placement_test.go diff --git a/convert/placement.go b/convert/placement.go new file mode 100644 index 000000000..c757e0a5e --- /dev/null +++ b/convert/placement.go @@ -0,0 +1,126 @@ +package convert + +import ( + "regexp" + "strings" + + "github.com/compose-spec/compose-go/types" + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" +) + +var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`) + +const ( + kubernetesOs = "beta.kubernetes.io/os" + kubernetesArch = "beta.kubernetes.io/arch" + kubernetesHostname = "kubernetes.io/hostname" +) + +// node.id Node ID node.id == 2ivku8v2gvtg4 +// node.hostname Node hostname node.hostname != node-2 +// node.role Node role node.role == manager +// node.labels user defined node labels node.labels.security == high +// engine.labels Docker Engine's labels engine.labels.operatingsystem == ubuntu 14.04 +func toNodeAffinity(deploy *types.DeployConfig) (*apiv1.Affinity, error) { + constraints := []string{} + if deploy != nil && deploy.Placement.Constraints != nil { + constraints = deploy.Placement.Constraints + } + requirements := []apiv1.NodeSelectorRequirement{} + for _, constraint := range constraints { + matches := constraintEquals.FindStringSubmatch(constraint) + if len(matches) == 4 { + key := matches[1] + operator, err := toRequirementOperator(matches[2]) + if err != nil { + return nil, err + } + value := matches[3] + + switch { + case key == constraintOs: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesOs, + Operator: operator, + Values: []string{value}, + }) + case key == constraintArch: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesArch, + Operator: operator, + Values: []string{value}, + }) + case key == constraintHostname: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesHostname, + Operator: operator, + Values: []string{value}, + }) + case strings.HasPrefix(key, constraintLabelPrefix): + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: strings.TrimPrefix(key, constraintLabelPrefix), + Operator: operator, + Values: []string{value}, + }) + } + } + } + + if !hasRequirement(requirements, kubernetesOs) { + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesOs, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{"linux"}, + }) + } + if !hasRequirement(requirements, kubernetesArch) { + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesArch, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{"amd64"}, + }) + } + return &apiv1.Affinity{ + NodeAffinity: &apiv1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ + NodeSelectorTerms: []apiv1.NodeSelectorTerm{ + { + MatchExpressions: requirements, + }, + }, + }, + }, + }, nil +} + +const ( + constraintOs = "node.platform.os" + constraintArch = "node.platform.arch" + constraintHostname = "node.hostname" + constraintLabelPrefix = "node.labels." +) + +func hasRequirement(requirements []apiv1.NodeSelectorRequirement, key string) bool { + for _, r := range requirements { + if r.Key == key { + return true + } + } + return false +} + +func toRequirementOperator(sign string) (apiv1.NodeSelectorOperator, error) { + switch sign { + case "==": + return apiv1.NodeSelectorOpIn, nil + case "!=": + return apiv1.NodeSelectorOpNotIn, nil + case ">": + return apiv1.NodeSelectorOpGt, nil + case "<": + return apiv1.NodeSelectorOpLt, nil + default: + return "", errors.Errorf("operator %s not supported", sign) + } +} diff --git a/convert/placement_test.go b/convert/placement_test.go new file mode 100644 index 000000000..1169d3c85 --- /dev/null +++ b/convert/placement_test.go @@ -0,0 +1,163 @@ +package convert + +import ( + "reflect" + "sort" + "testing" + + "github.com/compose-spec/compose-go/types" + + "github.com/stretchr/testify/assert" + apiv1 "k8s.io/api/core/v1" +) + +func TestToPodWithPlacement(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: redis:alpine + deploy: + placement: + constraints: + - node.platform.os == linux + - node.platform.arch == amd64 + - node.hostname == node01 + - node.labels.label1 == value1 + - node.labels.label2.subpath != value2 +`) + + expectedRequirements := []apiv1.NodeSelectorRequirement{ + {Key: "beta.kubernetes.io/os", Operator: apiv1.NodeSelectorOpIn, Values: []string{"linux"}}, + {Key: "beta.kubernetes.io/arch", Operator: apiv1.NodeSelectorOpIn, Values: []string{"amd64"}}, + {Key: "kubernetes.io/hostname", Operator: apiv1.NodeSelectorOpIn, Values: []string{"node01"}}, + {Key: "label1", Operator: apiv1.NodeSelectorOpIn, Values: []string{"value1"}}, + {Key: "label2.subpath", Operator: apiv1.NodeSelectorOpNotIn, Values: []string{"value2"}}, + } + + requirements := podTemplate.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions + + sort.Slice(expectedRequirements, func(i, j int) bool { return expectedRequirements[i].Key < expectedRequirements[j].Key }) + sort.Slice(requirements, func(i, j int) bool { return requirements[i].Key < requirements[j].Key }) + + assert.EqualValues(t, expectedRequirements, requirements) +} + +type keyValue struct { + key string + value string +} + +func kv(key, value string) keyValue { + return keyValue{key: key, value: value} +} + +func makeExpectedAffinity(kvs ...keyValue) *apiv1.Affinity { + + var matchExpressions []apiv1.NodeSelectorRequirement + for _, kv := range kvs { + matchExpressions = append( + matchExpressions, + apiv1.NodeSelectorRequirement{ + Key: kv.key, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{kv.value}, + }, + ) + } + return &apiv1.Affinity{ + NodeAffinity: &apiv1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ + NodeSelectorTerms: []apiv1.NodeSelectorTerm{ + { + MatchExpressions: matchExpressions, + }, + }, + }, + }, + } +} + +func TestNodeAfinity(t *testing.T) { + cases := []struct { + name string + source []string + expected *apiv1.Affinity + }{ + { + name: "nil", + expected: makeExpectedAffinity( + kv(kubernetesOs, "linux"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "hostname", + source: []string{"node.hostname == test"}, + expected: makeExpectedAffinity( + kv(kubernetesHostname, "test"), + kv(kubernetesOs, "linux"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "os", + source: []string{"node.platform.os == windows"}, + expected: makeExpectedAffinity( + kv(kubernetesOs, "windows"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "arch", + source: []string{"node.platform.arch == arm64"}, + expected: makeExpectedAffinity( + kv(kubernetesArch, "arm64"), + kv(kubernetesOs, "linux"), + ), + }, + { + name: "custom-labels", + source: []string{"node.platform.os == windows", "node.platform.arch == arm64"}, + expected: makeExpectedAffinity( + kv(kubernetesArch, "arm64"), + kv(kubernetesOs, "windows"), + ), + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := toNodeAffinity(&types.DeployConfig{ + Placement: types.Placement{ + Constraints: c.source, + }, + }) + assert.NoError(t, err) + assert.True(t, nodeAffinityMatch(c.expected, result)) + }) + } +} + +func nodeSelectorRequirementsToMap(source []apiv1.NodeSelectorRequirement, result map[string]apiv1.NodeSelectorRequirement) { + for _, t := range source { + result[t.Key] = t + } +} + +func nodeAffinityMatch(expected, actual *apiv1.Affinity) bool { + expectedTerms := expected.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + actualTerms := actual.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + expectedExpressions := make(map[string]apiv1.NodeSelectorRequirement) + expectedFields := make(map[string]apiv1.NodeSelectorRequirement) + actualExpressions := make(map[string]apiv1.NodeSelectorRequirement) + actualFields := make(map[string]apiv1.NodeSelectorRequirement) + for _, v := range expectedTerms { + nodeSelectorRequirementsToMap(v.MatchExpressions, expectedExpressions) + nodeSelectorRequirementsToMap(v.MatchFields, expectedFields) + } + for _, v := range actualTerms { + nodeSelectorRequirementsToMap(v.MatchExpressions, actualExpressions) + nodeSelectorRequirementsToMap(v.MatchFields, actualFields) + } + return reflect.DeepEqual(expectedExpressions, actualExpressions) && reflect.DeepEqual(expectedFields, actualFields) +} diff --git a/convert/pod.go b/convert/pod.go index 85036a031..03e91cc42 100644 --- a/convert/pod.go +++ b/convert/pod.go @@ -19,6 +19,10 @@ import ( func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *compose.Project) (apiv1.PodTemplateSpec, error) { tpl := apiv1.PodTemplateSpec{} + nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } hostAliases, err := toHostAliases(serviceConfig.ExtraHosts) if err != nil { return apiv1.PodTemplateSpec{}, err @@ -70,7 +74,7 @@ func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, tpl.Spec.Hostname = serviceConfig.Hostname tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod) tpl.Spec.HostAliases = hostAliases - // FIXME tpl.Spec.Affinity = nodeAffinity + tpl.Spec.Affinity = nodeAffinity // we dont want to remove all containers and recreate them because: // an admission plugin can add sidecar containers // we for sure want to keep the main container to be additive From 4ffc3939096d1c926ef72dd3d2860815ae7ebc2b Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 1 Apr 2020 09:40:37 +0200 Subject: [PATCH 06/24] Fix missing fields in chart generate Signed-off-by: aiordache --- convert/kube.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/convert/kube.go b/convert/kube.go index c90fe45a9..abfa97e04 100644 --- a/convert/kube.go +++ b/convert/kube.go @@ -16,6 +16,7 @@ import ( func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { objects := map[string]runtime.Object{} + for _, service := range model.Services { objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) if service.Deploy != nil && service.Deploy.Mode == "global" { @@ -53,6 +54,10 @@ func mapToService(model *compose.Project, service types.ServiceConfig) *core.Ser } return &core.Service{ + TypeMeta: meta.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, ObjectMeta: meta.ObjectMeta{ Name: service.Name, }, @@ -92,13 +97,22 @@ func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps if err != nil { return nil, err } - + selector := new(meta.LabelSelector) + selector.MatchLabels = make(map[string]string) + for key, val := range labels { + selector.MatchLabels[key] = val + } return &apps.Deployment{ + TypeMeta: meta.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, ObjectMeta: meta.ObjectMeta{ Name: service.Name, Labels: labels, }, Spec: apps.DeploymentSpec{ + Selector: selector, Replicas: toReplicas(service.Deploy), Strategy: toDeploymentStrategy(service.Deploy), Template: podTemplate, From ce3e4d7717cea415da755df465107606b9ceb818 Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 2 Apr 2020 00:33:33 +0200 Subject: [PATCH 07/24] project restructure Signed-off-by: aiordache --- compose/compose.go | 96 ++++++++++++++ compose/docker.go | 23 ---- {convert => compose/internal/convert}/kube.go | 19 ++- .../internal/convert}/placement.go | 0 .../internal/convert}/placement_test.go | 0 {convert => compose/internal/convert}/pod.go | 3 +- .../internal/convert}/pod_test.go | 25 ++-- .../internal/convert}/volumes.go | 7 +- compose/internal/helm/action.go | 34 +++++ {helm => compose/internal/helm}/output.go | 0 compose/internal/utils/config.go | 124 ++++++++++++++++++ compose/internal/utils/env.go | 20 +++ compose/{ => internal/utils}/errors.go | 4 +- compose/{ => internal/utils}/labels.go | 2 +- compose/project.go | 26 ---- 15 files changed, 305 insertions(+), 78 deletions(-) create mode 100644 compose/compose.go delete mode 100644 compose/docker.go rename {convert => compose/internal/convert}/kube.go (87%) rename {convert => compose/internal/convert}/placement.go (100%) rename {convert => compose/internal/convert}/placement_test.go (100%) rename {convert => compose/internal/convert}/pod.go (98%) rename {convert => compose/internal/convert}/pod_test.go (98%) rename {convert => compose/internal/convert}/volumes.go (93%) create mode 100644 compose/internal/helm/action.go rename {helm => compose/internal/helm}/output.go (100%) create mode 100644 compose/internal/utils/config.go create mode 100644 compose/internal/utils/env.go rename compose/{ => internal/utils}/errors.go (91%) rename compose/{ => internal/utils}/labels.go (98%) delete mode 100644 compose/project.go diff --git a/compose/compose.go b/compose/compose.go new file mode 100644 index 000000000..203b3b8a2 --- /dev/null +++ b/compose/compose.go @@ -0,0 +1,96 @@ +package compose + +import ( + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + registry "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/streams" + "github.com/docker/docker/client" + "github.com/docker/helm-prototype/pkg/compose/internal/convert" + "github.com/docker/helm-prototype/pkg/compose/internal/helm" + utils "github.com/docker/helm-prototype/pkg/compose/internal/utils" +) + +var ( + Client client.APIClient + RegistryClient registry.RegistryClient + ConfigFile *configfile.ConfigFile + Stdout *streams.Out +) + +func WithDockerCli(cli command.Cli) { + Client = cli.Client() + RegistryClient = cli.RegistryClient(false) + ConfigFile = cli.ConfigFile() + Stdout = cli.Out() +} + +// Orchestrator is "kubernetes" or "swarm" +type Orchestrator string + +const ( + // Kubernetes specifies to use kubernetes. + Kubernetes Orchestrator = "kubernetes" + // Swarm specifies to use Docker swarm. + Swarm Orchestrator = "swarm" +) + +type ProjectOptions struct { + ConfigPaths []string + Name string +} + +type Project struct { + Config *types.Config + ProjectDir string + Name string `yaml:"-" json:"-"` + Orchestrator Orchestrator +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: model, + ProjectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} + +// projectFromOptions load a compose project based on command line options +func ProjectFromOptions(options *ProjectOptions) (*Project, error) { + workingDir, configs, err := utils.GetConfigs( + options.Name, + options.ConfigPaths, + ) + if err != nil { + return nil, err + } + + return NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + }, options.Name) +} + +func (p *Project) GenerateCharts(path string) error { + objects, err := convert.MapToKubernetesObjects(p.Config, p.Name) + if err != nil { + return err + } + err = helm.Write(p.Name, objects, path) + if err != nil { + return err + } + return nil +} +func (p *Project) InstallCommand(options *ProjectOptions) error { + return nil +} diff --git a/compose/docker.go b/compose/docker.go deleted file mode 100644 index ada6e3b89..000000000 --- a/compose/docker.go +++ /dev/null @@ -1,23 +0,0 @@ -package compose - -import ( - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/config/configfile" - registry "github.com/docker/cli/cli/registry/client" - "github.com/docker/cli/cli/streams" - "github.com/docker/docker/client" -) - -var ( - Client client.APIClient - RegistryClient registry.RegistryClient - ConfigFile *configfile.ConfigFile - Stdout *streams.Out -) - -func WithDockerCli(cli command.Cli) { - Client = cli.Client() - RegistryClient = cli.RegistryClient(false) - ConfigFile = cli.ConfigFile() - Stdout = cli.Out() -} diff --git a/convert/kube.go b/compose/internal/convert/kube.go similarity index 87% rename from convert/kube.go rename to compose/internal/convert/kube.go index abfa97e04..884d56935 100644 --- a/convert/kube.go +++ b/compose/internal/convert/kube.go @@ -6,7 +6,6 @@ import ( "time" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -14,19 +13,19 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) -func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, error) { +func MapToKubernetesObjects(model *types.Config, name string) (map[string]runtime.Object, error) { objects := map[string]runtime.Object{} for _, service := range model.Services { objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) if service.Deploy != nil && service.Deploy.Mode == "global" { - daemonset, err := mapToDaemonset(service, model) + daemonset, err := mapToDaemonset(service, model, name) if err != nil { return nil, err } objects[fmt.Sprintf("%s-daemonset.yaml", service.Name)] = daemonset } else { - deployment, err := mapToDeployment(service, model) + deployment, err := mapToDeployment(service, model, name) if err != nil { return nil, err } @@ -41,7 +40,7 @@ func MapToKubernetesObjects(model *compose.Project) (map[string]runtime.Object, return objects, nil } -func mapToService(model *compose.Project, service types.ServiceConfig) *core.Service { +func mapToService(model *types.Config, service types.ServiceConfig) *core.Service { ports := []core.ServicePort{} for _, p := range service.Ports { ports = append(ports, @@ -69,7 +68,7 @@ func mapToService(model *compose.Project, service types.ServiceConfig) *core.Ser } } -func mapServiceToServiceType(service types.ServiceConfig, model *compose.Project) core.ServiceType { +func mapServiceToServiceType(service types.ServiceConfig, model *types.Config) core.ServiceType { serviceType := core.ServiceTypeClusterIP if len(service.Networks) == 0 { // service is implicitly attached to "default" network @@ -88,10 +87,10 @@ func mapServiceToServiceType(service types.ServiceConfig, model *compose.Project return serviceType } -func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps.Deployment, error) { +func mapToDeployment(service types.ServiceConfig, model *types.Config, name string) (*apps.Deployment, error) { labels := map[string]string{ "com.docker.compose.service": service.Name, - "com.docker.compose.project": model.Name, + "com.docker.compose.project": name, } podTemplate, err := toPodTemplate(service, labels, model) if err != nil { @@ -120,10 +119,10 @@ func mapToDeployment(service types.ServiceConfig, model *compose.Project) (*apps }, nil } -func mapToDaemonset(service types.ServiceConfig, model *compose.Project) (*apps.DaemonSet, error) { +func mapToDaemonset(service types.ServiceConfig, model *types.Config, name string) (*apps.DaemonSet, error) { labels := map[string]string{ "com.docker.compose.service": service.Name, - "com.docker.compose.project": model.Name, + "com.docker.compose.project": name, } podTemplate, err := toPodTemplate(service, labels, model) if err != nil { diff --git a/convert/placement.go b/compose/internal/convert/placement.go similarity index 100% rename from convert/placement.go rename to compose/internal/convert/placement.go diff --git a/convert/placement_test.go b/compose/internal/convert/placement_test.go similarity index 100% rename from convert/placement_test.go rename to compose/internal/convert/placement_test.go diff --git a/convert/pod.go b/compose/internal/convert/pod.go similarity index 98% rename from convert/pod.go rename to compose/internal/convert/pod.go index 03e91cc42..2842bba54 100644 --- a/convert/pod.go +++ b/compose/internal/convert/pod.go @@ -9,7 +9,6 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/docker/docker/api/types/swarm" - "github.com/docker/helm-prototype/pkg/compose" "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" @@ -17,7 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *compose.Project) (apiv1.PodTemplateSpec, error) { +func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *types.Config) (apiv1.PodTemplateSpec, error) { tpl := apiv1.PodTemplateSpec{} nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) if err != nil { diff --git a/convert/pod_test.go b/compose/internal/convert/pod_test.go similarity index 98% rename from convert/pod_test.go rename to compose/internal/convert/pod_test.go index 3e50dd0e5..078a8f839 100644 --- a/convert/pod_test.go +++ b/compose/internal/convert/pod_test.go @@ -8,13 +8,12 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) -func loadYAML(yaml string) (*compose.Project, error) { +func loadYAML(yaml string) (*loader.Config, error) { dict, err := loader.ParseYAML([]byte(yaml)) if err != nil { return nil, err @@ -23,12 +22,17 @@ func loadYAML(yaml string) (*compose.Project, error) { if err != nil { panic(err) } - return compose.NewProject(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: []types.ConfigFile{ - {Filename: "compose.yaml", Config: dict}, - }, - }, "test") + configs := ConfigFiles: []types.ConfigFile{ + {Filename: "compose.yaml", Config: dict}, + }, + + config := types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + } + model, err := loader.Load(config) + return model } func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { @@ -38,11 +42,12 @@ func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { } func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { - project, err := loadYAML(yaml) + model, err := loadYAML(yaml) if err != nil { return apiv1.PodTemplateSpec{}, err } - return toPodTemplate(project.Services[0], nil, project) + + return toPodTemplate(model.Services[0], nil, model) } func TestToPodWithDockerSocket(t *testing.T) { diff --git a/convert/volumes.go b/compose/internal/convert/volumes.go similarity index 93% rename from convert/volumes.go rename to compose/internal/convert/volumes.go index 57eb5bb70..bcc38eb3c 100644 --- a/convert/volumes.go +++ b/compose/internal/convert/volumes.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose" "github.com/pkg/errors" apiv1 "k8s.io/api/core/v1" @@ -30,7 +29,7 @@ func hasPersistentVolumes(s types.ServiceConfig) bool { return false } -func toVolumeSpecs(s types.ServiceConfig, model *compose.Project) ([]volumeSpec, error) { +func toVolumeSpecs(s types.ServiceConfig, model *types.Config) ([]volumeSpec, error) { var specs []volumeSpec for i, m := range s.Volumes { var source *apiv1.VolumeSource @@ -114,7 +113,7 @@ func or(v string, defaultValue string) string { return defaultValue } -func toVolumeMounts(s types.ServiceConfig, model *compose.Project) ([]apiv1.VolumeMount, error) { +func toVolumeMounts(s types.ServiceConfig, model *types.Config) ([]apiv1.VolumeMount, error) { var mounts []apiv1.VolumeMount specs, err := toVolumeSpecs(s, model) if err != nil { @@ -126,7 +125,7 @@ func toVolumeMounts(s types.ServiceConfig, model *compose.Project) ([]apiv1.Volu return mounts, nil } -func toVolumes(s types.ServiceConfig, model *compose.Project) ([]apiv1.Volume, error) { +func toVolumes(s types.ServiceConfig, model *types.Config) ([]apiv1.Volume, error) { var volumes []apiv1.Volume specs, err := toVolumeSpecs(s, model) if err != nil { diff --git a/compose/internal/helm/action.go b/compose/internal/helm/action.go new file mode 100644 index 000000000..e500abba8 --- /dev/null +++ b/compose/internal/helm/action.go @@ -0,0 +1,34 @@ +package helm + +import ( + "os" + "sync" + + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +type KubeConfig struct { + namespace string + config genericclioptions.RESTClientGetter + configOnce sync.Once + + // KubeConfig is the path to the kubeconfig file + KubeConfig string + // KubeContext is the name of the kubeconfig context. + KubeContext string + // Bearer KubeToken used for authentication + KubeToken string + // Kubernetes API Server Endpoint for authentication + KubeAPIServer string +} + +func New() *KubeConfig { + + env := KubeConfig{ + namespace: "", + KubeContext: os.Getenv("COMPOSE_KUBECONTEXT"), + KubeToken: os.Getenv("COMPOSE_KUBETOKEN"), + KubeAPIServer: os.Getenv("COMPOSE_KUBEAPISERVER"), + } + return &env +} diff --git a/helm/output.go b/compose/internal/helm/output.go similarity index 100% rename from helm/output.go rename to compose/internal/helm/output.go diff --git a/compose/internal/utils/config.go b/compose/internal/utils/config.go new file mode 100644 index 000000000..12974553b --- /dev/null +++ b/compose/internal/utils/config.go @@ -0,0 +1,124 @@ +package utils + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/prometheus/common/log" +) + +var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, error) { + configPath, err := getConfigPaths(configPaths) + if err != nil { + return "", nil, err + } + + if name == "" { + name = os.Getenv("COMPOSE_PROJECT_NAME") + } + + workingDir := filepath.Dir(configPath[0]) + + if name == "" { + r := regexp.MustCompile(`[^a-z0-9\\-_]+`) + name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") + } + + configs, err := parseConfigs(configPath) + if err != nil { + return "", nil, err + } + return workingDir, configs, nil +} + +func getConfigPaths(configPaths []string) ([]string, error) { + paths := []string{} + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + if len(configPaths) != 0 { + for _, f := range configPaths { + if f == "-" { + paths = append(paths, f) + continue + } + if !filepath.IsAbs(f) { + f = filepath.Join(pwd, f) + } + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil + } + + sep := os.Getenv("COMPOSE_FILE_SEPARATOR") + if sep == "" { + sep = string(os.PathListSeparator) + } + f := os.Getenv("COMPOSE_FILE") + if f != "" { + return strings.Split(f, sep), nil + } + + for { + candidates := []string{} + for _, n := range SupportedFilenames { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + log.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + log.Warnf("Using %s\n", winner) + } + return []string{winner}, nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") + } + pwd = parent + } +} + +func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { + files := []types.ConfigFile{} + for _, f := range configPaths { + var ( + b []byte + err error + ) + if f == "-" { + b, err = ioutil.ReadAll(os.Stdin) + } else { + if _, err := os.Stat(f); err != nil { + return nil, err + } + b, err = ioutil.ReadFile(f) + } + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(b) + if err != nil { + return nil, err + } + files = append(files, types.ConfigFile{Filename: f, Config: config}) + } + return files, nil +} diff --git a/compose/internal/utils/env.go b/compose/internal/utils/env.go new file mode 100644 index 000000000..3044742bb --- /dev/null +++ b/compose/internal/utils/env.go @@ -0,0 +1,20 @@ +package utils + +import ( + "os" + "strings" +) + +func Environment() map[string]string { + return getAsEqualsMap(os.Environ()) +} + +// getAsEqualsMap split key=value formatted strings into a key : value map +func getAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} diff --git a/compose/errors.go b/compose/internal/utils/errors.go similarity index 91% rename from compose/errors.go rename to compose/internal/utils/errors.go index 3f9e290c4..07a903e6c 100644 --- a/compose/errors.go +++ b/compose/internal/utils/errors.go @@ -1,11 +1,11 @@ -package compose +package utils import ( "fmt" "strings" ) -func combine(errors []error) error { +func CombineErrors(errors []error) error { if len(errors) == 0 { return nil } diff --git a/compose/labels.go b/compose/internal/utils/labels.go similarity index 98% rename from compose/labels.go rename to compose/internal/utils/labels.go index 8ef7d4985..4ff180c54 100644 --- a/compose/labels.go +++ b/compose/internal/utils/labels.go @@ -1,4 +1,4 @@ -package compose +package utils const ( LabelDockerComposePrefix = "com.docker.compose" diff --git a/compose/project.go b/compose/project.go deleted file mode 100644 index 901b19bcb..000000000 --- a/compose/project.go +++ /dev/null @@ -1,26 +0,0 @@ -package compose - -import ( - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" -) - -type Project struct { - types.Config - projectDir string - Name string `yaml:"-" json:"-"` -} - -func NewProject(config types.ConfigDetails, name string) (*Project, error) { - model, err := loader.Load(config) - if err != nil { - return nil, err - } - - p := Project{ - Config: *model, - projectDir: config.WorkingDir, - Name: name, - } - return &p, nil -} From 898b7d4666f632f1a5d8bc6269209d4ec9825cd1 Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 2 Apr 2020 14:36:01 +0200 Subject: [PATCH 08/24] implements helm install cmd Signed-off-by: aiordache --- compose/compose.go | 84 ++++++++++++++++++++++++--------- compose/internal/helm/action.go | 34 ------------- compose/internal/helm/output.go | 2 +- 3 files changed, 63 insertions(+), 57 deletions(-) delete mode 100644 compose/internal/helm/action.go diff --git a/compose/compose.go b/compose/compose.go index 203b3b8a2..d7a40d781 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -1,32 +1,20 @@ package compose import ( + "log" + + chartloader "helm.sh/helm/v3/pkg/chart/loader" + "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/config/configfile" - registry "github.com/docker/cli/cli/registry/client" - "github.com/docker/cli/cli/streams" - "github.com/docker/docker/client" "github.com/docker/helm-prototype/pkg/compose/internal/convert" "github.com/docker/helm-prototype/pkg/compose/internal/helm" utils "github.com/docker/helm-prototype/pkg/compose/internal/utils" + "helm.sh/helm/v3/pkg/action" + env "helm.sh/helm/v3/pkg/cli" + k "helm.sh/helm/v3/pkg/kube" ) -var ( - Client client.APIClient - RegistryClient registry.RegistryClient - ConfigFile *configfile.ConfigFile - Stdout *streams.Out -) - -func WithDockerCli(cli command.Cli) { - Client = cli.Client() - RegistryClient = cli.RegistryClient(false) - ConfigFile = cli.ConfigFile() - Stdout = cli.Out() -} - // Orchestrator is "kubernetes" or "swarm" type Orchestrator string @@ -65,6 +53,10 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) { // projectFromOptions load a compose project based on command line options func ProjectFromOptions(options *ProjectOptions) (*Project, error) { + if options.Name == "" { + options.Name = "docker-compose" + } + workingDir, configs, err := utils.GetConfigs( options.Name, options.ConfigPaths, @@ -80,7 +72,7 @@ func ProjectFromOptions(options *ProjectOptions) (*Project, error) { }, options.Name) } -func (p *Project) GenerateCharts(path string) error { +func (p *Project) GenerateChart(path string) error { objects, err := convert.MapToKubernetesObjects(p.Config, p.Name) if err != nil { return err @@ -91,6 +83,54 @@ func (p *Project) GenerateCharts(path string) error { } return nil } -func (p *Project) InstallCommand(options *ProjectOptions) error { - return nil + +func (p *Project) InstallChart(n, path string) error { + + if path == "" { + err := p.GenerateChart(path) + if err != nil { + return err + } + } + + settings := env.New() + actionConfig := new(action.Configuration) + println(".......... here ............") + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), "memory", nil); err != nil { + log.Fatal(err) + } + println(settings.EnvVars()) + client := action.NewInstall(actionConfig) + println("Original chart version:", client.Version) + client.Version = ">0.0.0-0" + + name, chart, err := client.NameAndChart([]string{n, path}) + if err != nil { + return nil + } + + client.ReleaseName = name + client.Namespace = settings.Namespace() + cp, err := client.ChartPathOptions.LocateChart(chart, settings) + if err != nil { + return err + } + println("CHART PATH: ", cp) + + chartRequested, err := chartloader.Load(cp) + if err != nil { + return err + } + kclient := k.New(settings.RESTClientGetter()) + println(kclient.Namespace) + if err = actionConfig.KubeClient.IsReachable(); err != nil { + println("Kube API is not reachable") + return err + } + println("....Running.....") + println("Chart description: ", chartRequested.Metadata.Description) + release, err := client.Run(chartRequested, map[string]interface{}{}) + + println(release.Name) + return err } diff --git a/compose/internal/helm/action.go b/compose/internal/helm/action.go deleted file mode 100644 index e500abba8..000000000 --- a/compose/internal/helm/action.go +++ /dev/null @@ -1,34 +0,0 @@ -package helm - -import ( - "os" - "sync" - - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -type KubeConfig struct { - namespace string - config genericclioptions.RESTClientGetter - configOnce sync.Once - - // KubeConfig is the path to the kubeconfig file - KubeConfig string - // KubeContext is the name of the kubeconfig context. - KubeContext string - // Bearer KubeToken used for authentication - KubeToken string - // Kubernetes API Server Endpoint for authentication - KubeAPIServer string -} - -func New() *KubeConfig { - - env := KubeConfig{ - namespace: "", - KubeContext: os.Getenv("COMPOSE_KUBECONTEXT"), - KubeToken: os.Getenv("COMPOSE_KUBETOKEN"), - KubeAPIServer: os.Getenv("COMPOSE_KUBEAPISERVER"), - } - return &env -} diff --git a/compose/internal/helm/output.go b/compose/internal/helm/output.go index edf7cdb1e..6c37e2685 100644 --- a/compose/internal/helm/output.go +++ b/compose/internal/helm/output.go @@ -37,7 +37,7 @@ home: Name string } var chartData bytes.Buffer - _ = t.Execute(&chartData, ChartDetails{project}) + _ = t.Execute(&chartData, ChartDetails{Name: project}) if err := out.Write("Chart.yaml", chartData.Bytes()); err != nil { return err From 896b9fd47e502034b69110644f82180db264189e Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 6 Apr 2020 12:22:52 +0200 Subject: [PATCH 09/24] add uninstall cmd and restructure compose pkg Signed-off-by: aiordache --- compose/compose.go | 128 ++++-------------- compose/internal/helm/chart.go | 70 ++++++++++ compose/internal/helm/helm.go | 39 ++++++ compose/internal/{convert => kube}/kube.go | 2 +- .../internal/{convert => kube}/placement.go | 2 +- .../{convert => kube}/placement_test.go | 2 +- compose/internal/{convert => kube}/pod.go | 2 +- .../internal/{convert => kube}/pod_test.go | 2 +- compose/internal/{convert => kube}/volumes.go | 2 +- compose/internal/project.go | 112 +++++++++++++++ compose/internal/utils/config.go | 4 +- compose/internal/utils/env.go | 16 +-- 12 files changed, 259 insertions(+), 122 deletions(-) create mode 100644 compose/internal/helm/chart.go create mode 100644 compose/internal/helm/helm.go rename compose/internal/{convert => kube}/kube.go (99%) rename compose/internal/{convert => kube}/placement.go (99%) rename compose/internal/{convert => kube}/placement_test.go (99%) rename compose/internal/{convert => kube}/pod.go (99%) rename compose/internal/{convert => kube}/pod_test.go (99%) rename compose/internal/{convert => kube}/volumes.go (99%) create mode 100644 compose/internal/project.go diff --git a/compose/compose.go b/compose/compose.go index d7a40d781..2f4f5fef1 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -1,28 +1,9 @@ package compose import ( - "log" + "os" - chartloader "helm.sh/helm/v3/pkg/chart/loader" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose/internal/convert" - "github.com/docker/helm-prototype/pkg/compose/internal/helm" - utils "github.com/docker/helm-prototype/pkg/compose/internal/utils" - "helm.sh/helm/v3/pkg/action" - env "helm.sh/helm/v3/pkg/cli" - k "helm.sh/helm/v3/pkg/kube" -) - -// Orchestrator is "kubernetes" or "swarm" -type Orchestrator string - -const ( - // Kubernetes specifies to use kubernetes. - Kubernetes Orchestrator = "kubernetes" - // Swarm specifies to use Docker swarm. - Swarm Orchestrator = "swarm" + internal "github.com/docker/helm-prototype/pkg/compose/internal" ) type ProjectOptions struct { @@ -30,107 +11,48 @@ type ProjectOptions struct { Name string } -type Project struct { - Config *types.Config - ProjectDir string - Name string `yaml:"-" json:"-"` - Orchestrator Orchestrator -} +var Settings = internal.GetDefault() -func NewProject(config types.ConfigDetails, name string) (*Project, error) { - model, err := loader.Load(config) - if err != nil { - return nil, err - } - - p := Project{ - Config: model, - ProjectDir: config.WorkingDir, - Name: name, - } - return &p, nil +type ComposeAPI struct { + project *internal.Project } // projectFromOptions load a compose project based on command line options -func ProjectFromOptions(options *ProjectOptions) (*Project, error) { +func ProjectFromOptions(options *ProjectOptions) (*ComposeAPI, error) { + if options == nil { + options = &ProjectOptions{ + ConfigPaths: []string{}, + Name: "docker-compose", + } + } + if options.Name == "" { options.Name = "docker-compose" } - workingDir, configs, err := utils.GetConfigs( - options.Name, - options.ConfigPaths, - ) + project, err := internal.GetProject(options.Name, options.ConfigPaths) if err != nil { return nil, err } - return NewProject(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: configs, - Environment: utils.Environment(), - }, options.Name) + return &ComposeAPI{project: project}, nil } -func (p *Project) GenerateChart(path string) error { - objects, err := convert.MapToKubernetesObjects(p.Config, p.Name) - if err != nil { - return err - } - err = helm.Write(p.Name, objects, path) - if err != nil { - return err - } - return nil +func (c *ComposeAPI) GenerateChart(dirname string) error { + return c.project.ExportToCharts(dirname) } -func (p *Project) InstallChart(n, path string) error { - +func (c *ComposeAPI) Install(name, path string) error { if path == "" { - err := p.GenerateChart(path) + cwd, err := os.Getwd() if err != nil { return err } + path = cwd } - - settings := env.New() - actionConfig := new(action.Configuration) - println(".......... here ............") - if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), "memory", nil); err != nil { - log.Fatal(err) - } - println(settings.EnvVars()) - client := action.NewInstall(actionConfig) - println("Original chart version:", client.Version) - client.Version = ">0.0.0-0" - - name, chart, err := client.NameAndChart([]string{n, path}) - if err != nil { - return nil - } - - client.ReleaseName = name - client.Namespace = settings.Namespace() - cp, err := client.ChartPathOptions.LocateChart(chart, settings) - if err != nil { - return err - } - println("CHART PATH: ", cp) - - chartRequested, err := chartloader.Load(cp) - if err != nil { - return err - } - kclient := k.New(settings.RESTClientGetter()) - println(kclient.Namespace) - if err = actionConfig.KubeClient.IsReachable(); err != nil { - println("Kube API is not reachable") - return err - } - println("....Running.....") - println("Chart description: ", chartRequested.Metadata.Description) - release, err := client.Run(chartRequested, map[string]interface{}{}) - - println(release.Name) - return err + return c.project.Install(name, path) +} + +func (c *ComposeAPI) Uninstall(name string) error { + return c.project.Uninstall(name) } diff --git a/compose/internal/helm/chart.go b/compose/internal/helm/chart.go new file mode 100644 index 000000000..36f7e4487 --- /dev/null +++ b/compose/internal/helm/chart.go @@ -0,0 +1,70 @@ +package helm + +import ( + "gopkg.in/yaml.v2" + action "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + loader "helm.sh/helm/v3/pkg/chart/loader" +) + +type HelmChart struct { + chart *chart.Chart + actionConfig *HelmConfig + Path string + Name string +} + +func NewChart(name, chartpath string) *HelmChart { + chart, err := loader.Load(chartpath) + if err != nil { + return nil + } + return &HelmChart{ + chart: chart, + Path: chartpath, + Name: name, + } +} +func (chart *HelmChart) SetActionConfig(config *HelmConfig) error { + chart.actionConfig = config + return nil +} + +func (chart *HelmChart) Validate() error { + _, err := yaml.Marshal(chart.chart.Metadata) + if err != nil { + return nil + } + + return nil +} + +func (chart *HelmChart) Install() error { + err := chart.actionConfig.InitKubeClient() + if err != nil { + return err + } + + actInstall := action.NewInstall(chart.actionConfig.Config) + actInstall.ReleaseName = chart.Name + actInstall.Namespace = chart.actionConfig.Settings.Namespace() + + release, err := actInstall.Run(chart.chart, map[string]interface{}{}) + if err != nil { + return err + } + + println("Release status: ", release.Info.Status) + println("Release description: ", release.Info.Description) + return chart.actionConfig.Config.Releases.Update(release) +} + +func (chart *HelmChart) Uninstall() error { + err := chart.actionConfig.InitKubeClient() + if err != nil { + return err + } + actUninstall := action.NewUninstall(chart.actionConfig.Config) + _, err = actUninstall.Run(chart.Name) + return err +} diff --git a/compose/internal/helm/helm.go b/compose/internal/helm/helm.go new file mode 100644 index 000000000..b73a81ec5 --- /dev/null +++ b/compose/internal/helm/helm.go @@ -0,0 +1,39 @@ +package helm + +import ( + "log" + + action "helm.sh/helm/v3/pkg/action" + env "helm.sh/helm/v3/pkg/cli" +) + +type HelmConfig struct { + Config *action.Configuration + Settings *env.EnvSettings + kube_conn_init bool +} + +func NewHelmConfig(settings *env.EnvSettings) *HelmConfig { + if settings == nil { + settings = env.New() + } + return &HelmConfig{ + Config: new(action.Configuration), + Settings: settings, + kube_conn_init: false, + } +} + +func (hc *HelmConfig) InitKubeClient() error { + if hc.kube_conn_init { + return nil + } + if err := hc.Config.Init(hc.Settings.RESTClientGetter(), hc.Settings.Namespace(), "memory", log.Printf); err != nil { + log.Fatal(err) + } + if err := hc.Config.KubeClient.IsReachable(); err != nil { + return err + } + hc.kube_conn_init = true + return nil +} diff --git a/compose/internal/convert/kube.go b/compose/internal/kube/kube.go similarity index 99% rename from compose/internal/convert/kube.go rename to compose/internal/kube/kube.go index 884d56935..75d44842a 100644 --- a/compose/internal/convert/kube.go +++ b/compose/internal/kube/kube.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "fmt" diff --git a/compose/internal/convert/placement.go b/compose/internal/kube/placement.go similarity index 99% rename from compose/internal/convert/placement.go rename to compose/internal/kube/placement.go index c757e0a5e..cb56ca7ce 100644 --- a/compose/internal/convert/placement.go +++ b/compose/internal/kube/placement.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "regexp" diff --git a/compose/internal/convert/placement_test.go b/compose/internal/kube/placement_test.go similarity index 99% rename from compose/internal/convert/placement_test.go rename to compose/internal/kube/placement_test.go index 1169d3c85..77d3d6db5 100644 --- a/compose/internal/convert/placement_test.go +++ b/compose/internal/kube/placement_test.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "reflect" diff --git a/compose/internal/convert/pod.go b/compose/internal/kube/pod.go similarity index 99% rename from compose/internal/convert/pod.go rename to compose/internal/kube/pod.go index 2842bba54..e0ef2614e 100644 --- a/compose/internal/convert/pod.go +++ b/compose/internal/kube/pod.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "fmt" diff --git a/compose/internal/convert/pod_test.go b/compose/internal/kube/pod_test.go similarity index 99% rename from compose/internal/convert/pod_test.go rename to compose/internal/kube/pod_test.go index 078a8f839..7d9cc1b67 100644 --- a/compose/internal/convert/pod_test.go +++ b/compose/internal/kube/pod_test.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "fmt" diff --git a/compose/internal/convert/volumes.go b/compose/internal/kube/volumes.go similarity index 99% rename from compose/internal/convert/volumes.go rename to compose/internal/kube/volumes.go index bcc38eb3c..b814280f9 100644 --- a/compose/internal/convert/volumes.go +++ b/compose/internal/kube/volumes.go @@ -1,4 +1,4 @@ -package convert +package kube import ( "fmt" diff --git a/compose/internal/project.go b/compose/internal/project.go new file mode 100644 index 000000000..f35515482 --- /dev/null +++ b/compose/internal/project.go @@ -0,0 +1,112 @@ +package project + +import ( + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose/internal/helm" + "github.com/docker/helm-prototype/pkg/compose/internal/utils" + + "github.com/docker/helm-prototype/pkg/compose/internal/kube" +) + +// Kind is "kubernetes" or "docker" +type Kind string + +const ( + // Kubernetes specifies to use a kubernetes cluster. + Kubernetes Kind = "kubernetes" + // Docker specifies to use Docker engine. + DockerEngine Kind = "docker" +) + +type Engine struct { + Namespace string + + Kind Kind + + Config string + // Context is the name of the kubeconfig/docker context. + Context string + // Token used for authentication (kubernetes) + Token string + // Kubernetes API Server Endpoint for authentication + APIServer string +} + +func GetDefault() *Engine { + return &Engine{Kind: Kubernetes} +} + +type Project struct { + Config *types.Config + HelmConfig *helm.HelmConfig + HelmChart *helm.HelmChart + ProjectDir string + Name string `yaml:"-" json:"-"` +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: model, + HelmConfig: helm.NewHelmConfig(nil), + HelmChart: nil, + ProjectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} + +func GetProject(name string, configPaths []string) (*Project, error) { + if name == "" { + name = "docker-compose" + } + + workingDir, configs, err := utils.GetConfigs( + name, + configPaths, + ) + if err != nil { + return nil, err + } + + return NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + }, name) + +} + +func (p *Project) ExportToCharts(path string) error { + objects, err := kube.MapToKubernetesObjects(p.Config, p.Name) + if err != nil { + return err + } + err = helm.Write(p.Name, objects, path) + if err != nil { + return err + } + return nil +} + +func (p *Project) Install(name, path string) error { + if p.HelmChart == nil { + chart := helm.NewChart(name, path) + chart.SetActionConfig(p.HelmConfig) + p.HelmChart = chart + } + return p.HelmChart.Install() +} + +func (p *Project) Uninstall(name string) error { + if p.HelmChart == nil { + p.HelmChart = helm.NewChart(name, "") + p.HelmChart.SetActionConfig(p.HelmConfig) + } + return p.HelmChart.Uninstall() +} diff --git a/compose/internal/utils/config.go b/compose/internal/utils/config.go index 12974553b..93baa91da 100644 --- a/compose/internal/utils/config.go +++ b/compose/internal/utils/config.go @@ -20,13 +20,11 @@ func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, if err != nil { return "", nil, err } + workingDir := filepath.Dir(configPath[0]) if name == "" { name = os.Getenv("COMPOSE_PROJECT_NAME") } - - workingDir := filepath.Dir(configPath[0]) - if name == "" { r := regexp.MustCompile(`[^a-z0-9\\-_]+`) name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") diff --git a/compose/internal/utils/env.go b/compose/internal/utils/env.go index 3044742bb..e7673fef0 100644 --- a/compose/internal/utils/env.go +++ b/compose/internal/utils/env.go @@ -6,15 +6,11 @@ import ( ) func Environment() map[string]string { - return getAsEqualsMap(os.Environ()) -} - -// getAsEqualsMap split key=value formatted strings into a key : value map -func getAsEqualsMap(em []string) map[string]string { - m := make(map[string]string) - for _, v := range em { - kv := strings.SplitN(v, "=", 2) - m[kv[0]] = kv[1] + vars := make(map[string]string) + env := os.Environ() + for _, v := range env { + k := strings.SplitN(v, "=", 2) + vars[k[0]] = k[1] } - return m + return vars } From 814d4ca832f1f096fef1a9ebd6f18ce3010d8b4c Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 6 Apr 2020 17:49:57 +0200 Subject: [PATCH 10/24] fixed uninstall cmd Signed-off-by: aiordache --- compose/internal/helm/chart.go | 70 ---------------------------------- compose/internal/helm/helm.go | 68 ++++++++++++++++++++++++++++++--- compose/internal/project.go | 20 +++------- 3 files changed, 68 insertions(+), 90 deletions(-) delete mode 100644 compose/internal/helm/chart.go diff --git a/compose/internal/helm/chart.go b/compose/internal/helm/chart.go deleted file mode 100644 index 36f7e4487..000000000 --- a/compose/internal/helm/chart.go +++ /dev/null @@ -1,70 +0,0 @@ -package helm - -import ( - "gopkg.in/yaml.v2" - action "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - loader "helm.sh/helm/v3/pkg/chart/loader" -) - -type HelmChart struct { - chart *chart.Chart - actionConfig *HelmConfig - Path string - Name string -} - -func NewChart(name, chartpath string) *HelmChart { - chart, err := loader.Load(chartpath) - if err != nil { - return nil - } - return &HelmChart{ - chart: chart, - Path: chartpath, - Name: name, - } -} -func (chart *HelmChart) SetActionConfig(config *HelmConfig) error { - chart.actionConfig = config - return nil -} - -func (chart *HelmChart) Validate() error { - _, err := yaml.Marshal(chart.chart.Metadata) - if err != nil { - return nil - } - - return nil -} - -func (chart *HelmChart) Install() error { - err := chart.actionConfig.InitKubeClient() - if err != nil { - return err - } - - actInstall := action.NewInstall(chart.actionConfig.Config) - actInstall.ReleaseName = chart.Name - actInstall.Namespace = chart.actionConfig.Settings.Namespace() - - release, err := actInstall.Run(chart.chart, map[string]interface{}{}) - if err != nil { - return err - } - - println("Release status: ", release.Info.Status) - println("Release description: ", release.Info.Description) - return chart.actionConfig.Config.Releases.Update(release) -} - -func (chart *HelmChart) Uninstall() error { - err := chart.actionConfig.InitKubeClient() - if err != nil { - return err - } - actUninstall := action.NewUninstall(chart.actionConfig.Config) - _, err = actUninstall.Run(chart.Name) - return err -} diff --git a/compose/internal/helm/helm.go b/compose/internal/helm/helm.go index b73a81ec5..277a2c85f 100644 --- a/compose/internal/helm/helm.go +++ b/compose/internal/helm/helm.go @@ -1,34 +1,43 @@ package helm import ( + "errors" "log" + "os" action "helm.sh/helm/v3/pkg/action" + loader "helm.sh/helm/v3/pkg/chart/loader" env "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" ) -type HelmConfig struct { +type HelmActions struct { Config *action.Configuration Settings *env.EnvSettings kube_conn_init bool } -func NewHelmConfig(settings *env.EnvSettings) *HelmConfig { +func NewHelmActions(settings *env.EnvSettings) *HelmActions { if settings == nil { settings = env.New() } - return &HelmConfig{ + return &HelmActions{ Config: new(action.Configuration), Settings: settings, kube_conn_init: false, } } -func (hc *HelmConfig) InitKubeClient() error { +func (hc *HelmActions) InitKubeClient() error { if hc.kube_conn_init { return nil } - if err := hc.Config.Init(hc.Settings.RESTClientGetter(), hc.Settings.Namespace(), "memory", log.Printf); err != nil { + if err := hc.Config.Init( + hc.Settings.RESTClientGetter(), + hc.Settings.Namespace(), + "configmap", + log.Printf, + ); err != nil { log.Fatal(err) } if err := hc.Config.KubeClient.IsReachable(); err != nil { @@ -37,3 +46,52 @@ func (hc *HelmConfig) InitKubeClient() error { hc.kube_conn_init = true return nil } + +func (hc *HelmActions) Install(name, chartpath string) error { + hc.InitKubeClient() + + if chartpath == "" { + cwd, err := os.Getwd() + if err != nil { + return nil + } + chartpath = cwd + } + chart, err := loader.Load(chartpath) + if err != nil { + return nil + } + actInstall := action.NewInstall(hc.Config) + actInstall.ReleaseName = name + actInstall.Namespace = hc.Settings.Namespace() + + release, err := actInstall.Run(chart, map[string]interface{}{}) + if err != nil { + return err + } + + println("Release status: ", release.Info.Status) + println("Release description: ", release.Info.Description) + return hc.Config.Releases.Update(release) +} + +func (hc *HelmActions) Uninstall(name string) error { + hc.InitKubeClient() + release, err := hc.Get(name) + if err != nil { + return err + } + if release == nil { + return errors.New("No release found with the name provided.") + } + actUninstall := action.NewUninstall(hc.Config) + _, err = actUninstall.Run(name) + return err +} + +func (hc *HelmActions) Get(name string) (*release.Release, error) { + hc.InitKubeClient() + + actGet := action.NewGet(hc.Config) + return actGet.Run(name) +} diff --git a/compose/internal/project.go b/compose/internal/project.go index f35515482..314a7b341 100644 --- a/compose/internal/project.go +++ b/compose/internal/project.go @@ -39,8 +39,7 @@ func GetDefault() *Engine { type Project struct { Config *types.Config - HelmConfig *helm.HelmConfig - HelmChart *helm.HelmChart + Helm *helm.HelmActions ProjectDir string Name string `yaml:"-" json:"-"` } @@ -53,8 +52,7 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) { p := Project{ Config: model, - HelmConfig: helm.NewHelmConfig(nil), - HelmChart: nil, + Helm: helm.NewHelmActions(nil), ProjectDir: config.WorkingDir, Name: name, } @@ -95,18 +93,10 @@ func (p *Project) ExportToCharts(path string) error { } func (p *Project) Install(name, path string) error { - if p.HelmChart == nil { - chart := helm.NewChart(name, path) - chart.SetActionConfig(p.HelmConfig) - p.HelmChart = chart - } - return p.HelmChart.Install() + return p.Helm.Install(name, path) } func (p *Project) Uninstall(name string) error { - if p.HelmChart == nil { - p.HelmChart = helm.NewChart(name, "") - p.HelmChart.SetActionConfig(p.HelmConfig) - } - return p.HelmChart.Uninstall() + + return p.Helm.Uninstall(name) } From facb86fab2a258b480ca2a1b001db02ba686d00b Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 6 Apr 2020 20:28:18 +0200 Subject: [PATCH 11/24] another package restructuring Signed-off-by: aiordache --- compose/compose.go | 64 ++++++++++++--------- compose/internal/env.go | 70 +++++++++++++++++++++++ compose/internal/helm/helm.go | 15 +++-- compose/internal/project.go | 102 ---------------------------------- compose/internal/utils/env.go | 16 ------ 5 files changed, 116 insertions(+), 151 deletions(-) create mode 100644 compose/internal/env.go delete mode 100644 compose/internal/project.go delete mode 100644 compose/internal/utils/env.go diff --git a/compose/compose.go b/compose/compose.go index 2f4f5fef1..ee51fd97d 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -3,46 +3,56 @@ package compose import ( "os" + "github.com/compose-spec/compose-go/types" internal "github.com/docker/helm-prototype/pkg/compose/internal" + "github.com/docker/helm-prototype/pkg/compose/internal/helm" + "github.com/docker/helm-prototype/pkg/compose/internal/kube" ) -type ProjectOptions struct { - ConfigPaths []string - Name string -} - var Settings = internal.GetDefault() -type ComposeAPI struct { - project *internal.Project +type ComposeProject struct { + config *types.Config + helm *helm.HelmActions + ProjectDir string + Name string `yaml:"-" json:"-"` } -// projectFromOptions load a compose project based on command line options -func ProjectFromOptions(options *ProjectOptions) (*ComposeAPI, error) { - if options == nil { - options = &ProjectOptions{ - ConfigPaths: []string{}, - Name: "docker-compose", - } - } +type ComposeResult struct { + Info string + Status string + Descriptin string +} - if options.Name == "" { - options.Name = "docker-compose" +func Load(name string, configpaths []string) (*ComposeProject, error) { + if name == "" { + name = "docker-compose" } - - project, err := internal.GetProject(options.Name, options.ConfigPaths) + model, workingDir, err := internal.GetConfig(name, configpaths) if err != nil { return nil, err } - - return &ComposeAPI{project: project}, nil + return &ComposeProject{ + config: model, + helm: helm.NewHelmActions(nil), + ProjectDir: workingDir, + Name: name, + }, nil } -func (c *ComposeAPI) GenerateChart(dirname string) error { - return c.project.ExportToCharts(dirname) +func (cp *ComposeProject) GenerateChart(dirname string) error { + objects, err := kube.MapToKubernetesObjects(cp.config, cp.Name) + if err != nil { + return err + } + err = helm.Write(cp.Name, objects, dirname) + if err != nil { + return err + } + return nil } -func (c *ComposeAPI) Install(name, path string) error { +func (cp *ComposeProject) Install(name, path string) error { if path == "" { cwd, err := os.Getwd() if err != nil { @@ -50,9 +60,9 @@ func (c *ComposeAPI) Install(name, path string) error { } path = cwd } - return c.project.Install(name, path) + return cp.helm.Install(name, path) } -func (c *ComposeAPI) Uninstall(name string) error { - return c.project.Uninstall(name) +func (cp *ComposeProject) Uninstall(name string) error { + return cp.helm.Uninstall(name) } diff --git a/compose/internal/env.go b/compose/internal/env.go new file mode 100644 index 000000000..4b571c80d --- /dev/null +++ b/compose/internal/env.go @@ -0,0 +1,70 @@ +package env + +import ( + "os" + "strings" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose/internal/utils" +) + +// Kind is "kubernetes" or "docker" +type Kind string + +const ( + // Kubernetes specifies to use a kubernetes cluster. + Kubernetes Kind = "kubernetes" + // Docker specifies to use Docker engine. + DockerEngine Kind = "docker" +) + +type Engine struct { + Namespace string + + Kind Kind + + Config string + // Context is the name of the kubeconfig/docker context. + Context string + // Token used for authentication (kubernetes) + Token string + // Kubernetes API Server Endpoint for authentication + APIServer string +} + +func GetDefault() *Engine { + return &Engine{Kind: Kubernetes} +} + +func Environment() map[string]string { + vars := make(map[string]string) + env := os.Environ() + for _, v := range env { + k := strings.SplitN(v, "=", 2) + vars[k[0]] = k[1] + } + return vars +} + +func GetConfig(name string, configPaths []string) (*types.Config, string, error) { + if name == "" { + name = "docker-compose" + } + workingDir, configs, err := utils.GetConfigs( + name, + configPaths, + ) + if err != nil { + return nil, "", err + } + config, err := loader.Load(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: Environment(), + }) + if err != nil { + return nil, "", err + } + return config, workingDir, nil +} diff --git a/compose/internal/helm/helm.go b/compose/internal/helm/helm.go index 277a2c85f..4d453f48c 100644 --- a/compose/internal/helm/helm.go +++ b/compose/internal/helm/helm.go @@ -69,10 +69,9 @@ func (hc *HelmActions) Install(name, chartpath string) error { if err != nil { return err } - - println("Release status: ", release.Info.Status) - println("Release description: ", release.Info.Description) - return hc.Config.Releases.Update(release) + log.Println("Release status: ", release.Info.Status) + log.Println(release.Info.Description) + return nil } func (hc *HelmActions) Uninstall(name string) error { @@ -85,8 +84,12 @@ func (hc *HelmActions) Uninstall(name string) error { return errors.New("No release found with the name provided.") } actUninstall := action.NewUninstall(hc.Config) - _, err = actUninstall.Run(name) - return err + response, err := actUninstall.Run(name) + if err != nil { + return err + } + log.Println(response.Release.Info.Description) + return nil } func (hc *HelmActions) Get(name string) (*release.Release, error) { diff --git a/compose/internal/project.go b/compose/internal/project.go deleted file mode 100644 index 314a7b341..000000000 --- a/compose/internal/project.go +++ /dev/null @@ -1,102 +0,0 @@ -package project - -import ( - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose/internal/helm" - "github.com/docker/helm-prototype/pkg/compose/internal/utils" - - "github.com/docker/helm-prototype/pkg/compose/internal/kube" -) - -// Kind is "kubernetes" or "docker" -type Kind string - -const ( - // Kubernetes specifies to use a kubernetes cluster. - Kubernetes Kind = "kubernetes" - // Docker specifies to use Docker engine. - DockerEngine Kind = "docker" -) - -type Engine struct { - Namespace string - - Kind Kind - - Config string - // Context is the name of the kubeconfig/docker context. - Context string - // Token used for authentication (kubernetes) - Token string - // Kubernetes API Server Endpoint for authentication - APIServer string -} - -func GetDefault() *Engine { - return &Engine{Kind: Kubernetes} -} - -type Project struct { - Config *types.Config - Helm *helm.HelmActions - ProjectDir string - Name string `yaml:"-" json:"-"` -} - -func NewProject(config types.ConfigDetails, name string) (*Project, error) { - model, err := loader.Load(config) - if err != nil { - return nil, err - } - - p := Project{ - Config: model, - Helm: helm.NewHelmActions(nil), - ProjectDir: config.WorkingDir, - Name: name, - } - return &p, nil -} - -func GetProject(name string, configPaths []string) (*Project, error) { - if name == "" { - name = "docker-compose" - } - - workingDir, configs, err := utils.GetConfigs( - name, - configPaths, - ) - if err != nil { - return nil, err - } - - return NewProject(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: configs, - Environment: utils.Environment(), - }, name) - -} - -func (p *Project) ExportToCharts(path string) error { - objects, err := kube.MapToKubernetesObjects(p.Config, p.Name) - if err != nil { - return err - } - err = helm.Write(p.Name, objects, path) - if err != nil { - return err - } - return nil -} - -func (p *Project) Install(name, path string) error { - return p.Helm.Install(name, path) -} - -func (p *Project) Uninstall(name string) error { - - return p.Helm.Uninstall(name) -} diff --git a/compose/internal/utils/env.go b/compose/internal/utils/env.go deleted file mode 100644 index e7673fef0..000000000 --- a/compose/internal/utils/env.go +++ /dev/null @@ -1,16 +0,0 @@ -package utils - -import ( - "os" - "strings" -) - -func Environment() map[string]string { - vars := make(map[string]string) - env := os.Environ() - for _, v := range env { - k := strings.SplitN(v, "=", 2) - vars[k[0]] = k[1] - } - return vars -} From 836732ed373c51562443db2e35ae52587331360a Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 6 Apr 2020 22:05:18 +0200 Subject: [PATCH 12/24] save chart structure Signed-off-by: aiordache --- compose/compose.go | 35 ++++++------ compose/internal/env.go | 21 +++++++ compose/internal/helm/{output.go => chart.go} | 56 ++++++++----------- compose/internal/helm/helm.go | 27 ++++----- 4 files changed, 75 insertions(+), 64 deletions(-) rename compose/internal/helm/{output.go => chart.go} (60%) diff --git a/compose/compose.go b/compose/compose.go index ee51fd97d..6f820c97a 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -1,12 +1,12 @@ package compose import ( - "os" + "path/filepath" + "strings" "github.com/compose-spec/compose-go/types" internal "github.com/docker/helm-prototype/pkg/compose/internal" "github.com/docker/helm-prototype/pkg/compose/internal/helm" - "github.com/docker/helm-prototype/pkg/compose/internal/kube" ) var Settings = internal.GetDefault() @@ -41,26 +41,27 @@ func Load(name string, configpaths []string) (*ComposeProject, error) { } func (cp *ComposeProject) GenerateChart(dirname string) error { - objects, err := kube.MapToKubernetesObjects(cp.config, cp.Name) - if err != nil { - return err + if dirname == "" { + dirname = cp.config.Filename + if strings.Contains(dirname, ".") { + splits := strings.SplitN(dirname, ".", 2) + dirname = splits[0] + } } - err = helm.Write(cp.Name, objects, dirname) - if err != nil { - return err - } - return nil + name := filepath.Base(dirname) + dirname = filepath.Dir(dirname) + return internal.SaveChart(cp.config, name, dirname) } func (cp *ComposeProject) Install(name, path string) error { - if path == "" { - cwd, err := os.Getwd() - if err != nil { - return err - } - path = cwd + if path != "" { + return cp.helm.InstallChartFromDir(name, path) } - return cp.helm.Install(name, path) + chart, err := internal.GetChartInMemory(cp.config, name) + if err != nil { + return err + } + return cp.helm.InstallChart(name, chart) } func (cp *ComposeProject) Uninstall(name string) error { diff --git a/compose/internal/env.go b/compose/internal/env.go index 4b571c80d..d16f6eac3 100644 --- a/compose/internal/env.go +++ b/compose/internal/env.go @@ -6,7 +6,11 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" + "github.com/docker/helm-prototype/pkg/compose/internal/helm" + "github.com/docker/helm-prototype/pkg/compose/internal/kube" "github.com/docker/helm-prototype/pkg/compose/internal/utils" + chart "helm.sh/helm/v3/pkg/chart" + util "helm.sh/helm/v3/pkg/chartutil" ) // Kind is "kubernetes" or "docker" @@ -68,3 +72,20 @@ func GetConfig(name string, configPaths []string) (*types.Config, string, error) } return config, workingDir, nil } + +func GetChartInMemory(config *types.Config, name string) (*chart.Chart, error) { + objects, err := kube.MapToKubernetesObjects(config, name) + if err != nil { + return nil, err + } + //in memory files + return helm.ConvertToChart(name, objects) +} + +func SaveChart(config *types.Config, name, dest string) error { + chart, err := GetChartInMemory(config, name) + if err != nil { + return err + } + return util.SaveDir(chart, dest) +} diff --git a/compose/internal/helm/output.go b/compose/internal/helm/chart.go similarity index 60% rename from compose/internal/helm/output.go rename to compose/internal/helm/chart.go index 6c37e2685..f4e86d82e 100644 --- a/compose/internal/helm/output.go +++ b/compose/internal/helm/chart.go @@ -4,20 +4,22 @@ import ( "bytes" "encoding/json" "html/template" - "io/ioutil" - "os" "path/filepath" "gopkg.in/yaml.v3" + + chart "helm.sh/helm/v3/pkg/chart" + loader "helm.sh/helm/v3/pkg/chart/loader" "k8s.io/apimachinery/pkg/runtime" ) -func Write(project string, objects map[string]runtime.Object, target string) error { - out := Outputer{target} +func ConvertToChart(name string, objects map[string]runtime.Object) (*chart.Chart, error) { - if err := out.Write("README.md", []byte("This chart was created by converting a Compose file")); err != nil { - return err - } + files := []*loader.BufferedFile{ + &loader.BufferedFile{ + Name: "README.md", + Data: []byte("This chart was created by converting a Compose file"), + }} chart := `name: {{.Name}} description: A generated Helm Chart for {{.Name}} from Skippbox Kompose @@ -31,42 +33,35 @@ home: t, err := template.New("ChartTmpl").Parse(chart) if err != nil { - return err + return nil, err } type ChartDetails struct { Name string } var chartData bytes.Buffer - _ = t.Execute(&chartData, ChartDetails{Name: project}) + _ = t.Execute(&chartData, ChartDetails{Name: name}) - if err := out.Write("Chart.yaml", chartData.Bytes()); err != nil { - return err - } + files = append(files, &loader.BufferedFile{ + Name: "Chart.yaml", + Data: chartData.Bytes(), + }) for name, o := range objects { j, err := json.Marshal(o) if err != nil { - return err + return nil, err } - b, err := jsonToYaml(j, 2) + buf, err := jsonToYaml(j, 2) if err != nil { - return err - } - if err := out.Write(filepath.Join("templates", name), b); err != nil { - return err + return nil, err } + files = append(files, &loader.BufferedFile{ + Name: filepath.Join("templates", name), + Data: buf, + }) + } - return nil -} - -type Outputer struct { - Dir string -} - -func (o Outputer) Write(path string, content []byte) error { - out := filepath.Join(o.Dir, path) - os.MkdirAll(filepath.Dir(out), 0744) - return ioutil.WriteFile(out, content, 0644) + return loader.LoadFiles(files) } // Convert JSON to YAML. @@ -90,7 +85,4 @@ func jsonToYaml(j []byte, spaces int) ([]byte, error) { return nil, err } return b.Bytes(), nil - - // Marshal this object into YAML. - // return yaml.Marshal(jsonObj) } diff --git a/compose/internal/helm/helm.go b/compose/internal/helm/helm.go index 4d453f48c..288e34884 100644 --- a/compose/internal/helm/helm.go +++ b/compose/internal/helm/helm.go @@ -3,9 +3,9 @@ package helm import ( "errors" "log" - "os" action "helm.sh/helm/v3/pkg/action" + chart "helm.sh/helm/v3/pkg/chart" loader "helm.sh/helm/v3/pkg/chart/loader" env "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/release" @@ -28,7 +28,7 @@ func NewHelmActions(settings *env.EnvSettings) *HelmActions { } } -func (hc *HelmActions) InitKubeClient() error { +func (hc *HelmActions) initKubeClient() error { if hc.kube_conn_init { return nil } @@ -47,20 +47,17 @@ func (hc *HelmActions) InitKubeClient() error { return nil } -func (hc *HelmActions) Install(name, chartpath string) error { - hc.InitKubeClient() - - if chartpath == "" { - cwd, err := os.Getwd() - if err != nil { - return nil - } - chartpath = cwd - } +func (hc *HelmActions) InstallChartFromDir(name string, chartpath string) error { chart, err := loader.Load(chartpath) if err != nil { - return nil + return err } + return hc.InstallChart(name, chart) +} + +func (hc *HelmActions) InstallChart(name string, chart *chart.Chart) error { + hc.initKubeClient() + actInstall := action.NewInstall(hc.Config) actInstall.ReleaseName = name actInstall.Namespace = hc.Settings.Namespace() @@ -75,7 +72,7 @@ func (hc *HelmActions) Install(name, chartpath string) error { } func (hc *HelmActions) Uninstall(name string) error { - hc.InitKubeClient() + hc.initKubeClient() release, err := hc.Get(name) if err != nil { return err @@ -93,7 +90,7 @@ func (hc *HelmActions) Uninstall(name string) error { } func (hc *HelmActions) Get(name string) (*release.Release, error) { - hc.InitKubeClient() + hc.initKubeClient() actGet := action.NewGet(hc.Config) return actGet.Run(name) From 5f0f72e27db64e90c38de21e3ec6ad325a38157a Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 6 Apr 2020 22:31:37 +0200 Subject: [PATCH 13/24] Add down command Signed-off-by: aiordache --- compose/compose.go | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/compose.go b/compose/compose.go index 6f820c97a..aea0c8505 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -57,6 +57,7 @@ func (cp *ComposeProject) Install(name, path string) error { if path != "" { return cp.helm.InstallChartFromDir(name, path) } + chart, err := internal.GetChartInMemory(cp.config, name) if err != nil { return err From 71469641683520f2db5904aa6ddc71886dedea20 Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 7 Apr 2020 17:04:03 +0200 Subject: [PATCH 14/24] support multi-service compose Signed-off-by: aiordache --- compose/internal/env.go | 7 +++++++ compose/internal/kube/kube.go | 33 ++++++++++++++++++++++++++++---- compose/internal/kube/pod.go | 10 +++++----- compose/internal/kube/volumes.go | 4 ++-- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/compose/internal/env.go b/compose/internal/env.go index d16f6eac3..8397ac0f8 100644 --- a/compose/internal/env.go +++ b/compose/internal/env.go @@ -74,6 +74,13 @@ func GetConfig(name string, configPaths []string) (*types.Config, string, error) } func GetChartInMemory(config *types.Config, name string) (*chart.Chart, error) { + for k, v := range config.Volumes { + volumeName := strings.ReplaceAll(k, "_", "-") + if volumeName != k { + config.Volumes[volumeName] = v + delete(config.Volumes, k) + } + } objects, err := kube.MapToKubernetesObjects(config, name) if err != nil { return nil, err diff --git a/compose/internal/kube/kube.go b/compose/internal/kube/kube.go index 75d44842a..713235157 100644 --- a/compose/internal/kube/kube.go +++ b/compose/internal/kube/kube.go @@ -2,12 +2,14 @@ package kube import ( "fmt" + "log" "strings" "time" "github.com/compose-spec/compose-go/types" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" + resource "k8s.io/apimachinery/pkg/api/resource" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -17,7 +19,13 @@ func MapToKubernetesObjects(model *types.Config, name string) (map[string]runtim objects := map[string]runtime.Object{} for _, service := range model.Services { - objects[fmt.Sprintf("%s-service.yaml", service.Name)] = mapToService(model, service) + svcObject := mapToService(model, service) + if svcObject != nil { + objects[fmt.Sprintf("%s-service.yaml", service.Name)] = svcObject + } else { + log.Println("Missing port mapping from service config.") + } + if service.Deploy != nil && service.Deploy.Mode == "global" { daemonset, err := mapToDaemonset(service, model, name) if err != nil { @@ -33,7 +41,8 @@ func MapToKubernetesObjects(model *types.Config, name string) (map[string]runtim } for _, vol := range service.Volumes { if vol.Type == "volume" { - objects[fmt.Sprintf("%s-persistentvolumeclain.yaml", service.Name)] = mapToPVC(service, vol) + vol.Source = strings.ReplaceAll(vol.Source, "_", "-") + objects[fmt.Sprintf("%s-persistentvolumeclaim.yaml", vol.Source)] = mapToPVC(service, vol) } } } @@ -51,7 +60,9 @@ func mapToService(model *types.Config, service types.ServiceConfig) *core.Servic Protocol: toProtocol(p.Protocol), }) } - + if len(ports) == 0 { + return nil + } return &core.Service{ TypeMeta: meta.TypeMeta{ Kind: "Service", @@ -167,13 +178,27 @@ func toDeploymentStrategy(deploy *types.DeployConfig) apps.DeploymentStrategy { } func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { + rwaccess := core.ReadWriteOnce + if vol.ReadOnly { + rwaccess = core.ReadOnlyMany + } return &core.PersistentVolumeClaim{ + TypeMeta: meta.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, ObjectMeta: meta.ObjectMeta{ Name: vol.Source, Labels: map[string]string{"com.docker.compose.service": service.Name}, }, Spec: core.PersistentVolumeClaimSpec{ - VolumeName: vol.Source, + VolumeName: vol.Source, + AccessModes: []core.PersistentVolumeAccessMode{rwaccess}, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceStorage: resource.MustParse("100Mi"), + }, + }, }, } } diff --git a/compose/internal/kube/pod.go b/compose/internal/kube/pod.go index e0ef2614e..18102a568 100644 --- a/compose/internal/kube/pod.go +++ b/compose/internal/kube/pod.go @@ -18,10 +18,10 @@ import ( func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *types.Config) (apiv1.PodTemplateSpec, error) { tpl := apiv1.PodTemplateSpec{} - nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } + //nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) + //if err != nil { + // return apiv1.PodTemplateSpec{}, err + //} hostAliases, err := toHostAliases(serviceConfig.ExtraHosts) if err != nil { return apiv1.PodTemplateSpec{}, err @@ -73,7 +73,7 @@ func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, tpl.Spec.Hostname = serviceConfig.Hostname tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod) tpl.Spec.HostAliases = hostAliases - tpl.Spec.Affinity = nodeAffinity + //tpl.Spec.Affinity = nodeAffinity // we dont want to remove all containers and recreate them because: // an admission plugin can add sidecar containers // we for sure want to keep the main container to be additive diff --git a/compose/internal/kube/volumes.go b/compose/internal/kube/volumes.go index b814280f9..3646de1e9 100644 --- a/compose/internal/kube/volumes.go +++ b/compose/internal/kube/volumes.go @@ -42,7 +42,7 @@ func toVolumeSpecs(s types.ServiceConfig, model *types.Config) ([]volumeSpec, er source = gitVolume(m.Source) } else if m.Type == "volume" { if m.Source != "" { - name = m.Source + name = strings.ReplaceAll(m.Source, "_", "-") } } else { // bind mount @@ -133,7 +133,7 @@ func toVolumes(s types.ServiceConfig, model *types.Config) ([]apiv1.Volume, erro } for _, spec := range specs { if spec.source == nil { - continue + spec.source = emptyVolumeInMemory() } volumes = append(volumes, apiv1.Volume{ Name: spec.mount.Name, From 17335ae06c1930b3d412dd93b56ef5c26941641f Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 7 Apr 2020 21:34:43 +0200 Subject: [PATCH 15/24] add list command Signed-off-by: aiordache --- compose/compose.go | 10 ++++------ compose/internal/env.go | 29 +++-------------------------- compose/internal/helm/helm.go | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index aea0c8505..77253ae2d 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -18,12 +18,6 @@ type ComposeProject struct { Name string `yaml:"-" json:"-"` } -type ComposeResult struct { - Info string - Status string - Descriptin string -} - func Load(name string, configpaths []string) (*ComposeProject, error) { if name == "" { name = "docker-compose" @@ -68,3 +62,7 @@ func (cp *ComposeProject) Install(name, path string) error { func (cp *ComposeProject) Uninstall(name string) error { return cp.helm.Uninstall(name) } + +func (cp *ComposeProject) List() (map[string]interface{}, error) { + return cp.helm.ListReleases() +} diff --git a/compose/internal/env.go b/compose/internal/env.go index 8397ac0f8..72075138b 100644 --- a/compose/internal/env.go +++ b/compose/internal/env.go @@ -11,34 +11,11 @@ import ( "github.com/docker/helm-prototype/pkg/compose/internal/utils" chart "helm.sh/helm/v3/pkg/chart" util "helm.sh/helm/v3/pkg/chartutil" + helmenv "helm.sh/helm/v3/pkg/cli" ) -// Kind is "kubernetes" or "docker" -type Kind string - -const ( - // Kubernetes specifies to use a kubernetes cluster. - Kubernetes Kind = "kubernetes" - // Docker specifies to use Docker engine. - DockerEngine Kind = "docker" -) - -type Engine struct { - Namespace string - - Kind Kind - - Config string - // Context is the name of the kubeconfig/docker context. - Context string - // Token used for authentication (kubernetes) - Token string - // Kubernetes API Server Endpoint for authentication - APIServer string -} - -func GetDefault() *Engine { - return &Engine{Kind: Kubernetes} +func GetDefault() *helmenv.EnvSettings { + return helmenv.New() } func Environment() map[string]string { diff --git a/compose/internal/helm/helm.go b/compose/internal/helm/helm.go index 288e34884..60cb39df0 100644 --- a/compose/internal/helm/helm.go +++ b/compose/internal/helm/helm.go @@ -95,3 +95,21 @@ func (hc *HelmActions) Get(name string) (*release.Release, error) { actGet := action.NewGet(hc.Config) return actGet.Run(name) } + +func (hc *HelmActions) ListReleases() (map[string]interface{}, error) { + hc.initKubeClient() + + actList := action.NewList(hc.Config) + releases, err := actList.Run() + if err != nil { + return map[string]interface{}{}, err + } + result := map[string]interface{}{} + for _, rel := range releases { + result[rel.Name] = map[string]string{ + "Status": string(rel.Info.Status), + "Description": rel.Info.Description, + } + } + return result, nil +} From 881818223c399daf887317d2c408efc0f76153ab Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 8 Apr 2020 15:39:10 +0200 Subject: [PATCH 16/24] cleanup and small fixes Signed-off-by: aiordache --- compose/internal/helm/chart.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compose/internal/helm/chart.go b/compose/internal/helm/chart.go index f4e86d82e..26a139f4b 100644 --- a/compose/internal/helm/chart.go +++ b/compose/internal/helm/chart.go @@ -39,8 +39,10 @@ home: Name string } var chartData bytes.Buffer - _ = t.Execute(&chartData, ChartDetails{Name: name}) - + err = t.Execute(&chartData, ChartDetails{Name: name}) + if err != nil { + return nil, err + } files = append(files, &loader.BufferedFile{ Name: "Chart.yaml", Data: chartData.Bytes(), From 3bd15bf0d4b89e4a23d04f61610ac5289acb05c3 Mon Sep 17 00:00:00 2001 From: aiordache Date: Fri, 10 Apr 2020 17:08:18 +0200 Subject: [PATCH 17/24] better error display and cleanup Signed-off-by: aiordache --- compose/compose.go | 35 ++++++++++++++++++++++++++++---- compose/internal/env.go | 6 +++--- compose/internal/utils/config.go | 6 ++++-- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/compose/compose.go b/compose/compose.go index 77253ae2d..8c9e47b9f 100644 --- a/compose/compose.go +++ b/compose/compose.go @@ -1,6 +1,7 @@ package compose import ( + "errors" "path/filepath" "strings" @@ -19,13 +20,19 @@ type ComposeProject struct { } func Load(name string, configpaths []string) (*ComposeProject, error) { - if name == "" { - name = "docker-compose" - } model, workingDir, err := internal.GetConfig(name, configpaths) if err != nil { return nil, err } + + if name == "" { + if model != nil { + name = filepath.Base(filepath.Dir(model.Filename)) + } else if workingDir != "" { + name = filepath.Base(filepath.Dir(workingDir)) + } + } + return &ComposeProject{ config: model, helm: helm.NewHelmActions(nil), @@ -35,6 +42,10 @@ func Load(name string, configpaths []string) (*ComposeProject, error) { } func (cp *ComposeProject) GenerateChart(dirname string) error { + if cp.config == nil { + return errors.New(`Can't find a suitable configuration file in this directory or any +parent. Are you in the right directory?`) + } if dirname == "" { dirname = cp.config.Filename if strings.Contains(dirname, ".") { @@ -51,7 +62,13 @@ func (cp *ComposeProject) Install(name, path string) error { if path != "" { return cp.helm.InstallChartFromDir(name, path) } - + if cp.config == nil { + return errors.New(`Can't find a suitable configuration file in this directory or any +parent. Are you in the right directory?`) + } + if name == "" { + name = cp.Name + } chart, err := internal.GetChartInMemory(cp.config, name) if err != nil { return err @@ -60,6 +77,16 @@ func (cp *ComposeProject) Install(name, path string) error { } func (cp *ComposeProject) Uninstall(name string) error { + if name == "" { + if cp.config == nil { + return errors.New(`Can't find a suitable configuration file in this directory or any +parent. Are you in the right directory? + +Alternative: uninstall [INSTALLATION NAME] +`) + } + name = cp.Name + } return cp.helm.Uninstall(name) } diff --git a/compose/internal/env.go b/compose/internal/env.go index 72075138b..141f15b1e 100644 --- a/compose/internal/env.go +++ b/compose/internal/env.go @@ -29,9 +29,6 @@ func Environment() map[string]string { } func GetConfig(name string, configPaths []string) (*types.Config, string, error) { - if name == "" { - name = "docker-compose" - } workingDir, configs, err := utils.GetConfigs( name, configPaths, @@ -39,6 +36,9 @@ func GetConfig(name string, configPaths []string) (*types.Config, string, error) if err != nil { return nil, "", err } + if configs == nil { + return nil, "", nil + } config, err := loader.Load(types.ConfigDetails{ WorkingDir: workingDir, ConfigFiles: configs, diff --git a/compose/internal/utils/config.go b/compose/internal/utils/config.go index 93baa91da..9db651b43 100644 --- a/compose/internal/utils/config.go +++ b/compose/internal/utils/config.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -20,6 +19,9 @@ func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, if err != nil { return "", nil, err } + if configPath == nil { + return "", nil, nil + } workingDir := filepath.Dir(configPath[0]) if name == "" { @@ -88,7 +90,7 @@ func getConfigPaths(configPaths []string) ([]string, error) { } parent := filepath.Dir(pwd) if parent == pwd { - return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") + return nil, nil } pwd = parent } From ef22ce257831792eb80a77c6d882fdd953ed132d Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 20 Jan 2021 15:49:24 +0100 Subject: [PATCH 18/24] Cleanup before migration to compose-cli Signed-off-by: aiordache --- {compose => kube/compose}/compose.go | 0 {compose => kube/compose}/internal/env.go | 0 {compose => kube/compose}/internal/helm/chart.go | 0 {compose => kube/compose}/internal/helm/helm.go | 0 {compose => kube/compose}/internal/kube/kube.go | 0 {compose => kube/compose}/internal/kube/placement.go | 0 {compose => kube/compose}/internal/kube/placement_test.go | 0 {compose => kube/compose}/internal/kube/pod.go | 0 {compose => kube/compose}/internal/kube/pod_test.go | 0 {compose => kube/compose}/internal/kube/volumes.go | 0 {compose => kube/compose}/internal/utils/config.go | 0 {compose => kube/compose}/internal/utils/errors.go | 0 {compose => kube/compose}/internal/utils/labels.go | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename {compose => kube/compose}/compose.go (100%) rename {compose => kube/compose}/internal/env.go (100%) rename {compose => kube/compose}/internal/helm/chart.go (100%) rename {compose => kube/compose}/internal/helm/helm.go (100%) rename {compose => kube/compose}/internal/kube/kube.go (100%) rename {compose => kube/compose}/internal/kube/placement.go (100%) rename {compose => kube/compose}/internal/kube/placement_test.go (100%) rename {compose => kube/compose}/internal/kube/pod.go (100%) rename {compose => kube/compose}/internal/kube/pod_test.go (100%) rename {compose => kube/compose}/internal/kube/volumes.go (100%) rename {compose => kube/compose}/internal/utils/config.go (100%) rename {compose => kube/compose}/internal/utils/errors.go (100%) rename {compose => kube/compose}/internal/utils/labels.go (100%) diff --git a/compose/compose.go b/kube/compose/compose.go similarity index 100% rename from compose/compose.go rename to kube/compose/compose.go diff --git a/compose/internal/env.go b/kube/compose/internal/env.go similarity index 100% rename from compose/internal/env.go rename to kube/compose/internal/env.go diff --git a/compose/internal/helm/chart.go b/kube/compose/internal/helm/chart.go similarity index 100% rename from compose/internal/helm/chart.go rename to kube/compose/internal/helm/chart.go diff --git a/compose/internal/helm/helm.go b/kube/compose/internal/helm/helm.go similarity index 100% rename from compose/internal/helm/helm.go rename to kube/compose/internal/helm/helm.go diff --git a/compose/internal/kube/kube.go b/kube/compose/internal/kube/kube.go similarity index 100% rename from compose/internal/kube/kube.go rename to kube/compose/internal/kube/kube.go diff --git a/compose/internal/kube/placement.go b/kube/compose/internal/kube/placement.go similarity index 100% rename from compose/internal/kube/placement.go rename to kube/compose/internal/kube/placement.go diff --git a/compose/internal/kube/placement_test.go b/kube/compose/internal/kube/placement_test.go similarity index 100% rename from compose/internal/kube/placement_test.go rename to kube/compose/internal/kube/placement_test.go diff --git a/compose/internal/kube/pod.go b/kube/compose/internal/kube/pod.go similarity index 100% rename from compose/internal/kube/pod.go rename to kube/compose/internal/kube/pod.go diff --git a/compose/internal/kube/pod_test.go b/kube/compose/internal/kube/pod_test.go similarity index 100% rename from compose/internal/kube/pod_test.go rename to kube/compose/internal/kube/pod_test.go diff --git a/compose/internal/kube/volumes.go b/kube/compose/internal/kube/volumes.go similarity index 100% rename from compose/internal/kube/volumes.go rename to kube/compose/internal/kube/volumes.go diff --git a/compose/internal/utils/config.go b/kube/compose/internal/utils/config.go similarity index 100% rename from compose/internal/utils/config.go rename to kube/compose/internal/utils/config.go diff --git a/compose/internal/utils/errors.go b/kube/compose/internal/utils/errors.go similarity index 100% rename from compose/internal/utils/errors.go rename to kube/compose/internal/utils/errors.go diff --git a/compose/internal/utils/labels.go b/kube/compose/internal/utils/labels.go similarity index 100% rename from compose/internal/utils/labels.go rename to kube/compose/internal/utils/labels.go From 50792c4621a51e14b7659dce09681a40f7445609 Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 19 Jan 2021 11:49:50 +0100 Subject: [PATCH 19/24] Add Kubernetes backend Signed-off-by: aiordache --- Makefile | 2 +- api/context/store/contextmetadata.go | 6 + api/context/store/store.go | 6 + cli/cmd/context/create_kube.go | 86 ++ go.mod | 9 +- go.sum | 212 +++++ kube/backend.go | 84 ++ kube/charts/charts.go | 126 +++ kube/charts/helm/chart.go | 108 +++ kube/charts/helm/helm.go | 135 +++ kube/charts/kubernetes/kube.go | 232 +++++ kube/charts/kubernetes/placement.go | 144 ++++ kube/charts/kubernetes/placement_test.go | 181 ++++ kube/charts/kubernetes/pod.go | 367 ++++++++ kube/charts/kubernetes/pod_test.go | 1005 ++++++++++++++++++++++ kube/charts/kubernetes/volumes.go | 246 ++++++ kube/compose.go | 106 +++ kube/context.go | 27 + kube/utils/config.go | 142 +++ kube/utils/errors.go | 56 ++ kube/utils/labels.go | 35 + kube/utils/utils.go | 34 + 22 files changed, 3344 insertions(+), 5 deletions(-) create mode 100644 cli/cmd/context/create_kube.go create mode 100644 kube/backend.go create mode 100644 kube/charts/charts.go create mode 100644 kube/charts/helm/chart.go create mode 100644 kube/charts/helm/helm.go create mode 100644 kube/charts/kubernetes/kube.go create mode 100644 kube/charts/kubernetes/placement.go create mode 100644 kube/charts/kubernetes/placement_test.go create mode 100644 kube/charts/kubernetes/pod.go create mode 100644 kube/charts/kubernetes/pod_test.go create mode 100644 kube/charts/kubernetes/volumes.go create mode 100644 kube/compose.go create mode 100644 kube/context.go create mode 100644 kube/utils/config.go create mode 100644 kube/utils/errors.go create mode 100644 kube/utils/labels.go create mode 100644 kube/utils/utils.go diff --git a/Makefile b/Makefile index 6e51ca7c9..961f2067e 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ protos: ## Generate go code from .proto files cli: ## Compile the cli @docker build . --target cli \ --platform local \ - --build-arg BUILD_TAGS=e2e \ + --build-arg BUILD_TAGS=e2e,kube \ --build-arg GIT_TAG=$(GIT_TAG) \ --output ./bin diff --git a/api/context/store/contextmetadata.go b/api/context/store/contextmetadata.go index f8b73211c..5ec187320 100644 --- a/api/context/store/contextmetadata.go +++ b/api/context/store/contextmetadata.go @@ -55,6 +55,12 @@ type EcsContext struct { Profile string `json:",omitempty"` } +// KubeContext is the context for a kube backend +type KubeContext struct { + Endpoint string `json:",omitempty"` + FromEnvironment bool +} + // AwsContext is the context for the ecs plugin type AwsContext EcsContext diff --git a/api/context/store/store.go b/api/context/store/store.go index b79e862b6..5427f7529 100644 --- a/api/context/store/store.go +++ b/api/context/store/store.go @@ -55,6 +55,9 @@ const ( // LocalContextType is the endpoint key in the context endpoints for a new // local backend LocalContextType = "local" + // KubeContextType is the endpoint key in the context endpoints for a new + // kube backend + KubeContextType = "kube" ) const ( @@ -328,5 +331,8 @@ func getters() map[string]func() interface{} { LocalContextType: func() interface{} { return &LocalContext{} }, + KubeContextType: func() interface{} { + return &KubeContext{} + }, } } diff --git a/cli/cmd/context/create_kube.go b/cli/cmd/context/create_kube.go new file mode 100644 index 000000000..c11e204f6 --- /dev/null +++ b/cli/cmd/context/create_kube.go @@ -0,0 +1,86 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package context + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/api/errdefs" + "github.com/docker/compose-cli/kube" +) + +func init() { + extraCommands = append(extraCommands, createKubeCommand) + extraHelp = append(extraHelp, ` +Create a Kube context: +$ docker context create kube CONTEXT [flags] +(see docker context create kube --help) +`) +} + +func createKubeCommand() *cobra.Command { + var opts kube.ContextParams + cmd := &cobra.Command{ + Use: "kube CONTEXT [flags]", + Short: "Create context for a Kubernetes Cluster", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Name = args[0] + + if opts.Endpoint != "" { + opts.FromEnvironment = false + } + return runCreateKube(cmd.Context(), args[0], opts) + }, + } + + addDescriptionFlag(cmd, &opts.Description) + cmd.Flags().StringVar(&opts.Endpoint, "endpoint", "", "The endpoint of the Kubernetes manager") + cmd.Flags().BoolVar(&opts.FromEnvironment, "from-env", true, "Get endpoint and creds from env vars") + return cmd +} + +func runCreateKube(ctx context.Context, contextName string, opts kube.ContextParams) error { + if contextExists(ctx, contextName) { + return errors.Wrapf(errdefs.ErrAlreadyExists, "context %q", contextName) + } + + contextData, description, err := createContextData(ctx, opts) + if err != nil { + return err + } + return createDockerContext(ctx, contextName, store.KubeContextType, description, contextData) +} + +func createContextData(ctx context.Context, opts kube.ContextParams) (interface{}, string, error) { + description := "" + if opts.Description != "" { + description = fmt.Sprintf("%s (%s)", opts.Description, description) + } + + return store.KubeContext{ + Endpoint: opts.Endpoint, + FromEnvironment: opts.FromEnvironment, + }, description, nil +} diff --git a/go.mod b/go.mod index dc10eec8d..074731c7c 100644 --- a/go.mod +++ b/go.mod @@ -39,14 +39,13 @@ require ( github.com/joho/godotenv v1.3.0 github.com/labstack/echo v3.3.10+incompatible github.com/labstack/gommon v0.3.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/moby/buildkit v0.8.1-0.20201205083753-0af7b1b9c693 github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/procfs v0.2.0 // indirect + github.com/prometheus/common v0.10.0 github.com/prometheus/tsdb v0.10.0 github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b github.com/sirupsen/logrus v1.7.0 @@ -57,13 +56,15 @@ require ( golang.org/x/net v0.0.0-20201110031124-69a78807bb2b golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect google.golang.org/grpc v1.33.2 google.golang.org/protobuf v1.25.0 gopkg.in/ini.v1 v1.62.0 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.3 - k8s.io/client-go v0.20.1 // indirect + helm.sh/helm/v3 v3.5.0 + k8s.io/api v0.20.1 + k8s.io/apimachinery v0.20.1 sigs.k8s.io/kustomize/kyaml v0.10.5 ) diff --git a/go.sum b/go.sum index b872ec88b..cd907ba47 100644 --- a/go.sum +++ b/go.sum @@ -128,16 +128,30 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Djarvur/go-err113 v0.0.0-20200410182137-af658d038157/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.0 h1:P1ekkbuU73Ui/wS0nK1HOM37hh4xdfZo485UPf8rc+Y= +github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI= +github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= +github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -164,10 +178,13 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= @@ -175,14 +192,17 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs= github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= @@ -195,8 +215,12 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.15.90/go.mod h1:es1KtYUFs7le0xQ3rOihkuoVD90z7D0fR2Qm4S00/gU= @@ -205,10 +229,12 @@ github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.35.33 h1:8qPRZqCRok5i7VNN51k/Ky7CuyoXMdSs4mUfKyCqvPw= github.com/aws/aws-sdk-go v1.35.33/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/awslabs/goformation/v4 v4.15.6 h1:9F0MbtJVSMkuI19G6Fm+qHc1nqScHcOIf+3YRRv+Ohc= github.com/awslabs/goformation/v4 v4.15.6/go.mod h1:wB5lKZf1J0MYH1Lt4B9w3opqz0uIjP7MMCAcib3QkwA= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= @@ -218,28 +244,35 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg= github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/bugsnag-go v1.4.1/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/bugsnag-go v1.5.0 h1:tP8hiPv1pGGW3LA6LKy5lW6WG+y9J2xWUdPd3WC452k= github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -251,6 +284,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -258,6 +292,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775 h1:cHzBGGVew0ezFsq2grfy2RsB8hO/eNyBgOLHBCqfR1U= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= @@ -273,6 +308,7 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/compose-spec/compose-go v0.0.0-20210119095023-cd294eea46e9 h1:fk9KYzKkVy6q1ETSXOPDHxeoj2ZBKZFP27XVfVMRMUM= github.com/compose-spec/compose-go v0.0.0-20210119095023-cd294eea46e9/go.mod h1:rz7rjxJGA/pWpLdBmDdqymGm2okEDYgBE7yx569xW+I= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340 h1:9atoWyI9RtXFwf7UDbme/6M8Ud0rFrx+Q3ZWgSnsxtw= @@ -286,12 +322,14 @@ github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.1-0.20201117152358-0edc412565dc/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM= github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY= @@ -338,14 +376,19 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c= +github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As= github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -358,6 +401,7 @@ github.com/docker/buildx v0.5.1 h1:lkMHQPKHyUUDbO2QO2JE9LLi8mlOULpzSoV7B/2XKso= github.com/docker/buildx v0.5.1/go.mod h1:YlxswdEKSMrxCCSYWU2p/Ii1oOOwu8lT3tJzJDpP7J4= github.com/docker/cli v0.0.0-20190925022749-754388324470/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.0-beta1.0.20201029214301-1d20b15adc38+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.1+incompatible h1:7wfpCo1keo9xXmH6fFu29m0BCK0+Uzu4oIopGUS09bM= github.com/docker/cli v20.10.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -369,6 +413,7 @@ github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r github.com/docker/docker v1.4.2-0.20180531152204-71cd53e4a197/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20181229214054-f76d6a078d88/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v17.12.0-ce-rc1.0.20200730172259-9f28837c1d93+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.0-beta1.0.20201110211921-af34b94a78a1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.1+incompatible h1:u0HIBLwOJdemyBdTCkoBX34u3lb5KyBo0rQE3a5Yg+E= @@ -396,6 +441,7 @@ github.com/docker/libtrust v0.0.0-20150526203908-9cbd2a1374f4/go.mod h1:cyGadeNE github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -404,33 +450,47 @@ github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8= github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -446,10 +506,14 @@ github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= @@ -506,6 +570,7 @@ github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85n github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -525,7 +590,17 @@ github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslW github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -539,10 +614,15 @@ github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7 github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.7.3 h1:I0EKY9l8HZCXTMYC4F80vwT6KNypV9uYKP3Alm/hjmQ= github.com/gofrs/flock v0.7.3/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= +github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/googleapis v1.3.0/go.mod h1:d+q1s/xVJxZGKWwC/6UfPIF33J+G1Tq4GYv9Y+Tg/EU= github.com/gogo/googleapis v1.3.2 h1:kX1es4djPJrsDhY7aZKJy7aZasdcB5oSOEphMjSB53c= @@ -554,6 +634,7 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5 github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -608,8 +689,12 @@ github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bz github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= +github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/crfs v0.0.0-20191108021818-71d77da419c9/go.mod h1:etGhoOqfwPkooV6aqoX3eBGQOJblqdoc9XvWOeuxpPw= @@ -674,6 +759,7 @@ github.com/goreleaser/goreleaser v0.136.0/go.mod h1:wiKrPUeSNh6Wu8nUHxZydSOVQ/OZ github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= github.com/goreleaser/nfpm v1.3.0/go.mod h1:w0p7Kc9TAUgWMyrub63ex3M2Mgw88M4GZXoTq5UCb40= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33 h1:893HsJqtxp9z1SF76gg6hY70hRY1wVlTSnC/h1yUDCo= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -685,8 +771,11 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -705,7 +794,9 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMW github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.0.3/go.mod h1:0EQM6aH2ctVpvZ6a+onrQ/vaykxh2GH7hy3e13vzTUY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -743,6 +834,9 @@ github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09Uny github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U= github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -753,6 +847,7 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= @@ -776,6 +871,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5 h1:lrdPtrORjGv1HbbEvKWDUAy97mPpFm4B8hp77tcCUJY= github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= @@ -795,6 +892,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= @@ -813,6 +911,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -829,11 +928,23 @@ github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8 github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -865,12 +976,17 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -884,12 +1000,16 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/pkcs11 v0.0.0-20190322140431-074fd7a1ed19/go.mod h1:WCBAbTOdfhHhz7YXujeZMF7owC4tPb1naKFsgfUISjo= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -899,6 +1019,8 @@ github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/buildkit v0.8.1-0.20201205083753-0af7b1b9c693 h1:WD0QJVZm1JRoUYhnu37QoCHC1wsrJYIUnRaPzX55Xlc= github.com/moby/buildkit v0.8.1-0.20201205083753-0af7b1b9c693/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -910,6 +1032,7 @@ github.com/moby/sys/mountinfo v0.1.0/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+S github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o= github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ= github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw= github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= @@ -934,6 +1057,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -941,8 +1071,12 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -984,20 +1118,32 @@ github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6/go.m github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY= github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/paulmach/orb v0.1.3/go.mod h1:VFlX/8C+IQ1p6FTRRKzKoOPJnvEtA5G0Veuqwbu//Vk= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pivotal/image-relocation v0.0.0-20191111101224-e94aff6df06c/go.mod h1:/JNbQwGylYm3AQh8q+MBF8e/h0W1Jy20JGTvozuXYTE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1005,6 +1151,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -1017,12 +1164,14 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= @@ -1043,6 +1192,7 @@ github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= @@ -1062,15 +1212,23 @@ github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uY github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.0.4/go.mod h1:9T/Cfuxs5StfsocWr4WzDL36HqnX0fVb9d5fSEaLhoE= github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8= @@ -1090,6 +1248,8 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -1110,12 +1270,14 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -1136,6 +1298,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1232,11 +1397,15 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -1247,6 +1416,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -1255,6 +1425,7 @@ go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1265,8 +1436,12 @@ go.starlark.net v0.0.0-20190528202925-30ae18b8564f/go.mod h1:c1/X6cHgvdXj6pUlmWK go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4= @@ -1285,6 +1460,7 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1292,6 +1468,7 @@ golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= @@ -1431,6 +1608,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1457,6 +1635,7 @@ golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1544,8 +1723,11 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191014205221-18e3458ac98b/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1557,6 +1739,7 @@ golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200102140908-9497f49d5709/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1576,8 +1759,10 @@ golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -1675,6 +1860,7 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1703,6 +1889,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1716,8 +1903,11 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -1731,6 +1921,7 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1749,6 +1940,8 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +helm.sh/helm/v3 v3.5.0 h1:uqIT3Bh4hVEyZRThyTPik8FkiABj3VJIY+POvDFT3a4= +helm.sh/helm/v3 v3.5.0/go.mod h1:bjwXfmGAF+SEuJZ2AtN1xmTuz4FqaNYOJrXP+vtj6Tw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1766,6 +1959,8 @@ k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= k8s.io/api v0.20.1 h1:ud1c3W3YNzGd6ABJlbFfKXBKXO+1KdGfcgGGNgFR03E= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/apiextensions-apiserver v0.20.1 h1:ZrXQeslal+6zKM/HjDXLzThlz/vPSxrfK3OqL8txgVQ= +k8s.io/apiextensions-apiserver v0.20.1/go.mod h1:ntnrZV+6a3dB504qwC5PN/Yg9PBiDNt1EVqbW2kORVk= k8s.io/apimachinery v0.0.0-20180904193909-def12e63c512/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= k8s.io/apimachinery v0.0.0-20190806215851-162a2dabc72f/go.mod h1:+ntn62igV2hyNj7/0brOvXSMONE2KxcePkSxK7/9FFQ= k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= @@ -1776,6 +1971,10 @@ k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apiserver v0.17.4 h1:bYc9LvDPEF9xAL3fhbDzqNOQOAnNF2ZYCrMW8v52/mE= k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I= +k8s.io/apiserver v0.20.1 h1:yEqdkxlnQbxi/3e74cp0X16h140fpvPrNnNRAJBDuBk= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/cli-runtime v0.20.1 h1:fJhRQ9EfTpJpCqSFOAqnYLuu5aAM7yyORWZ26qW1jJc= +k8s.io/cli-runtime v0.20.1/go.mod h1:6wkMM16ZXTi7Ow3JLYPe10bS+XBnIkL6V9dmEz0mbuY= k8s.io/client-go v0.0.0-20180910083459-2cefa64ff137/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v0.0.0-20191016111102-bec269661e48/go.mod h1:hrwktSwYGI4JK+TJA3dMaFyyvHVi/aLarVHpbs8bgCU= k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc= @@ -1785,13 +1984,18 @@ k8s.io/client-go v0.20.1 h1:Qquik0xNFbK9aUG92pxHYsyfea5/RPO9o9bSywNor+M= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KNK9U= k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.20.1/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= k8s.io/component-base v0.17.4 h1:H9cdWZyiGVJfWmWIcHd66IsNBWTk1iEgU7D4kJksEnw= k8s.io/component-base v0.17.4/go.mod h1:5BRqHMbbQPm2kKu35v3G+CpVq4K0RJKC7TRioF0I9lE= +k8s.io/component-base v0.20.1 h1:6OQaHr205NSl24t5wOF2IhdrlxZTWEZwuGlLvBgaeIg= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-helpers v0.20.1/go.mod h1:Q8trCj1zyLNdeur6pD2QvsF8d/nWVfK71YjN5+qVXy4= k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= k8s.io/csi-translation-lib v0.17.4/go.mod h1:CsxmjwxEI0tTNMzffIAcgR9lX4wOh6AKHdxQrT7L0oo= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -1808,11 +2012,15 @@ k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4y k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubectl v0.20.1 h1:7h1vSrL/B3hLrhlCJhbTADElPKDbx+oVUt3+QDSXxBo= +k8s.io/kubectl v0.20.1/go.mod h1:2bE0JLYTRDVKDiTREFsjLAx4R2GvUtL/mGYFXfFFMzY= k8s.io/kubernetes v1.11.10/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js= +k8s.io/metrics v0.20.1/go.mod h1:JhpBE/fad3yRGsgEpiZz5FQQM5wJ18OTLkD7Tv40c0s= k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= @@ -1832,6 +2040,9 @@ pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= +sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/kustomize/kyaml v0.10.5 h1:PbJcsZsEM7O3hHtUWTR+4WkHVbQRW9crSy75or1gRbI= sigs.k8s.io/kustomize/kyaml v0.10.5/go.mod h1:P6Oy/ah/GZMKzJMIJA2a3/bc8YrBkuL5kJji13PSIzY= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= @@ -1844,6 +2055,7 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/kube/backend.go b/kube/backend.go new file mode 100644 index 000000000..aa10cd884 --- /dev/null +++ b/kube/backend.go @@ -0,0 +1,84 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kube + +import ( + "context" + + "github.com/docker/compose-cli/api/backend" + "github.com/docker/compose-cli/api/cloud" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/containers" + apicontext "github.com/docker/compose-cli/api/context" + "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/api/resources" + "github.com/docker/compose-cli/api/secrets" + "github.com/docker/compose-cli/api/volumes" +) + +const backendType = store.KubeContextType + +type kubeAPIService struct { + ctx store.KubeContext + composeService compose.Service +} + +func init() { + backend.Register(backendType, backendType, service, cloud.NotImplementedCloudService) +} + +func service(ctx context.Context) (backend.Service, error) { + + contextStore := store.ContextStore(ctx) + currentContext := apicontext.CurrentContext(ctx) + var kubeContext store.KubeContext + + if err := contextStore.GetEndpoint(currentContext, &kubeContext); err != nil { + return nil, err + } + + s, err := NewComposeService(kubeContext) + if err != nil { + return nil, err + } + return &kubeAPIService{ + ctx: kubeContext, + composeService: s, + }, nil +} + +func (s *kubeAPIService) ContainerService() containers.Service { + return nil +} + +func (s *kubeAPIService) ComposeService() compose.Service { + return s.composeService +} + +func (s *kubeAPIService) SecretsService() secrets.Service { + return nil +} + +func (s *kubeAPIService) VolumeService() volumes.Service { + return nil +} + +func (s *kubeAPIService) ResourceService() resources.Service { + return nil +} diff --git a/kube/charts/charts.go b/kube/charts/charts.go new file mode 100644 index 000000000..df035eca7 --- /dev/null +++ b/kube/charts/charts.go @@ -0,0 +1,126 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package charts + +import ( + "context" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/kube/charts/helm" + "github.com/docker/compose-cli/kube/charts/kubernetes" + kubeutils "github.com/docker/compose-cli/kube/utils" + chart "helm.sh/helm/v3/pkg/chart" + util "helm.sh/helm/v3/pkg/chartutil" + helmenv "helm.sh/helm/v3/pkg/cli" +) + +// API defines management methods for helm charts +type API interface { + GetDefaultEnv() *helmenv.EnvSettings + Connect(ctx context.Context) error + GenerateChart(project *types.Project, dirname string) error + GetChartInMemory(project *types.Project) (*chart.Chart, error) + SaveChart(project *types.Project, dest string) error + + Install(project *types.Project) error + Uninstall(projectName string) error + List(projectName string) ([]compose.Stack, error) +} + +type sdk struct { + h *helm.HelmActions + environment map[string]string +} + +// sdk implement API +var _ API = sdk{} + +func NewSDK(ctx store.KubeContext) (sdk, error) { + return sdk{ + environment: kubeutils.Environment(), + h: helm.NewHelmActions(nil), + }, nil +} + +func (s sdk) Connect(ctx context.Context) error { + return nil +} + +// Install deploys a Compose stack +func (s sdk) Install(project *types.Project) error { + chart, err := s.GetChartInMemory(project) + if err != nil { + return err + } + return s.h.InstallChart(project.Name, chart) +} + +// Uninstall removes a runnign compose stack +func (s sdk) Uninstall(projectName string) error { + return s.h.Uninstall(projectName) +} + +// List returns a list of compose stacks +func (s sdk) List(projectName string) ([]compose.Stack, error) { + return s.h.ListReleases() +} + +// GetDefault initializes Helm EnvSettings +func (s sdk) GetDefaultEnv() *helmenv.EnvSettings { + return helmenv.New() +} + +func (s sdk) GetChartInMemory(project *types.Project) (*chart.Chart, error) { + // replace _ with - in volume names + for k, v := range project.Volumes { + volumeName := strings.ReplaceAll(k, "_", "-") + if volumeName != k { + project.Volumes[volumeName] = v + delete(project.Volumes, k) + } + } + objects, err := kubernetes.MapToKubernetesObjects(project) + if err != nil { + return nil, err + } + //in memory files + return helm.ConvertToChart(project.Name, objects) +} + +func (s sdk) SaveChart(project *types.Project, dest string) error { + chart, err := s.GetChartInMemory(project) + if err != nil { + return err + } + return util.SaveDir(chart, dest) +} + +func (s sdk) GenerateChart(project *types.Project, dirname string) error { + if strings.Contains(dirname, ".") { + splits := strings.SplitN(dirname, ".", 2) + dirname = splits[0] + } + + dirname = filepath.Dir(dirname) + return s.SaveChart(project, dirname) +} diff --git a/kube/charts/helm/chart.go b/kube/charts/helm/chart.go new file mode 100644 index 000000000..d82ed6e9c --- /dev/null +++ b/kube/charts/helm/chart.go @@ -0,0 +1,108 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package helm + +import ( + "bytes" + "encoding/json" + "html/template" + "path/filepath" + + "gopkg.in/yaml.v3" + + chart "helm.sh/helm/v3/pkg/chart" + loader "helm.sh/helm/v3/pkg/chart/loader" + "k8s.io/apimachinery/pkg/runtime" +) + +func ConvertToChart(name string, objects map[string]runtime.Object) (*chart.Chart, error) { + + files := []*loader.BufferedFile{ + &loader.BufferedFile{ + Name: "README.md", + Data: []byte("This chart was created by converting a Compose file"), + }} + + chart := `name: {{.Name}} +description: A generated Helm Chart for {{.Name}} from Skippbox Kompose +version: 0.0.1 +apiVersion: v1 +keywords: + - {{.Name}} +sources: +home: +` + + t, err := template.New("ChartTmpl").Parse(chart) + if err != nil { + return nil, err + } + type ChartDetails struct { + Name string + } + var chartData bytes.Buffer + err = t.Execute(&chartData, ChartDetails{Name: name}) + if err != nil { + return nil, err + } + files = append(files, &loader.BufferedFile{ + Name: "Chart.yaml", + Data: chartData.Bytes(), + }) + + for name, o := range objects { + j, err := json.Marshal(o) + if err != nil { + return nil, err + } + buf, err := jsonToYaml(j, 2) + if err != nil { + return nil, err + } + files = append(files, &loader.BufferedFile{ + Name: filepath.Join("templates", name), + Data: buf, + }) + + } + return loader.LoadFiles(files) +} + +// Convert JSON to YAML. +func jsonToYaml(j []byte, spaces int) ([]byte, error) { + // Convert the JSON to an object. + var jsonObj interface{} + // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the + // Go JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshling to interface{}, it just picks float64 + // universally. go-yaml does go through the effort of picking the right + // number type, so we can preserve number type throughout this process. + err := yaml.Unmarshal(j, &jsonObj) + if err != nil { + return nil, err + } + + var b bytes.Buffer + encoder := yaml.NewEncoder(&b) + encoder.SetIndent(spaces) + if err := encoder.Encode(jsonObj); err != nil { + return nil, err + } + return b.Bytes(), nil +} diff --git a/kube/charts/helm/helm.go b/kube/charts/helm/helm.go new file mode 100644 index 000000000..e7e7f42fd --- /dev/null +++ b/kube/charts/helm/helm.go @@ -0,0 +1,135 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package helm + +import ( + "errors" + "log" + + "github.com/docker/compose-cli/api/compose" + action "helm.sh/helm/v3/pkg/action" + chart "helm.sh/helm/v3/pkg/chart" + loader "helm.sh/helm/v3/pkg/chart/loader" + env "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +type HelmActions struct { + Config *action.Configuration + Settings *env.EnvSettings + kube_conn_init bool +} + +func NewHelmActions(settings *env.EnvSettings) *HelmActions { + if settings == nil { + settings = env.New() + } + return &HelmActions{ + Config: new(action.Configuration), + Settings: settings, + kube_conn_init: false, + } +} + +func (hc *HelmActions) initKubeClient() error { + if hc.kube_conn_init { + return nil + } + if err := hc.Config.Init( + hc.Settings.RESTClientGetter(), + hc.Settings.Namespace(), + "configmap", + log.Printf, + ); err != nil { + log.Fatal(err) + } + if err := hc.Config.KubeClient.IsReachable(); err != nil { + return err + } + hc.kube_conn_init = true + return nil +} + +func (hc *HelmActions) InstallChartFromDir(name string, chartpath string) error { + chart, err := loader.Load(chartpath) + if err != nil { + return err + } + return hc.InstallChart(name, chart) +} + +func (hc *HelmActions) InstallChart(name string, chart *chart.Chart) error { + hc.initKubeClient() + + actInstall := action.NewInstall(hc.Config) + actInstall.ReleaseName = name + actInstall.Namespace = hc.Settings.Namespace() + + release, err := actInstall.Run(chart, map[string]interface{}{}) + if err != nil { + return err + } + log.Println("Release status: ", release.Info.Status) + log.Println(release.Info.Description) + return nil +} + +func (hc *HelmActions) Uninstall(name string) error { + hc.initKubeClient() + release, err := hc.Get(name) + if err != nil { + return err + } + if release == nil { + return errors.New("No release found with the name provided.") + } + actUninstall := action.NewUninstall(hc.Config) + response, err := actUninstall.Run(name) + if err != nil { + return err + } + log.Println(response.Release.Info.Description) + return nil +} + +func (hc *HelmActions) Get(name string) (*release.Release, error) { + hc.initKubeClient() + + actGet := action.NewGet(hc.Config) + return actGet.Run(name) +} + +func (hc *HelmActions) ListReleases() ([]compose.Stack, error) { + hc.initKubeClient() + + actList := action.NewList(hc.Config) + releases, err := actList.Run() + if err != nil { + return nil, err + } + result := []compose.Stack{} + for _, rel := range releases { + result = append(result, compose.Stack{ + ID: rel.Name, + Name: rel.Name, + Status: string(rel.Info.Status), + }) + } + return result, nil +} diff --git a/kube/charts/kubernetes/kube.go b/kube/charts/kubernetes/kube.go new file mode 100644 index 000000000..ded7419b8 --- /dev/null +++ b/kube/charts/kubernetes/kube.go @@ -0,0 +1,232 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/compose-spec/compose-go/types" + apps "k8s.io/api/apps/v1" + core "k8s.io/api/core/v1" + resource "k8s.io/apimachinery/pkg/api/resource" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func MapToKubernetesObjects(project *types.Project) (map[string]runtime.Object, error) { + objects := map[string]runtime.Object{} + + for _, service := range project.Services { + svcObject := mapToService(project, service) + if svcObject != nil { + objects[fmt.Sprintf("%s-service.yaml", service.Name)] = svcObject + } else { + log.Println("Missing port mapping from service config.") + } + + if service.Deploy != nil && service.Deploy.Mode == "global" { + daemonset, err := mapToDaemonset(project, service, project.Name) + if err != nil { + return nil, err + } + objects[fmt.Sprintf("%s-daemonset.yaml", service.Name)] = daemonset + } else { + deployment, err := mapToDeployment(project, service, project.Name) + if err != nil { + return nil, err + } + objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = deployment + } + for _, vol := range service.Volumes { + if vol.Type == "volume" { + vol.Source = strings.ReplaceAll(vol.Source, "_", "-") + objects[fmt.Sprintf("%s-persistentvolumeclaim.yaml", vol.Source)] = mapToPVC(service, vol) + } + } + } + return objects, nil +} + +func mapToService(project *types.Project, service types.ServiceConfig) *core.Service { + ports := []core.ServicePort{} + for _, p := range service.Ports { + ports = append(ports, + core.ServicePort{ + Name: fmt.Sprintf("%d-%s", p.Target, strings.ToLower(string(p.Protocol))), + Port: int32(p.Target), + TargetPort: intstr.FromInt(int(p.Target)), + Protocol: toProtocol(p.Protocol), + }) + } + if len(ports) == 0 { + return nil + } + return &core.Service{ + TypeMeta: meta.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + }, + Spec: core.ServiceSpec{ + Selector: map[string]string{"com.docker.compose.service": service.Name}, + Ports: ports, + Type: mapServiceToServiceType(project, service), + }, + } +} + +func mapServiceToServiceType(project *types.Project, service types.ServiceConfig) core.ServiceType { + serviceType := core.ServiceTypeClusterIP + if len(service.Networks) == 0 { + // service is implicitly attached to "default" network + serviceType = core.ServiceTypeLoadBalancer + } + for name := range service.Networks { + if !project.Networks[name].Internal { + serviceType = core.ServiceTypeLoadBalancer + } + } + for _, port := range service.Ports { + if port.Published != 0 { + serviceType = core.ServiceTypeNodePort + } + } + return serviceType +} + +func mapToDeployment(project *types.Project, service types.ServiceConfig, name string) (*apps.Deployment, error) { + labels := map[string]string{ + "com.docker.compose.service": service.Name, + "com.docker.compose.project": name, + } + podTemplate, err := toPodTemplate(project, service, labels) + if err != nil { + return nil, err + } + selector := new(meta.LabelSelector) + selector.MatchLabels = make(map[string]string) + for key, val := range labels { + selector.MatchLabels[key] = val + } + return &apps.Deployment{ + TypeMeta: meta.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: labels, + }, + Spec: apps.DeploymentSpec{ + Selector: selector, + Replicas: toReplicas(service.Deploy), + Strategy: toDeploymentStrategy(service.Deploy), + Template: podTemplate, + }, + }, nil +} + +func mapToDaemonset(project *types.Project, service types.ServiceConfig, name string) (*apps.DaemonSet, error) { + labels := map[string]string{ + "com.docker.compose.service": service.Name, + "com.docker.compose.project": name, + } + podTemplate, err := toPodTemplate(project, service, labels) + if err != nil { + return nil, err + } + + return &apps.DaemonSet{ + ObjectMeta: meta.ObjectMeta{ + Name: service.Name, + Labels: labels, + }, + Spec: apps.DaemonSetSpec{ + Template: podTemplate, + }, + }, nil +} + +func toReplicas(deploy *types.DeployConfig) *int32 { + v := int32(1) + if deploy != nil { + v = int32(*deploy.Replicas) + } + return &v +} + +func toDeploymentStrategy(deploy *types.DeployConfig) apps.DeploymentStrategy { + if deploy == nil || deploy.UpdateConfig == nil { + return apps.DeploymentStrategy{ + Type: apps.RecreateDeploymentStrategyType, + } + } + return apps.DeploymentStrategy{ + Type: apps.RollingUpdateDeploymentStrategyType, + RollingUpdate: &apps.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(*deploy.UpdateConfig.Parallelism), + }, + MaxSurge: nil, + }, + } +} + +func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { + rwaccess := core.ReadWriteOnce + if vol.ReadOnly { + rwaccess = core.ReadOnlyMany + } + return &core.PersistentVolumeClaim{ + TypeMeta: meta.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: meta.ObjectMeta{ + Name: vol.Source, + Labels: map[string]string{"com.docker.compose.service": service.Name}, + }, + Spec: core.PersistentVolumeClaimSpec{ + VolumeName: vol.Source, + AccessModes: []core.PersistentVolumeAccessMode{rwaccess}, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + core.ResourceStorage: resource.MustParse("100Mi"), + }, + }, + }, + } +} + +// toSecondsOrDefault converts a duration string in seconds and defaults to a +// given value if the duration is nil. +// The supported units are us, ms, s, m and h. +func toSecondsOrDefault(duration *types.Duration, defaultValue int32) int32 { //nolint: unparam + if duration == nil { + return defaultValue + } + return int32(time.Duration(*duration).Seconds()) +} diff --git a/kube/charts/kubernetes/placement.go b/kube/charts/kubernetes/placement.go new file mode 100644 index 000000000..f5480da48 --- /dev/null +++ b/kube/charts/kubernetes/placement.go @@ -0,0 +1,144 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "regexp" + "strings" + + "github.com/compose-spec/compose-go/types" + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" +) + +var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`) + +const ( + kubernetesOs = "beta.kubernetes.io/os" + kubernetesArch = "beta.kubernetes.io/arch" + kubernetesHostname = "kubernetes.io/hostname" +) + +// node.id Node ID node.id == 2ivku8v2gvtg4 +// node.hostname Node hostname node.hostname != node-2 +// node.role Node role node.role == manager +// node.labels user defined node labels node.labels.security == high +// engine.labels Docker Engine's labels engine.labels.operatingsystem == ubuntu 14.04 +func toNodeAffinity(deploy *types.DeployConfig) (*apiv1.Affinity, error) { + constraints := []string{} + if deploy != nil && deploy.Placement.Constraints != nil { + constraints = deploy.Placement.Constraints + } + requirements := []apiv1.NodeSelectorRequirement{} + for _, constraint := range constraints { + matches := constraintEquals.FindStringSubmatch(constraint) + if len(matches) == 4 { + key := matches[1] + operator, err := toRequirementOperator(matches[2]) + if err != nil { + return nil, err + } + value := matches[3] + + switch { + case key == constraintOs: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesOs, + Operator: operator, + Values: []string{value}, + }) + case key == constraintArch: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesArch, + Operator: operator, + Values: []string{value}, + }) + case key == constraintHostname: + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesHostname, + Operator: operator, + Values: []string{value}, + }) + case strings.HasPrefix(key, constraintLabelPrefix): + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: strings.TrimPrefix(key, constraintLabelPrefix), + Operator: operator, + Values: []string{value}, + }) + } + } + } + + if !hasRequirement(requirements, kubernetesOs) { + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesOs, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{"linux"}, + }) + } + if !hasRequirement(requirements, kubernetesArch) { + requirements = append(requirements, apiv1.NodeSelectorRequirement{ + Key: kubernetesArch, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{"amd64"}, + }) + } + return &apiv1.Affinity{ + NodeAffinity: &apiv1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ + NodeSelectorTerms: []apiv1.NodeSelectorTerm{ + { + MatchExpressions: requirements, + }, + }, + }, + }, + }, nil +} + +const ( + constraintOs = "node.platform.os" + constraintArch = "node.platform.arch" + constraintHostname = "node.hostname" + constraintLabelPrefix = "node.labels." +) + +func hasRequirement(requirements []apiv1.NodeSelectorRequirement, key string) bool { + for _, r := range requirements { + if r.Key == key { + return true + } + } + return false +} + +func toRequirementOperator(sign string) (apiv1.NodeSelectorOperator, error) { + switch sign { + case "==": + return apiv1.NodeSelectorOpIn, nil + case "!=": + return apiv1.NodeSelectorOpNotIn, nil + case ">": + return apiv1.NodeSelectorOpGt, nil + case "<": + return apiv1.NodeSelectorOpLt, nil + default: + return "", errors.Errorf("operator %s not supported", sign) + } +} diff --git a/kube/charts/kubernetes/placement_test.go b/kube/charts/kubernetes/placement_test.go new file mode 100644 index 000000000..24e51ecdf --- /dev/null +++ b/kube/charts/kubernetes/placement_test.go @@ -0,0 +1,181 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "reflect" + "sort" + "testing" + + "github.com/compose-spec/compose-go/types" + + "github.com/stretchr/testify/assert" + apiv1 "k8s.io/api/core/v1" +) + +func TestToPodWithPlacement(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: redis:alpine + deploy: + placement: + constraints: + - node.platform.os == linux + - node.platform.arch == amd64 + - node.hostname == node01 + - node.labels.label1 == value1 + - node.labels.label2.subpath != value2 +`) + + expectedRequirements := []apiv1.NodeSelectorRequirement{ + {Key: "beta.kubernetes.io/os", Operator: apiv1.NodeSelectorOpIn, Values: []string{"linux"}}, + {Key: "beta.kubernetes.io/arch", Operator: apiv1.NodeSelectorOpIn, Values: []string{"amd64"}}, + {Key: "kubernetes.io/hostname", Operator: apiv1.NodeSelectorOpIn, Values: []string{"node01"}}, + {Key: "label1", Operator: apiv1.NodeSelectorOpIn, Values: []string{"value1"}}, + {Key: "label2.subpath", Operator: apiv1.NodeSelectorOpNotIn, Values: []string{"value2"}}, + } + + requirements := podTemplate.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions + + sort.Slice(expectedRequirements, func(i, j int) bool { return expectedRequirements[i].Key < expectedRequirements[j].Key }) + sort.Slice(requirements, func(i, j int) bool { return requirements[i].Key < requirements[j].Key }) + + assert.EqualValues(t, expectedRequirements, requirements) +} + +type keyValue struct { + key string + value string +} + +func kv(key, value string) keyValue { + return keyValue{key: key, value: value} +} + +func makeExpectedAffinity(kvs ...keyValue) *apiv1.Affinity { + + var matchExpressions []apiv1.NodeSelectorRequirement + for _, kv := range kvs { + matchExpressions = append( + matchExpressions, + apiv1.NodeSelectorRequirement{ + Key: kv.key, + Operator: apiv1.NodeSelectorOpIn, + Values: []string{kv.value}, + }, + ) + } + return &apiv1.Affinity{ + NodeAffinity: &apiv1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ + NodeSelectorTerms: []apiv1.NodeSelectorTerm{ + { + MatchExpressions: matchExpressions, + }, + }, + }, + }, + } +} + +func TestNodeAfinity(t *testing.T) { + cases := []struct { + name string + source []string + expected *apiv1.Affinity + }{ + { + name: "nil", + expected: makeExpectedAffinity( + kv(kubernetesOs, "linux"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "hostname", + source: []string{"node.hostname == test"}, + expected: makeExpectedAffinity( + kv(kubernetesHostname, "test"), + kv(kubernetesOs, "linux"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "os", + source: []string{"node.platform.os == windows"}, + expected: makeExpectedAffinity( + kv(kubernetesOs, "windows"), + kv(kubernetesArch, "amd64"), + ), + }, + { + name: "arch", + source: []string{"node.platform.arch == arm64"}, + expected: makeExpectedAffinity( + kv(kubernetesArch, "arm64"), + kv(kubernetesOs, "linux"), + ), + }, + { + name: "custom-labels", + source: []string{"node.platform.os == windows", "node.platform.arch == arm64"}, + expected: makeExpectedAffinity( + kv(kubernetesArch, "arm64"), + kv(kubernetesOs, "windows"), + ), + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := toNodeAffinity(&types.DeployConfig{ + Placement: types.Placement{ + Constraints: c.source, + }, + }) + assert.NoError(t, err) + assert.True(t, nodeAffinityMatch(c.expected, result)) + }) + } +} + +func nodeSelectorRequirementsToMap(source []apiv1.NodeSelectorRequirement, result map[string]apiv1.NodeSelectorRequirement) { + for _, t := range source { + result[t.Key] = t + } +} + +func nodeAffinityMatch(expected, actual *apiv1.Affinity) bool { + expectedTerms := expected.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + actualTerms := actual.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + expectedExpressions := make(map[string]apiv1.NodeSelectorRequirement) + expectedFields := make(map[string]apiv1.NodeSelectorRequirement) + actualExpressions := make(map[string]apiv1.NodeSelectorRequirement) + actualFields := make(map[string]apiv1.NodeSelectorRequirement) + for _, v := range expectedTerms { + nodeSelectorRequirementsToMap(v.MatchExpressions, expectedExpressions) + nodeSelectorRequirementsToMap(v.MatchFields, expectedFields) + } + for _, v := range actualTerms { + nodeSelectorRequirementsToMap(v.MatchExpressions, actualExpressions) + nodeSelectorRequirementsToMap(v.MatchFields, actualFields) + } + return reflect.DeepEqual(expectedExpressions, actualExpressions) && reflect.DeepEqual(expectedFields, actualFields) +} diff --git a/kube/charts/kubernetes/pod.go b/kube/charts/kubernetes/pod.go new file mode 100644 index 000000000..632d122e2 --- /dev/null +++ b/kube/charts/kubernetes/pod.go @@ -0,0 +1,367 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/compose-spec/compose-go/types" + "github.com/docker/docker/api/types/swarm" + + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func toPodTemplate(project *types.Project, serviceConfig types.ServiceConfig, labels map[string]string) (apiv1.PodTemplateSpec, error) { + tpl := apiv1.PodTemplateSpec{} + //nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) + //if err != nil { + // return apiv1.PodTemplateSpec{}, err + //} + hostAliases, err := toHostAliases(serviceConfig.ExtraHosts) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + env, err := toEnv(serviceConfig.Environment) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + restartPolicy, err := toRestartPolicy(serviceConfig) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + + var limits apiv1.ResourceList + if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Limits != nil { + limits, err = toResource(serviceConfig.Deploy.Resources.Limits) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + } + var requests apiv1.ResourceList + if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Reservations != nil { + requests, err = toResource(serviceConfig.Deploy.Resources.Reservations) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + } + + volumes, err := toVolumes(project, serviceConfig) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + volumeMounts, err := toVolumeMounts(project, serviceConfig) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + /* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } */ + tpl.ObjectMeta = metav1.ObjectMeta{ + Labels: labels, + Annotations: serviceConfig.Labels, + } + tpl.Spec.RestartPolicy = restartPolicy + tpl.Spec.Volumes = volumes + tpl.Spec.HostPID = toHostPID(serviceConfig.Pid) + tpl.Spec.HostIPC = toHostIPC(serviceConfig.Ipc) + tpl.Spec.Hostname = serviceConfig.Hostname + tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod) + tpl.Spec.HostAliases = hostAliases + //tpl.Spec.Affinity = nodeAffinity + // we dont want to remove all containers and recreate them because: + // an admission plugin can add sidecar containers + // we for sure want to keep the main container to be additive + if len(tpl.Spec.Containers) == 0 { + tpl.Spec.Containers = []apiv1.Container{{}} + } + + containerIX := 0 + for ix, c := range tpl.Spec.Containers { + if c.Name == serviceConfig.Name { + containerIX = ix + break + } + } + tpl.Spec.Containers[containerIX].Name = serviceConfig.Name + tpl.Spec.Containers[containerIX].Image = serviceConfig.Image + // FIXME tpl.Spec.Containers[containerIX].ImagePullPolicy = pullPolicy + tpl.Spec.Containers[containerIX].Command = serviceConfig.Entrypoint + tpl.Spec.Containers[containerIX].Args = serviceConfig.Command + tpl.Spec.Containers[containerIX].WorkingDir = serviceConfig.WorkingDir + tpl.Spec.Containers[containerIX].TTY = serviceConfig.Tty + tpl.Spec.Containers[containerIX].Stdin = serviceConfig.StdinOpen + tpl.Spec.Containers[containerIX].Ports = toPorts(serviceConfig.Ports) + tpl.Spec.Containers[containerIX].LivenessProbe = toLivenessProbe(serviceConfig.HealthCheck) + tpl.Spec.Containers[containerIX].Env = env + tpl.Spec.Containers[containerIX].VolumeMounts = volumeMounts + tpl.Spec.Containers[containerIX].SecurityContext = toSecurityContext(serviceConfig) + tpl.Spec.Containers[containerIX].Resources = apiv1.ResourceRequirements{ + Limits: limits, + Requests: requests, + } + + /* FIXME + if serviceConfig.PullSecret != "" { + pullSecrets := map[string]struct{}{} + for _, ps := range tpl.Spec.ImagePullSecrets { + pullSecrets[ps.Name] = struct{}{} + } + if _, ok := pullSecrets[serviceConfig.PullSecret]; !ok { + tpl.Spec.ImagePullSecrets = append(tpl.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: serviceConfig.PullSecret}) + } + } + */ + return tpl, nil +} + +func toImagePullPolicy(image string, specifiedPolicy string) (apiv1.PullPolicy, error) { + if specifiedPolicy == "" { + if strings.HasSuffix(image, ":latest") { + return apiv1.PullAlways, nil + } + return apiv1.PullIfNotPresent, nil + } + switch apiv1.PullPolicy(specifiedPolicy) { + case apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever: + return apiv1.PullPolicy(specifiedPolicy), nil + default: + return "", errors.Errorf("invalid pull policy %q, must be %q, %q or %q", specifiedPolicy, apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever) + } +} + +func toHostAliases(extraHosts []string) ([]apiv1.HostAlias, error) { + if extraHosts == nil { + return nil, nil + } + + byHostnames := map[string]string{} + for _, host := range extraHosts { + split := strings.SplitN(host, ":", 2) + if len(split) != 2 { + return nil, errors.Errorf("malformed host %s", host) + } + byHostnames[split[0]] = split[1] + } + + byIPs := map[string][]string{} + for k, v := range byHostnames { + byIPs[v] = append(byIPs[v], k) + } + + aliases := make([]apiv1.HostAlias, len(byIPs)) + i := 0 + for key, hosts := range byIPs { + sort.Strings(hosts) + aliases[i] = apiv1.HostAlias{ + IP: key, + Hostnames: hosts, + } + i++ + } + sort.Slice(aliases, func(i, j int) bool { return aliases[i].IP < aliases[j].IP }) + return aliases, nil +} + +func toHostPID(pid string) bool { + return "host" == pid +} + +func toHostIPC(ipc string) bool { + return "host" == ipc +} + +func toTerminationGracePeriodSeconds(duration *types.Duration) *int64 { + if duration == nil { + return nil + } + gracePeriod := int64(time.Duration(*duration).Seconds()) + return &gracePeriod +} + +func toLivenessProbe(hc *types.HealthCheckConfig) *apiv1.Probe { + if hc == nil || len(hc.Test) < 1 || hc.Test[0] == "NONE" { + return nil + } + + command := hc.Test[1:] + if hc.Test[0] == "CMD-SHELL" { + command = append([]string{"sh", "-c"}, command...) + } + + return &apiv1.Probe{ + TimeoutSeconds: toSecondsOrDefault(hc.Timeout, 1), + PeriodSeconds: toSecondsOrDefault(hc.Interval, 1), + FailureThreshold: int32(defaultUint64(hc.Retries, 3)), + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: command, + }, + }, + } +} + +func toEnv(env map[string]*string) ([]apiv1.EnvVar, error) { + var envVars []apiv1.EnvVar + + for k, v := range env { + if v == nil { + return nil, errors.Errorf("%s has no value, unsetting an environment variable is not supported", k) + } + envVars = append(envVars, toEnvVar(k, *v)) + } + sort.Slice(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) + return envVars, nil +} + +func toEnvVar(key, value string) apiv1.EnvVar { + return apiv1.EnvVar{ + Name: key, + Value: value, + } +} + +func toPorts(list []types.ServicePortConfig) []apiv1.ContainerPort { + var ports []apiv1.ContainerPort + + for _, v := range list { + ports = append(ports, apiv1.ContainerPort{ + ContainerPort: int32(v.Target), + Protocol: toProtocol(v.Protocol), + }) + } + + return ports +} + +func toProtocol(value string) apiv1.Protocol { + if value == "udp" { + return apiv1.ProtocolUDP + } + return apiv1.ProtocolTCP +} + +func toRestartPolicy(s types.ServiceConfig) (apiv1.RestartPolicy, error) { + if s.Deploy == nil || s.Deploy.RestartPolicy == nil { + return apiv1.RestartPolicyAlways, nil + } + policy := s.Deploy.RestartPolicy + + switch policy.Condition { + case string(swarm.RestartPolicyConditionAny): + return apiv1.RestartPolicyAlways, nil + case string(swarm.RestartPolicyConditionNone): + return apiv1.RestartPolicyNever, nil + case string(swarm.RestartPolicyConditionOnFailure): + return apiv1.RestartPolicyOnFailure, nil + default: + return "", errors.Errorf("unsupported restart policy %s", policy.Condition) + } +} + +func toResource(res *types.Resource) (apiv1.ResourceList, error) { + list := make(apiv1.ResourceList) + if res.NanoCPUs != "" { + cpus, err := resource.ParseQuantity(res.NanoCPUs) + if err != nil { + return nil, err + } + list[apiv1.ResourceCPU] = cpus + } + if res.MemoryBytes != 0 { + memory, err := resource.ParseQuantity(fmt.Sprintf("%v", res.MemoryBytes)) + if err != nil { + return nil, err + } + list[apiv1.ResourceMemory] = memory + } + return list, nil +} + +func toSecurityContext(s types.ServiceConfig) *apiv1.SecurityContext { + isPrivileged := toBoolPointer(s.Privileged) + isReadOnly := toBoolPointer(s.ReadOnly) + + var capabilities *apiv1.Capabilities + if s.CapAdd != nil || s.CapDrop != nil { + capabilities = &apiv1.Capabilities{ + Add: toCapabilities(s.CapAdd), + Drop: toCapabilities(s.CapDrop), + } + } + + var userID *int64 + if s.User != "" { + numerical, err := strconv.Atoi(s.User) + if err == nil { + unixUserID := int64(numerical) + userID = &unixUserID + } + } + + if isPrivileged == nil && isReadOnly == nil && capabilities == nil && userID == nil { + return nil + } + + return &apiv1.SecurityContext{ + RunAsUser: userID, + Privileged: isPrivileged, + ReadOnlyRootFilesystem: isReadOnly, + Capabilities: capabilities, + } +} + +func toBoolPointer(value bool) *bool { + if value { + return &value + } + + return nil +} + +func defaultUint64(v *uint64, defaultValue uint64) uint64 { //nolint: unparam + if v == nil { + return defaultValue + } + + return *v +} + +func toCapabilities(list []string) (capabilities []apiv1.Capability) { + for _, c := range list { + capabilities = append(capabilities, apiv1.Capability(c)) + } + return +} + +//nolint: unparam +func forceRestartPolicy(podTemplate apiv1.PodTemplateSpec, forcedRestartPolicy apiv1.RestartPolicy) apiv1.PodTemplateSpec { + if podTemplate.Spec.RestartPolicy != "" { + podTemplate.Spec.RestartPolicy = forcedRestartPolicy + } + + return podTemplate +} diff --git a/kube/charts/kubernetes/pod_test.go b/kube/charts/kubernetes/pod_test.go new file mode 100644 index 000000000..dee5dbb02 --- /dev/null +++ b/kube/charts/kubernetes/pod_test.go @@ -0,0 +1,1005 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "fmt" + "os" + "runtime" + "testing" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/stretchr/testify/assert" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func loadYAML(yaml string) (*loader.Config, error) { + dict, err := loader.ParseYAML([]byte(yaml)) + if err != nil { + return nil, err + } + workingDir, err := os.Getwd() + if err != nil { + panic(err) + } + configs := []types.ConfigFiles{} + config := types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: utils.Environment(), + } + model, err := loader.Load(config) + return model +} + +func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { + res, err := podTemplateWithError(yaml) + assert.NoError(t, err) + return res +} + +func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { + model, err := loadYAML(yaml) + if err != nil { + return apiv1.PodTemplateSpec{}, err + } + + return toPodTemplate(model.Services[0], nil, model) +} + +func TestToPodWithDockerSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" +`) + + expectedVolume := apiv1.Volume{ + Name: "mount-0", + VolumeSource: apiv1.VolumeSource{ + HostPath: &apiv1.HostPathVolumeSource{ + Path: "/var/run", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "mount-0", + MountPath: "/var/run/docker.sock", + SubPath: "docker.sock", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithFunkyCommand(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: basi/node-exporter + command: ["-collector.procfs", "/host/proc", "-collector.sysfs", "/host/sys"] +`) + + expectedArgs := []string{ + `-collector.procfs`, + `/host/proc`, // ? + `-collector.sysfs`, + `/host/sys`, // ? + } + assert.Equal(t, expectedArgs, podTemplate.Spec.Containers[0].Args) +} + +func TestToPodWithGlobalVolume(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + db: + image: "postgres:9.4" + volumes: + - dbdata:/var/lib/postgresql/data +`) + + expectedMount := apiv1.VolumeMount{ + Name: "dbdata", + MountPath: "/var/lib/postgresql/data", + } + assert.Len(t, podTemplate.Spec.Volumes, 0) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithResources(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + db: + image: "postgres:9.4" + deploy: + resources: + limits: + cpus: "0.001" + memory: 50Mb + reservations: + cpus: "0.0001" + memory: 20Mb +`) + + expectedResourceRequirements := apiv1.ResourceRequirements{ + Limits: map[apiv1.ResourceName]resource.Quantity{ + apiv1.ResourceCPU: resource.MustParse("0.001"), + apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 50*1024*1024)), + }, + Requests: map[apiv1.ResourceName]resource.Quantity{ + apiv1.ResourceCPU: resource.MustParse("0.0001"), + apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 20*1024*1024)), + }, + } + assert.Equal(t, expectedResourceRequirements, podTemplate.Spec.Containers[0].Resources) +} + +func TestToPodWithCapabilities(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + cap_add: + - ALL + cap_drop: + - NET_ADMIN + - SYS_ADMIN +`) + + expectedSecurityContext := &apiv1.SecurityContext{ + Capabilities: &apiv1.Capabilities{ + Add: []apiv1.Capability{"ALL"}, + Drop: []apiv1.Capability{"NET_ADMIN", "SYS_ADMIN"}, + }, + } + + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithReadOnly(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + read_only: true +`) + + yes := true + expectedSecurityContext := &apiv1.SecurityContext{ + ReadOnlyRootFilesystem: &yes, + } + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithPrivileged(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + privileged: true +`) + + yes := true + expectedSecurityContext := &apiv1.SecurityContext{ + Privileged: &yes, + } + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithEnvNilShouldErrorOut(t *testing.T) { + _, err := podTemplateWithError(` +version: "3" +services: + redis: + image: "redis:alpine" + environment: + - SESSION_SECRET +`) + assert.Error(t, err) +} + +func TestToPodWithEnv(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + environment: + - RACK_ENV=development + - SHOW=true +`) + + expectedEnv := []apiv1.EnvVar{ + { + Name: "RACK_ENV", + Value: "development", + }, + { + Name: "SHOW", + Value: "true", + }, + } + + assert.Equal(t, expectedEnv, podTemplate.Spec.Containers[0].Env) +} + +func TestToPodWithVolume(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + volumes: + - /ignore:/ignore + - /opt/data:/var/lib/mysql:ro +`) + + assert.Len(t, podTemplate.Spec.Volumes, 2) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 2) +} + +func /*FIXME Test*/ ToPodWithRelativeVolumes(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") + return + } + _, err := podTemplateWithError(` +version: "3" +services: + nginx: + image: nginx + volumes: + - ./fail:/ignore +`) + + assert.Error(t, err) +} + +func TestToPodWithHealthCheck(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 90s + timeout: 10s + retries: 3 +`) + + expectedLivenessProbe := &apiv1.Probe{ + TimeoutSeconds: 10, + PeriodSeconds: 90, + FailureThreshold: 3, + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: []string{"curl", "-f", "http://localhost"}, + }, + }, + } + + assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) +} + +func TestToPodWithShellHealthCheck(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost"] +`) + + expectedLivenessProbe := &apiv1.Probe{ + TimeoutSeconds: 1, + PeriodSeconds: 1, + FailureThreshold: 3, + Handler: apiv1.Handler{ + Exec: &apiv1.ExecAction{ + Command: []string{"sh", "-c", "curl -f http://localhost"}, + }, + }, + } + + assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) +} + +func TestToPodWithTargetlessExternalSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - my_secret +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external secrets use + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithExternalSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret + target: nginx_secret +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external secrets use + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/nginx_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ ToPodWithFileBasedSecret(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret +secrets: + my_secret: + file: ./secret.txt +`) + + expectedVolume := apiv1.Volume{ + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret", + Items: []apiv1.KeyToPath{ + { + Key: "secret.txt", + Path: "secret-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret", + SubPath: "secret-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ ToPodWithTwoFileBasedSecrets(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + secrets: + - source: my_secret1 + - source: my_secret2 + target: secret2 +secrets: + my_secret1: + file: ./secret1.txt + my_secret2: + file: ./secret2.txt +`) + + expectedVolumes := []apiv1.Volume{ + { + Name: "secret-0", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret1", + Items: []apiv1.KeyToPath{ + { + Key: "secret1.txt", + Path: "secret-0", + }, + }, + }, + }, + }, + { + Name: "secret-1", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: "my_secret2", + Items: []apiv1.KeyToPath{ + { + Key: "secret2.txt", + Path: "secret-1", + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "secret-0", + ReadOnly: true, + MountPath: "/run/secrets/my_secret1", + SubPath: "secret-0", + }, + { + Name: "secret-1", + ReadOnly: true, + MountPath: "/run/secrets/secret2", + SubPath: "secret-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func TestToPodWithTerminationGracePeriod(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + stop_grace_period: 100s +`) + + expected := int64(100) + assert.Equal(t, &expected, podTemplate.Spec.TerminationGracePeriodSeconds) +} + +func TestToPodWithTmpfs(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + tmpfs: + - /tmp +`) + + expectedVolume := apiv1.Volume{ + Name: "tmp-0", + VolumeSource: apiv1.VolumeSource{ + EmptyDir: &apiv1.EmptyDirVolumeSource{ + Medium: "Memory", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "tmp-0", + MountPath: "/tmp", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithNumericalUser(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + user: "1000" +`) + + userID := int64(1000) + + expectedSecurityContext := &apiv1.SecurityContext{ + RunAsUser: &userID, + } + + assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) +} + +func TestToPodWithGitVolume(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + volumes: + - source: "git@github.com:moby/moby.git" + target: /sources + type: git +`) + + expectedVolume := apiv1.Volume{ + Name: "mount-0", + VolumeSource: apiv1.VolumeSource{ + GitRepo: &apiv1.GitRepoVolumeSource{ + Repository: "git@github.com:moby/moby.git", + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "mount-0", + ReadOnly: false, + MountPath: "/sources", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ ToPodWithFileBasedConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - source: my_config + target: /usr/share/nginx/html/index.html + uid: "103" + gid: "103" + mode: 0440 +configs: + my_config: + file: ./file.html +`) + + mode := int32(0440) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "my_config", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file.html", + Path: "config-0", + Mode: &mode, + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/usr/share/nginx/html/index.html", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ ToPodWithTargetlessFileBasedConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - my_config +configs: + my_config: + file: ./file.html +`) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "myconfig", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file.html", + Path: "config-0", + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/myconfig", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func TestToPodWithExternalConfig(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + redis: + image: "redis:alpine" + configs: + - source: my_config + target: /usr/share/nginx/html/index.html + uid: "103" + gid: "103" + mode: 0440 +configs: + my_config: + external: true +`) + + mode := int32(0440) + + expectedVolume := apiv1.Volume{ + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "my_config", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", // TODO: This is the key we assume external config use + Path: "config-0", + Mode: &mode, + }, + }, + }, + }, + } + + expectedMount := apiv1.VolumeMount{ + Name: "config-0", + ReadOnly: true, + MountPath: "/usr/share/nginx/html/index.html", + SubPath: "config-0", + } + + assert.Len(t, podTemplate.Spec.Volumes, 1) + assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) + assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) +} + +func /*FIXME Test*/ ToPodWithTwoConfigsSameMountPoint(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + configs: + - source: first + target: /data/first.json + mode: "0440" + - source: second + target: /data/second.json + mode: "0550" +configs: + first: + file: ./file1 + secondv: + file: ./file2 +`) + + mode0440 := int32(0440) + mode0550 := int32(0550) + + expectedVolumes := []apiv1.Volume{ + { + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "first", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file1", + Path: "config-0", + Mode: &mode0440, + }, + }, + }, + }, + }, + { + Name: "config-1", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "second", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file2", + Path: "config-1", + Mode: &mode0550, + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "config-0", + ReadOnly: true, + MountPath: "/data/first.json", + SubPath: "config-0", + }, + { + Name: "config-1", + ReadOnly: true, + MountPath: "/data/second.json", + SubPath: "config-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func TestToPodWithTwoExternalConfigsSameMountPoint(t *testing.T) { + podTemplate := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + configs: + - source: first + target: /data/first.json + - source: second + target: /data/second.json +configs: + first: + file: ./file1 + second: + file: ./file2 +`) + + expectedVolumes := []apiv1.Volume{ + { + Name: "config-0", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "first", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", + Path: "config-0", + }, + }, + }, + }, + }, + { + Name: "config-1", + VolumeSource: apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: "second", + }, + Items: []apiv1.KeyToPath{ + { + Key: "file", + Path: "config-1", + }, + }, + }, + }, + }, + } + + expectedMounts := []apiv1.VolumeMount{ + { + Name: "config-0", + ReadOnly: true, + MountPath: "/data/first.json", + SubPath: "config-0", + }, + { + Name: "config-1", + ReadOnly: true, + MountPath: "/data/second.json", + SubPath: "config-1", + }, + } + + assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) + assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) +} + +func /*FIXME Test*/ ToPodWithPullSecret(t *testing.T) { + podTemplateWithSecret := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx + x-kubernetes.pull-secret: test-pull-secret +`) + + assert.Equal(t, 1, len(podTemplateWithSecret.Spec.ImagePullSecrets)) + assert.Equal(t, "test-pull-secret", podTemplateWithSecret.Spec.ImagePullSecrets[0].Name) + + podTemplateNoSecret := podTemplate(t, ` +version: "3" +services: + nginx: + image: nginx +`) + + assert.Nil(t, podTemplateNoSecret.Spec.ImagePullSecrets) +} + +func /*FIXME Test*/ ToPodWithPullPolicy(t *testing.T) { + cases := []struct { + name string + stack string + expectedPolicy apiv1.PullPolicy + expectedError string + }{ + { + name: "specific tag", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific +`, + expectedPolicy: apiv1.PullIfNotPresent, + }, + { + name: "latest tag", + stack: ` +version: "3" +services: + nginx: + image: nginx:latest +`, + expectedPolicy: apiv1.PullAlways, + }, + { + name: "explicit policy", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific + x-kubernetes.pull-policy: Never +`, + expectedPolicy: apiv1.PullNever, + }, + { + name: "invalid policy", + stack: ` +version: "3" +services: + nginx: + image: nginx:specific + x-kubernetes.pull-policy: Invalid +`, + expectedError: `invalid pull policy "Invalid", must be "Always", "IfNotPresent" or "Never"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + pod, err := podTemplateWithError(c.stack) + if c.expectedError != "" { + assert.EqualError(t, err, c.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, c.expectedPolicy) + } + }) + } +} diff --git a/kube/charts/kubernetes/volumes.go b/kube/charts/kubernetes/volumes.go new file mode 100644 index 000000000..6182cf055 --- /dev/null +++ b/kube/charts/kubernetes/volumes.go @@ -0,0 +1,246 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kubernetes + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/types" + + "github.com/pkg/errors" + apiv1 "k8s.io/api/core/v1" +) + +const dockerSock = "/var/run/docker.sock" + +type volumeSpec struct { + mount apiv1.VolumeMount + source *apiv1.VolumeSource +} + +func hasPersistentVolumes(s types.ServiceConfig) bool { + for _, volume := range s.Volumes { + if volume.Type == "volume" { + return true + } + } + + return false +} + +func toVolumeSpecs(project *types.Project, s types.ServiceConfig) ([]volumeSpec, error) { + var specs []volumeSpec + for i, m := range s.Volumes { + var source *apiv1.VolumeSource + name := fmt.Sprintf("mount-%d", i) + subpath := "" + if m.Source == dockerSock && m.Target == dockerSock { + subpath = "docker.sock" + source = hostPathVolume("/var/run") + } else if strings.HasSuffix(m.Source, ".git") { + source = gitVolume(m.Source) + } else if m.Type == "volume" { + if m.Source != "" { + name = strings.ReplaceAll(m.Source, "_", "-") + } + } else { + // bind mount + if !filepath.IsAbs(m.Source) { + return nil, errors.Errorf("%s: only absolute paths can be specified in mount source", m.Source) + } + if m.Source == "/" { + source = hostPathVolume("/") + } else { + parent, file := filepath.Split(m.Source) + if parent != "/" { + parent = strings.TrimSuffix(parent, "/") + } + source = hostPathVolume(parent) + subpath = file + } + } + + specs = append(specs, volumeSpec{ + source: source, + mount: volumeMount(name, m.Target, m.ReadOnly, subpath), + }) + } + + for i, m := range s.Tmpfs { + name := fmt.Sprintf("tmp-%d", i) + + specs = append(specs, volumeSpec{ + source: emptyVolumeInMemory(), + mount: volumeMount(name, m, false, ""), + }) + } + + for i, s := range s.Secrets { + name := fmt.Sprintf("secret-%d", i) + + target := path.Join("/run/secrets", or(s.Target, s.Source)) + subPath := name + readOnly := true + + specs = append(specs, volumeSpec{ + source: secretVolume(s, project.Secrets[name], subPath), + mount: volumeMount(name, target, readOnly, subPath), + }) + } + + for i, c := range s.Configs { + name := fmt.Sprintf("config-%d", i) + + target := or(c.Target, "/"+c.Source) + subPath := name + readOnly := true + + specs = append(specs, volumeSpec{ + source: configVolume(c, project.Configs[name], subPath), + mount: volumeMount(name, target, readOnly, subPath), + }) + } + + return specs, nil +} + +func or(v string, defaultValue string) string { + if v != "" && v != "." { + return v + } + + return defaultValue +} + +func toVolumeMounts(project *types.Project, s types.ServiceConfig) ([]apiv1.VolumeMount, error) { + var mounts []apiv1.VolumeMount + specs, err := toVolumeSpecs(project, s) + if err != nil { + return nil, err + } + for _, spec := range specs { + mounts = append(mounts, spec.mount) + } + return mounts, nil +} + +func toVolumes(project *types.Project, s types.ServiceConfig) ([]apiv1.Volume, error) { + var volumes []apiv1.Volume + specs, err := toVolumeSpecs(project, s) + if err != nil { + return nil, err + } + for _, spec := range specs { + if spec.source == nil { + spec.source = emptyVolumeInMemory() + } + volumes = append(volumes, apiv1.Volume{ + Name: spec.mount.Name, + VolumeSource: *spec.source, + }) + } + return volumes, nil +} + +func gitVolume(path string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + GitRepo: &apiv1.GitRepoVolumeSource{ + Repository: filepath.ToSlash(path), + }, + } +} + +func hostPathVolume(path string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + HostPath: &apiv1.HostPathVolumeSource{ + Path: path, + }, + } +} + +func defaultMode(mode *uint32) *int32 { + var defaultMode *int32 + + if mode != nil { + signedMode := int32(*mode) + defaultMode = &signedMode + } + + return defaultMode +} + +func secretVolume(config types.ServiceSecretConfig, topLevelConfig types.SecretConfig, subPath string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: config.Source, + Items: []apiv1.KeyToPath{ + { + Key: toKey(topLevelConfig.File), + Path: subPath, + Mode: defaultMode(config.Mode), + }, + }, + }, + } +} + +func volumeMount(name, path string, readOnly bool, subPath string) apiv1.VolumeMount { + return apiv1.VolumeMount{ + Name: name, + MountPath: path, + ReadOnly: readOnly, + SubPath: subPath, + } +} + +func configVolume(config types.ServiceConfigObjConfig, topLevelConfig types.ConfigObjConfig, subPath string) *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + ConfigMap: &apiv1.ConfigMapVolumeSource{ + LocalObjectReference: apiv1.LocalObjectReference{ + Name: config.Source, + }, + Items: []apiv1.KeyToPath{ + { + Key: toKey(topLevelConfig.File), + Path: subPath, + Mode: defaultMode(config.Mode), + }, + }, + }, + } +} + +func toKey(file string) string { + if file != "" { + return path.Base(file) + } + + return "file" // TODO: hard-coded key for external configs +} + +func emptyVolumeInMemory() *apiv1.VolumeSource { + return &apiv1.VolumeSource{ + EmptyDir: &apiv1.EmptyDirVolumeSource{ + Medium: apiv1.StorageMediumMemory, + }, + } +} diff --git a/kube/compose.go b/kube/compose.go new file mode 100644 index 000000000..33fb069df --- /dev/null +++ b/kube/compose.go @@ -0,0 +1,106 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kube + +import ( + "context" + + "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/context/store" + "github.com/docker/compose-cli/api/errdefs" + "github.com/docker/compose-cli/kube/charts" +) + +// NewComposeService create a kubernetes implementation of the compose.Service API +func NewComposeService(ctx store.KubeContext) (compose.Service, error) { + apiclient, err := charts.NewSDK(ctx) + if err != nil { + return nil, err + } + return &composeService{ + ctx: ctx, + sdk: apiclient, + }, nil +} + +type composeService struct { + ctx store.KubeContext + sdk charts.API +} + +// Up executes the equivalent to a `compose up` +func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { + return s.sdk.Install(project) +} + +// Down executes the equivalent to a `compose down` +func (s *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error { + return s.sdk.Uninstall(projectName) +} + +// List executes the equivalent to a `docker stack ls` +func (s *composeService) List(ctx context.Context, projectName string) ([]compose.Stack, error) { + return s.sdk.List(projectName) +} + +// Build executes the equivalent to a `compose build` +func (s *composeService) Build(ctx context.Context, project *types.Project) error { + return errdefs.ErrNotImplemented +} + +// Push executes the equivalent ot a `compose push` +func (s *composeService) Push(ctx context.Context, project *types.Project) error { + return errdefs.ErrNotImplemented +} + +// Pull executes the equivalent of a `compose pull` +func (s *composeService) Pull(ctx context.Context, project *types.Project) error { + return errdefs.ErrNotImplemented +} + +// Create executes the equivalent to a `compose create` +func (s *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error { + return errdefs.ErrNotImplemented +} + +// Start executes the equivalent to a `compose start` +func (s *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error { + return errdefs.ErrNotImplemented +} + +// Logs executes the equivalent to a `compose logs` +func (s *composeService) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer, options compose.LogOptions) error { + return errdefs.ErrNotImplemented +} + +// Ps executes the equivalent to a `compose ps` +func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) { + return nil, errdefs.ErrNotImplemented +} + +// Convert translate compose model into backend's native format +func (s *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) { + return nil, errdefs.ErrNotImplemented +} + +// RunOneOffContainer creates a service oneoff container and starts its dependencies +func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) error { + return errdefs.ErrNotImplemented +} diff --git a/kube/context.go b/kube/context.go new file mode 100644 index 000000000..a9ce58a74 --- /dev/null +++ b/kube/context.go @@ -0,0 +1,27 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package kube + +// ContextParams options for creating a Kubernetes context +type ContextParams struct { + Name string + Description string + Endpoint string + FromEnvironment bool +} diff --git a/kube/utils/config.go b/kube/utils/config.go new file mode 100644 index 000000000..408558923 --- /dev/null +++ b/kube/utils/config.go @@ -0,0 +1,142 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/prometheus/common/log" +) + +var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, error) { + configPath, err := getConfigPaths(configPaths) + if err != nil { + return "", nil, err + } + if configPath == nil { + return "", nil, nil + } + workingDir := filepath.Dir(configPath[0]) + + if name == "" { + name = os.Getenv("COMPOSE_PROJECT_NAME") + } + if name == "" { + r := regexp.MustCompile(`[^a-z0-9\\-_]+`) + name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") + } + + configs, err := parseConfigs(configPath) + if err != nil { + return "", nil, err + } + return workingDir, configs, nil +} + +func getConfigPaths(configPaths []string) ([]string, error) { + paths := []string{} + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + if len(configPaths) != 0 { + for _, f := range configPaths { + if f == "-" { + paths = append(paths, f) + continue + } + if !filepath.IsAbs(f) { + f = filepath.Join(pwd, f) + } + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil + } + + sep := os.Getenv("COMPOSE_FILE_SEPARATOR") + if sep == "" { + sep = string(os.PathListSeparator) + } + f := os.Getenv("COMPOSE_FILE") + if f != "" { + return strings.Split(f, sep), nil + } + + for { + candidates := []string{} + for _, n := range SupportedFilenames { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + log.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + log.Warnf("Using %s\n", winner) + } + return []string{winner}, nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + return nil, nil + } + pwd = parent + } +} + +func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { + files := []types.ConfigFile{} + for _, f := range configPaths { + var ( + b []byte + err error + ) + if f == "-" { + b, err = ioutil.ReadAll(os.Stdin) + } else { + if _, err := os.Stat(f); err != nil { + return nil, err + } + b, err = ioutil.ReadFile(f) + } + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(b) + if err != nil { + return nil, err + } + files = append(files, types.ConfigFile{Filename: f, Config: config}) + } + return files, nil +} diff --git a/kube/utils/errors.go b/kube/utils/errors.go new file mode 100644 index 000000000..0b58b9a54 --- /dev/null +++ b/kube/utils/errors.go @@ -0,0 +1,56 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "fmt" + "strings" +) + +func CombineErrors(errors []error) error { + if len(errors) == 0 { + return nil + } + if len(errors) == 1 { + return errors[0] + } + err := combinedError{} + for _, e := range errors { + if c, ok := e.(combinedError); ok { + err.errors = append(err.errors, c.errors...) + } else { + err.errors = append(err.errors, e) + } + } + return combinedError{errors} +} + +type combinedError struct { + errors []error +} + +func (c combinedError) Error() string { + points := make([]string, len(c.errors)) + for i, err := range c.errors { + points[i] = fmt.Sprintf("* %s", err.Error()) + } + return fmt.Sprintf( + "%d errors occurred:\n\t%s", + len(c.errors), strings.Join(points, "\n\t")) +} diff --git a/kube/utils/labels.go b/kube/utils/labels.go new file mode 100644 index 000000000..0ef3ecc07 --- /dev/null +++ b/kube/utils/labels.go @@ -0,0 +1,35 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +const ( + LabelDockerComposePrefix = "com.docker.compose" + LabelService = LabelDockerComposePrefix + ".service" + LabelVersion = LabelDockerComposePrefix + ".version" + LabelContainerNumber = LabelDockerComposePrefix + ".container-number" + LabelOneOff = LabelDockerComposePrefix + ".oneoff" + LabelNetwork = LabelDockerComposePrefix + ".network" + LabelSlug = LabelDockerComposePrefix + ".slug" + LabelVolume = LabelDockerComposePrefix + ".volume" + LabelConfigHash = LabelDockerComposePrefix + ".config-hash" + LabelProject = LabelDockerComposePrefix + ".project" + LabelWorkingDir = LabelDockerComposePrefix + ".working_dir" + LabelConfigFiles = LabelDockerComposePrefix + ".config_files" + LabelEnvironmentFile = LabelDockerComposePrefix + ".environment_file" +) diff --git a/kube/utils/utils.go b/kube/utils/utils.go new file mode 100644 index 000000000..dab4cd5fd --- /dev/null +++ b/kube/utils/utils.go @@ -0,0 +1,34 @@ +// +build kube + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package utils + +import ( + "os" + "strings" +) + +func Environment() map[string]string { + vars := make(map[string]string) + env := os.Environ() + for _, v := range env { + k := strings.SplitN(v, "=", 2) + vars[k[0]] = k[1] + } + return vars +} From f291b779027c0969372cfc9d4b9f22f270eff30d Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 19 Jan 2021 15:23:20 +0100 Subject: [PATCH 20/24] rename context to kubernetes Signed-off-by: aiordache --- api/context/store/store.go | 2 +- cli/cmd/context/create_kube.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/context/store/store.go b/api/context/store/store.go index 5427f7529..9b943a583 100644 --- a/api/context/store/store.go +++ b/api/context/store/store.go @@ -57,7 +57,7 @@ const ( LocalContextType = "local" // KubeContextType is the endpoint key in the context endpoints for a new // kube backend - KubeContextType = "kube" + KubeContextType = "kubernetes" ) const ( diff --git a/cli/cmd/context/create_kube.go b/cli/cmd/context/create_kube.go index c11e204f6..24a91d489 100644 --- a/cli/cmd/context/create_kube.go +++ b/cli/cmd/context/create_kube.go @@ -33,16 +33,16 @@ import ( func init() { extraCommands = append(extraCommands, createKubeCommand) extraHelp = append(extraHelp, ` -Create a Kube context: -$ docker context create kube CONTEXT [flags] -(see docker context create kube --help) +Create a Kubernetes context: +$ docker context create kubernetes CONTEXT [flags] +(see docker context create kubernetes --help) `) } func createKubeCommand() *cobra.Command { var opts kube.ContextParams cmd := &cobra.Command{ - Use: "kube CONTEXT [flags]", + Use: "kubernetes CONTEXT [flags]", Short: "Create context for a Kubernetes Cluster", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From 68b29f569baa80ad3c46a1b221890bb5da0f16e1 Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 19 Jan 2021 15:26:08 +0100 Subject: [PATCH 21/24] small fixes Signed-off-by: aiordache --- cli/cmd/context/create_kube.go | 8 +------- kube/backend.go | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/cli/cmd/context/create_kube.go b/cli/cmd/context/create_kube.go index 24a91d489..5e2260f65 100644 --- a/cli/cmd/context/create_kube.go +++ b/cli/cmd/context/create_kube.go @@ -20,7 +20,6 @@ package context import ( "context" - "fmt" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -74,13 +73,8 @@ func runCreateKube(ctx context.Context, contextName string, opts kube.ContextPar } func createContextData(ctx context.Context, opts kube.ContextParams) (interface{}, string, error) { - description := "" - if opts.Description != "" { - description = fmt.Sprintf("%s (%s)", opts.Description, description) - } - return store.KubeContext{ Endpoint: opts.Endpoint, FromEnvironment: opts.FromEnvironment, - }, description, nil + }, opts.Description, nil } diff --git a/kube/backend.go b/kube/backend.go index aa10cd884..2954c807a 100644 --- a/kube/backend.go +++ b/kube/backend.go @@ -44,7 +44,6 @@ func init() { } func service(ctx context.Context) (backend.Service, error) { - contextStore := store.ContextStore(ctx) currentContext := apicontext.CurrentContext(ctx) var kubeContext store.KubeContext From 1574ebdfffdea6ba96b96725e6cdbc782c3e8003 Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 20 Jan 2021 16:19:10 +0100 Subject: [PATCH 22/24] refactored compose in charts Signed-off-by: aiordache --- kube/compose/compose.go | 95 -- kube/compose/internal/env.go | 75 -- kube/compose/internal/helm/chart.go | 90 -- kube/compose/internal/helm/helm.go | 115 --- kube/compose/internal/kube/kube.go | 214 ---- kube/compose/internal/kube/placement.go | 126 --- kube/compose/internal/kube/placement_test.go | 163 --- kube/compose/internal/kube/pod.go | 349 ------- kube/compose/internal/kube/pod_test.go | 990 ------------------- kube/compose/internal/kube/volumes.go | 228 ----- kube/compose/internal/utils/config.go | 124 --- kube/compose/internal/utils/errors.go | 38 - kube/compose/internal/utils/labels.go | 17 - 13 files changed, 2624 deletions(-) delete mode 100644 kube/compose/compose.go delete mode 100644 kube/compose/internal/env.go delete mode 100644 kube/compose/internal/helm/chart.go delete mode 100644 kube/compose/internal/helm/helm.go delete mode 100644 kube/compose/internal/kube/kube.go delete mode 100644 kube/compose/internal/kube/placement.go delete mode 100644 kube/compose/internal/kube/placement_test.go delete mode 100644 kube/compose/internal/kube/pod.go delete mode 100644 kube/compose/internal/kube/pod_test.go delete mode 100644 kube/compose/internal/kube/volumes.go delete mode 100644 kube/compose/internal/utils/config.go delete mode 100644 kube/compose/internal/utils/errors.go delete mode 100644 kube/compose/internal/utils/labels.go diff --git a/kube/compose/compose.go b/kube/compose/compose.go deleted file mode 100644 index 8c9e47b9f..000000000 --- a/kube/compose/compose.go +++ /dev/null @@ -1,95 +0,0 @@ -package compose - -import ( - "errors" - "path/filepath" - "strings" - - "github.com/compose-spec/compose-go/types" - internal "github.com/docker/helm-prototype/pkg/compose/internal" - "github.com/docker/helm-prototype/pkg/compose/internal/helm" -) - -var Settings = internal.GetDefault() - -type ComposeProject struct { - config *types.Config - helm *helm.HelmActions - ProjectDir string - Name string `yaml:"-" json:"-"` -} - -func Load(name string, configpaths []string) (*ComposeProject, error) { - model, workingDir, err := internal.GetConfig(name, configpaths) - if err != nil { - return nil, err - } - - if name == "" { - if model != nil { - name = filepath.Base(filepath.Dir(model.Filename)) - } else if workingDir != "" { - name = filepath.Base(filepath.Dir(workingDir)) - } - } - - return &ComposeProject{ - config: model, - helm: helm.NewHelmActions(nil), - ProjectDir: workingDir, - Name: name, - }, nil -} - -func (cp *ComposeProject) GenerateChart(dirname string) error { - if cp.config == nil { - return errors.New(`Can't find a suitable configuration file in this directory or any -parent. Are you in the right directory?`) - } - if dirname == "" { - dirname = cp.config.Filename - if strings.Contains(dirname, ".") { - splits := strings.SplitN(dirname, ".", 2) - dirname = splits[0] - } - } - name := filepath.Base(dirname) - dirname = filepath.Dir(dirname) - return internal.SaveChart(cp.config, name, dirname) -} - -func (cp *ComposeProject) Install(name, path string) error { - if path != "" { - return cp.helm.InstallChartFromDir(name, path) - } - if cp.config == nil { - return errors.New(`Can't find a suitable configuration file in this directory or any -parent. Are you in the right directory?`) - } - if name == "" { - name = cp.Name - } - chart, err := internal.GetChartInMemory(cp.config, name) - if err != nil { - return err - } - return cp.helm.InstallChart(name, chart) -} - -func (cp *ComposeProject) Uninstall(name string) error { - if name == "" { - if cp.config == nil { - return errors.New(`Can't find a suitable configuration file in this directory or any -parent. Are you in the right directory? - -Alternative: uninstall [INSTALLATION NAME] -`) - } - name = cp.Name - } - return cp.helm.Uninstall(name) -} - -func (cp *ComposeProject) List() (map[string]interface{}, error) { - return cp.helm.ListReleases() -} diff --git a/kube/compose/internal/env.go b/kube/compose/internal/env.go deleted file mode 100644 index 141f15b1e..000000000 --- a/kube/compose/internal/env.go +++ /dev/null @@ -1,75 +0,0 @@ -package env - -import ( - "os" - "strings" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/docker/helm-prototype/pkg/compose/internal/helm" - "github.com/docker/helm-prototype/pkg/compose/internal/kube" - "github.com/docker/helm-prototype/pkg/compose/internal/utils" - chart "helm.sh/helm/v3/pkg/chart" - util "helm.sh/helm/v3/pkg/chartutil" - helmenv "helm.sh/helm/v3/pkg/cli" -) - -func GetDefault() *helmenv.EnvSettings { - return helmenv.New() -} - -func Environment() map[string]string { - vars := make(map[string]string) - env := os.Environ() - for _, v := range env { - k := strings.SplitN(v, "=", 2) - vars[k[0]] = k[1] - } - return vars -} - -func GetConfig(name string, configPaths []string) (*types.Config, string, error) { - workingDir, configs, err := utils.GetConfigs( - name, - configPaths, - ) - if err != nil { - return nil, "", err - } - if configs == nil { - return nil, "", nil - } - config, err := loader.Load(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: configs, - Environment: Environment(), - }) - if err != nil { - return nil, "", err - } - return config, workingDir, nil -} - -func GetChartInMemory(config *types.Config, name string) (*chart.Chart, error) { - for k, v := range config.Volumes { - volumeName := strings.ReplaceAll(k, "_", "-") - if volumeName != k { - config.Volumes[volumeName] = v - delete(config.Volumes, k) - } - } - objects, err := kube.MapToKubernetesObjects(config, name) - if err != nil { - return nil, err - } - //in memory files - return helm.ConvertToChart(name, objects) -} - -func SaveChart(config *types.Config, name, dest string) error { - chart, err := GetChartInMemory(config, name) - if err != nil { - return err - } - return util.SaveDir(chart, dest) -} diff --git a/kube/compose/internal/helm/chart.go b/kube/compose/internal/helm/chart.go deleted file mode 100644 index 26a139f4b..000000000 --- a/kube/compose/internal/helm/chart.go +++ /dev/null @@ -1,90 +0,0 @@ -package helm - -import ( - "bytes" - "encoding/json" - "html/template" - "path/filepath" - - "gopkg.in/yaml.v3" - - chart "helm.sh/helm/v3/pkg/chart" - loader "helm.sh/helm/v3/pkg/chart/loader" - "k8s.io/apimachinery/pkg/runtime" -) - -func ConvertToChart(name string, objects map[string]runtime.Object) (*chart.Chart, error) { - - files := []*loader.BufferedFile{ - &loader.BufferedFile{ - Name: "README.md", - Data: []byte("This chart was created by converting a Compose file"), - }} - - chart := `name: {{.Name}} -description: A generated Helm Chart for {{.Name}} from Skippbox Kompose -version: 0.0.1 -apiVersion: v1 -keywords: - - {{.Name}} -sources: -home: -` - - t, err := template.New("ChartTmpl").Parse(chart) - if err != nil { - return nil, err - } - type ChartDetails struct { - Name string - } - var chartData bytes.Buffer - err = t.Execute(&chartData, ChartDetails{Name: name}) - if err != nil { - return nil, err - } - files = append(files, &loader.BufferedFile{ - Name: "Chart.yaml", - Data: chartData.Bytes(), - }) - - for name, o := range objects { - j, err := json.Marshal(o) - if err != nil { - return nil, err - } - buf, err := jsonToYaml(j, 2) - if err != nil { - return nil, err - } - files = append(files, &loader.BufferedFile{ - Name: filepath.Join("templates", name), - Data: buf, - }) - - } - return loader.LoadFiles(files) -} - -// Convert JSON to YAML. -func jsonToYaml(j []byte, spaces int) ([]byte, error) { - // Convert the JSON to an object. - var jsonObj interface{} - // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the - // Go JSON library doesn't try to pick the right number type (int, float, - // etc.) when unmarshling to interface{}, it just picks float64 - // universally. go-yaml does go through the effort of picking the right - // number type, so we can preserve number type throughout this process. - err := yaml.Unmarshal(j, &jsonObj) - if err != nil { - return nil, err - } - - var b bytes.Buffer - encoder := yaml.NewEncoder(&b) - encoder.SetIndent(spaces) - if err := encoder.Encode(jsonObj); err != nil { - return nil, err - } - return b.Bytes(), nil -} diff --git a/kube/compose/internal/helm/helm.go b/kube/compose/internal/helm/helm.go deleted file mode 100644 index 60cb39df0..000000000 --- a/kube/compose/internal/helm/helm.go +++ /dev/null @@ -1,115 +0,0 @@ -package helm - -import ( - "errors" - "log" - - action "helm.sh/helm/v3/pkg/action" - chart "helm.sh/helm/v3/pkg/chart" - loader "helm.sh/helm/v3/pkg/chart/loader" - env "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/release" -) - -type HelmActions struct { - Config *action.Configuration - Settings *env.EnvSettings - kube_conn_init bool -} - -func NewHelmActions(settings *env.EnvSettings) *HelmActions { - if settings == nil { - settings = env.New() - } - return &HelmActions{ - Config: new(action.Configuration), - Settings: settings, - kube_conn_init: false, - } -} - -func (hc *HelmActions) initKubeClient() error { - if hc.kube_conn_init { - return nil - } - if err := hc.Config.Init( - hc.Settings.RESTClientGetter(), - hc.Settings.Namespace(), - "configmap", - log.Printf, - ); err != nil { - log.Fatal(err) - } - if err := hc.Config.KubeClient.IsReachable(); err != nil { - return err - } - hc.kube_conn_init = true - return nil -} - -func (hc *HelmActions) InstallChartFromDir(name string, chartpath string) error { - chart, err := loader.Load(chartpath) - if err != nil { - return err - } - return hc.InstallChart(name, chart) -} - -func (hc *HelmActions) InstallChart(name string, chart *chart.Chart) error { - hc.initKubeClient() - - actInstall := action.NewInstall(hc.Config) - actInstall.ReleaseName = name - actInstall.Namespace = hc.Settings.Namespace() - - release, err := actInstall.Run(chart, map[string]interface{}{}) - if err != nil { - return err - } - log.Println("Release status: ", release.Info.Status) - log.Println(release.Info.Description) - return nil -} - -func (hc *HelmActions) Uninstall(name string) error { - hc.initKubeClient() - release, err := hc.Get(name) - if err != nil { - return err - } - if release == nil { - return errors.New("No release found with the name provided.") - } - actUninstall := action.NewUninstall(hc.Config) - response, err := actUninstall.Run(name) - if err != nil { - return err - } - log.Println(response.Release.Info.Description) - return nil -} - -func (hc *HelmActions) Get(name string) (*release.Release, error) { - hc.initKubeClient() - - actGet := action.NewGet(hc.Config) - return actGet.Run(name) -} - -func (hc *HelmActions) ListReleases() (map[string]interface{}, error) { - hc.initKubeClient() - - actList := action.NewList(hc.Config) - releases, err := actList.Run() - if err != nil { - return map[string]interface{}{}, err - } - result := map[string]interface{}{} - for _, rel := range releases { - result[rel.Name] = map[string]string{ - "Status": string(rel.Info.Status), - "Description": rel.Info.Description, - } - } - return result, nil -} diff --git a/kube/compose/internal/kube/kube.go b/kube/compose/internal/kube/kube.go deleted file mode 100644 index 713235157..000000000 --- a/kube/compose/internal/kube/kube.go +++ /dev/null @@ -1,214 +0,0 @@ -package kube - -import ( - "fmt" - "log" - "strings" - "time" - - "github.com/compose-spec/compose-go/types" - apps "k8s.io/api/apps/v1" - core "k8s.io/api/core/v1" - resource "k8s.io/apimachinery/pkg/api/resource" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func MapToKubernetesObjects(model *types.Config, name string) (map[string]runtime.Object, error) { - objects := map[string]runtime.Object{} - - for _, service := range model.Services { - svcObject := mapToService(model, service) - if svcObject != nil { - objects[fmt.Sprintf("%s-service.yaml", service.Name)] = svcObject - } else { - log.Println("Missing port mapping from service config.") - } - - if service.Deploy != nil && service.Deploy.Mode == "global" { - daemonset, err := mapToDaemonset(service, model, name) - if err != nil { - return nil, err - } - objects[fmt.Sprintf("%s-daemonset.yaml", service.Name)] = daemonset - } else { - deployment, err := mapToDeployment(service, model, name) - if err != nil { - return nil, err - } - objects[fmt.Sprintf("%s-deployment.yaml", service.Name)] = deployment - } - for _, vol := range service.Volumes { - if vol.Type == "volume" { - vol.Source = strings.ReplaceAll(vol.Source, "_", "-") - objects[fmt.Sprintf("%s-persistentvolumeclaim.yaml", vol.Source)] = mapToPVC(service, vol) - } - } - } - return objects, nil -} - -func mapToService(model *types.Config, service types.ServiceConfig) *core.Service { - ports := []core.ServicePort{} - for _, p := range service.Ports { - ports = append(ports, - core.ServicePort{ - Name: fmt.Sprintf("%d-%s", p.Target, strings.ToLower(string(p.Protocol))), - Port: int32(p.Target), - TargetPort: intstr.FromInt(int(p.Target)), - Protocol: toProtocol(p.Protocol), - }) - } - if len(ports) == 0 { - return nil - } - return &core.Service{ - TypeMeta: meta.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - }, - ObjectMeta: meta.ObjectMeta{ - Name: service.Name, - }, - Spec: core.ServiceSpec{ - Selector: map[string]string{"com.docker.compose.service": service.Name}, - Ports: ports, - Type: mapServiceToServiceType(service, model), - }, - } -} - -func mapServiceToServiceType(service types.ServiceConfig, model *types.Config) core.ServiceType { - serviceType := core.ServiceTypeClusterIP - if len(service.Networks) == 0 { - // service is implicitly attached to "default" network - serviceType = core.ServiceTypeLoadBalancer - } - for name := range service.Networks { - if !model.Networks[name].Internal { - serviceType = core.ServiceTypeLoadBalancer - } - } - for _, port := range service.Ports { - if port.Published != 0 { - serviceType = core.ServiceTypeNodePort - } - } - return serviceType -} - -func mapToDeployment(service types.ServiceConfig, model *types.Config, name string) (*apps.Deployment, error) { - labels := map[string]string{ - "com.docker.compose.service": service.Name, - "com.docker.compose.project": name, - } - podTemplate, err := toPodTemplate(service, labels, model) - if err != nil { - return nil, err - } - selector := new(meta.LabelSelector) - selector.MatchLabels = make(map[string]string) - for key, val := range labels { - selector.MatchLabels[key] = val - } - return &apps.Deployment{ - TypeMeta: meta.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1", - }, - ObjectMeta: meta.ObjectMeta{ - Name: service.Name, - Labels: labels, - }, - Spec: apps.DeploymentSpec{ - Selector: selector, - Replicas: toReplicas(service.Deploy), - Strategy: toDeploymentStrategy(service.Deploy), - Template: podTemplate, - }, - }, nil -} - -func mapToDaemonset(service types.ServiceConfig, model *types.Config, name string) (*apps.DaemonSet, error) { - labels := map[string]string{ - "com.docker.compose.service": service.Name, - "com.docker.compose.project": name, - } - podTemplate, err := toPodTemplate(service, labels, model) - if err != nil { - return nil, err - } - - return &apps.DaemonSet{ - ObjectMeta: meta.ObjectMeta{ - Name: service.Name, - Labels: labels, - }, - Spec: apps.DaemonSetSpec{ - Template: podTemplate, - }, - }, nil -} - -func toReplicas(deploy *types.DeployConfig) *int32 { - v := int32(1) - if deploy != nil { - v = int32(*deploy.Replicas) - } - return &v -} - -func toDeploymentStrategy(deploy *types.DeployConfig) apps.DeploymentStrategy { - if deploy == nil || deploy.UpdateConfig == nil { - return apps.DeploymentStrategy{ - Type: apps.RecreateDeploymentStrategyType, - } - } - return apps.DeploymentStrategy{ - Type: apps.RollingUpdateDeploymentStrategyType, - RollingUpdate: &apps.RollingUpdateDeployment{ - MaxUnavailable: &intstr.IntOrString{ - Type: intstr.Int, - IntVal: int32(*deploy.UpdateConfig.Parallelism), - }, - MaxSurge: nil, - }, - } -} - -func mapToPVC(service types.ServiceConfig, vol types.ServiceVolumeConfig) runtime.Object { - rwaccess := core.ReadWriteOnce - if vol.ReadOnly { - rwaccess = core.ReadOnlyMany - } - return &core.PersistentVolumeClaim{ - TypeMeta: meta.TypeMeta{ - Kind: "PersistentVolumeClaim", - APIVersion: "v1", - }, - ObjectMeta: meta.ObjectMeta{ - Name: vol.Source, - Labels: map[string]string{"com.docker.compose.service": service.Name}, - }, - Spec: core.PersistentVolumeClaimSpec{ - VolumeName: vol.Source, - AccessModes: []core.PersistentVolumeAccessMode{rwaccess}, - Resources: core.ResourceRequirements{ - Requests: core.ResourceList{ - core.ResourceStorage: resource.MustParse("100Mi"), - }, - }, - }, - } -} - -// toSecondsOrDefault converts a duration string in seconds and defaults to a -// given value if the duration is nil. -// The supported units are us, ms, s, m and h. -func toSecondsOrDefault(duration *types.Duration, defaultValue int32) int32 { //nolint: unparam - if duration == nil { - return defaultValue - } - return int32(time.Duration(*duration).Seconds()) -} diff --git a/kube/compose/internal/kube/placement.go b/kube/compose/internal/kube/placement.go deleted file mode 100644 index cb56ca7ce..000000000 --- a/kube/compose/internal/kube/placement.go +++ /dev/null @@ -1,126 +0,0 @@ -package kube - -import ( - "regexp" - "strings" - - "github.com/compose-spec/compose-go/types" - "github.com/pkg/errors" - apiv1 "k8s.io/api/core/v1" -) - -var constraintEquals = regexp.MustCompile(`([\w\.]*)\W*(==|!=)\W*([\w\.]*)`) - -const ( - kubernetesOs = "beta.kubernetes.io/os" - kubernetesArch = "beta.kubernetes.io/arch" - kubernetesHostname = "kubernetes.io/hostname" -) - -// node.id Node ID node.id == 2ivku8v2gvtg4 -// node.hostname Node hostname node.hostname != node-2 -// node.role Node role node.role == manager -// node.labels user defined node labels node.labels.security == high -// engine.labels Docker Engine's labels engine.labels.operatingsystem == ubuntu 14.04 -func toNodeAffinity(deploy *types.DeployConfig) (*apiv1.Affinity, error) { - constraints := []string{} - if deploy != nil && deploy.Placement.Constraints != nil { - constraints = deploy.Placement.Constraints - } - requirements := []apiv1.NodeSelectorRequirement{} - for _, constraint := range constraints { - matches := constraintEquals.FindStringSubmatch(constraint) - if len(matches) == 4 { - key := matches[1] - operator, err := toRequirementOperator(matches[2]) - if err != nil { - return nil, err - } - value := matches[3] - - switch { - case key == constraintOs: - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: kubernetesOs, - Operator: operator, - Values: []string{value}, - }) - case key == constraintArch: - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: kubernetesArch, - Operator: operator, - Values: []string{value}, - }) - case key == constraintHostname: - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: kubernetesHostname, - Operator: operator, - Values: []string{value}, - }) - case strings.HasPrefix(key, constraintLabelPrefix): - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: strings.TrimPrefix(key, constraintLabelPrefix), - Operator: operator, - Values: []string{value}, - }) - } - } - } - - if !hasRequirement(requirements, kubernetesOs) { - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: kubernetesOs, - Operator: apiv1.NodeSelectorOpIn, - Values: []string{"linux"}, - }) - } - if !hasRequirement(requirements, kubernetesArch) { - requirements = append(requirements, apiv1.NodeSelectorRequirement{ - Key: kubernetesArch, - Operator: apiv1.NodeSelectorOpIn, - Values: []string{"amd64"}, - }) - } - return &apiv1.Affinity{ - NodeAffinity: &apiv1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ - NodeSelectorTerms: []apiv1.NodeSelectorTerm{ - { - MatchExpressions: requirements, - }, - }, - }, - }, - }, nil -} - -const ( - constraintOs = "node.platform.os" - constraintArch = "node.platform.arch" - constraintHostname = "node.hostname" - constraintLabelPrefix = "node.labels." -) - -func hasRequirement(requirements []apiv1.NodeSelectorRequirement, key string) bool { - for _, r := range requirements { - if r.Key == key { - return true - } - } - return false -} - -func toRequirementOperator(sign string) (apiv1.NodeSelectorOperator, error) { - switch sign { - case "==": - return apiv1.NodeSelectorOpIn, nil - case "!=": - return apiv1.NodeSelectorOpNotIn, nil - case ">": - return apiv1.NodeSelectorOpGt, nil - case "<": - return apiv1.NodeSelectorOpLt, nil - default: - return "", errors.Errorf("operator %s not supported", sign) - } -} diff --git a/kube/compose/internal/kube/placement_test.go b/kube/compose/internal/kube/placement_test.go deleted file mode 100644 index 77d3d6db5..000000000 --- a/kube/compose/internal/kube/placement_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package kube - -import ( - "reflect" - "sort" - "testing" - - "github.com/compose-spec/compose-go/types" - - "github.com/stretchr/testify/assert" - apiv1 "k8s.io/api/core/v1" -) - -func TestToPodWithPlacement(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: redis:alpine - deploy: - placement: - constraints: - - node.platform.os == linux - - node.platform.arch == amd64 - - node.hostname == node01 - - node.labels.label1 == value1 - - node.labels.label2.subpath != value2 -`) - - expectedRequirements := []apiv1.NodeSelectorRequirement{ - {Key: "beta.kubernetes.io/os", Operator: apiv1.NodeSelectorOpIn, Values: []string{"linux"}}, - {Key: "beta.kubernetes.io/arch", Operator: apiv1.NodeSelectorOpIn, Values: []string{"amd64"}}, - {Key: "kubernetes.io/hostname", Operator: apiv1.NodeSelectorOpIn, Values: []string{"node01"}}, - {Key: "label1", Operator: apiv1.NodeSelectorOpIn, Values: []string{"value1"}}, - {Key: "label2.subpath", Operator: apiv1.NodeSelectorOpNotIn, Values: []string{"value2"}}, - } - - requirements := podTemplate.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions - - sort.Slice(expectedRequirements, func(i, j int) bool { return expectedRequirements[i].Key < expectedRequirements[j].Key }) - sort.Slice(requirements, func(i, j int) bool { return requirements[i].Key < requirements[j].Key }) - - assert.EqualValues(t, expectedRequirements, requirements) -} - -type keyValue struct { - key string - value string -} - -func kv(key, value string) keyValue { - return keyValue{key: key, value: value} -} - -func makeExpectedAffinity(kvs ...keyValue) *apiv1.Affinity { - - var matchExpressions []apiv1.NodeSelectorRequirement - for _, kv := range kvs { - matchExpressions = append( - matchExpressions, - apiv1.NodeSelectorRequirement{ - Key: kv.key, - Operator: apiv1.NodeSelectorOpIn, - Values: []string{kv.value}, - }, - ) - } - return &apiv1.Affinity{ - NodeAffinity: &apiv1.NodeAffinity{ - RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{ - NodeSelectorTerms: []apiv1.NodeSelectorTerm{ - { - MatchExpressions: matchExpressions, - }, - }, - }, - }, - } -} - -func TestNodeAfinity(t *testing.T) { - cases := []struct { - name string - source []string - expected *apiv1.Affinity - }{ - { - name: "nil", - expected: makeExpectedAffinity( - kv(kubernetesOs, "linux"), - kv(kubernetesArch, "amd64"), - ), - }, - { - name: "hostname", - source: []string{"node.hostname == test"}, - expected: makeExpectedAffinity( - kv(kubernetesHostname, "test"), - kv(kubernetesOs, "linux"), - kv(kubernetesArch, "amd64"), - ), - }, - { - name: "os", - source: []string{"node.platform.os == windows"}, - expected: makeExpectedAffinity( - kv(kubernetesOs, "windows"), - kv(kubernetesArch, "amd64"), - ), - }, - { - name: "arch", - source: []string{"node.platform.arch == arm64"}, - expected: makeExpectedAffinity( - kv(kubernetesArch, "arm64"), - kv(kubernetesOs, "linux"), - ), - }, - { - name: "custom-labels", - source: []string{"node.platform.os == windows", "node.platform.arch == arm64"}, - expected: makeExpectedAffinity( - kv(kubernetesArch, "arm64"), - kv(kubernetesOs, "windows"), - ), - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - result, err := toNodeAffinity(&types.DeployConfig{ - Placement: types.Placement{ - Constraints: c.source, - }, - }) - assert.NoError(t, err) - assert.True(t, nodeAffinityMatch(c.expected, result)) - }) - } -} - -func nodeSelectorRequirementsToMap(source []apiv1.NodeSelectorRequirement, result map[string]apiv1.NodeSelectorRequirement) { - for _, t := range source { - result[t.Key] = t - } -} - -func nodeAffinityMatch(expected, actual *apiv1.Affinity) bool { - expectedTerms := expected.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms - actualTerms := actual.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms - expectedExpressions := make(map[string]apiv1.NodeSelectorRequirement) - expectedFields := make(map[string]apiv1.NodeSelectorRequirement) - actualExpressions := make(map[string]apiv1.NodeSelectorRequirement) - actualFields := make(map[string]apiv1.NodeSelectorRequirement) - for _, v := range expectedTerms { - nodeSelectorRequirementsToMap(v.MatchExpressions, expectedExpressions) - nodeSelectorRequirementsToMap(v.MatchFields, expectedFields) - } - for _, v := range actualTerms { - nodeSelectorRequirementsToMap(v.MatchExpressions, actualExpressions) - nodeSelectorRequirementsToMap(v.MatchFields, actualFields) - } - return reflect.DeepEqual(expectedExpressions, actualExpressions) && reflect.DeepEqual(expectedFields, actualFields) -} diff --git a/kube/compose/internal/kube/pod.go b/kube/compose/internal/kube/pod.go deleted file mode 100644 index 18102a568..000000000 --- a/kube/compose/internal/kube/pod.go +++ /dev/null @@ -1,349 +0,0 @@ -package kube - -import ( - "fmt" - "sort" - "strconv" - "strings" - "time" - - "github.com/compose-spec/compose-go/types" - "github.com/docker/docker/api/types/swarm" - - "github.com/pkg/errors" - apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func toPodTemplate(serviceConfig types.ServiceConfig, labels map[string]string, model *types.Config) (apiv1.PodTemplateSpec, error) { - tpl := apiv1.PodTemplateSpec{} - //nodeAffinity, err := toNodeAffinity(serviceConfig.Deploy) - //if err != nil { - // return apiv1.PodTemplateSpec{}, err - //} - hostAliases, err := toHostAliases(serviceConfig.ExtraHosts) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - env, err := toEnv(serviceConfig.Environment) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - restartPolicy, err := toRestartPolicy(serviceConfig) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - - var limits apiv1.ResourceList - if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Limits != nil { - limits, err = toResource(serviceConfig.Deploy.Resources.Limits) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - } - var requests apiv1.ResourceList - if serviceConfig.Deploy != nil && serviceConfig.Deploy.Resources.Reservations != nil { - requests, err = toResource(serviceConfig.Deploy.Resources.Reservations) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - } - - volumes, err := toVolumes(serviceConfig, model) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - volumeMounts, err := toVolumeMounts(serviceConfig, model) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - /* pullPolicy, err := toImagePullPolicy(serviceConfig.Image, x-kubernetes-pull-policy) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } */ - tpl.ObjectMeta = metav1.ObjectMeta{ - Labels: labels, - Annotations: serviceConfig.Labels, - } - tpl.Spec.RestartPolicy = restartPolicy - tpl.Spec.Volumes = volumes - tpl.Spec.HostPID = toHostPID(serviceConfig.Pid) - tpl.Spec.HostIPC = toHostIPC(serviceConfig.Ipc) - tpl.Spec.Hostname = serviceConfig.Hostname - tpl.Spec.TerminationGracePeriodSeconds = toTerminationGracePeriodSeconds(serviceConfig.StopGracePeriod) - tpl.Spec.HostAliases = hostAliases - //tpl.Spec.Affinity = nodeAffinity - // we dont want to remove all containers and recreate them because: - // an admission plugin can add sidecar containers - // we for sure want to keep the main container to be additive - if len(tpl.Spec.Containers) == 0 { - tpl.Spec.Containers = []apiv1.Container{{}} - } - - containerIX := 0 - for ix, c := range tpl.Spec.Containers { - if c.Name == serviceConfig.Name { - containerIX = ix - break - } - } - tpl.Spec.Containers[containerIX].Name = serviceConfig.Name - tpl.Spec.Containers[containerIX].Image = serviceConfig.Image - // FIXME tpl.Spec.Containers[containerIX].ImagePullPolicy = pullPolicy - tpl.Spec.Containers[containerIX].Command = serviceConfig.Entrypoint - tpl.Spec.Containers[containerIX].Args = serviceConfig.Command - tpl.Spec.Containers[containerIX].WorkingDir = serviceConfig.WorkingDir - tpl.Spec.Containers[containerIX].TTY = serviceConfig.Tty - tpl.Spec.Containers[containerIX].Stdin = serviceConfig.StdinOpen - tpl.Spec.Containers[containerIX].Ports = toPorts(serviceConfig.Ports) - tpl.Spec.Containers[containerIX].LivenessProbe = toLivenessProbe(serviceConfig.HealthCheck) - tpl.Spec.Containers[containerIX].Env = env - tpl.Spec.Containers[containerIX].VolumeMounts = volumeMounts - tpl.Spec.Containers[containerIX].SecurityContext = toSecurityContext(serviceConfig) - tpl.Spec.Containers[containerIX].Resources = apiv1.ResourceRequirements{ - Limits: limits, - Requests: requests, - } - - /* FIXME - if serviceConfig.PullSecret != "" { - pullSecrets := map[string]struct{}{} - for _, ps := range tpl.Spec.ImagePullSecrets { - pullSecrets[ps.Name] = struct{}{} - } - if _, ok := pullSecrets[serviceConfig.PullSecret]; !ok { - tpl.Spec.ImagePullSecrets = append(tpl.Spec.ImagePullSecrets, apiv1.LocalObjectReference{Name: serviceConfig.PullSecret}) - } - } - */ - return tpl, nil -} - -func toImagePullPolicy(image string, specifiedPolicy string) (apiv1.PullPolicy, error) { - if specifiedPolicy == "" { - if strings.HasSuffix(image, ":latest") { - return apiv1.PullAlways, nil - } - return apiv1.PullIfNotPresent, nil - } - switch apiv1.PullPolicy(specifiedPolicy) { - case apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever: - return apiv1.PullPolicy(specifiedPolicy), nil - default: - return "", errors.Errorf("invalid pull policy %q, must be %q, %q or %q", specifiedPolicy, apiv1.PullAlways, apiv1.PullIfNotPresent, apiv1.PullNever) - } -} - -func toHostAliases(extraHosts []string) ([]apiv1.HostAlias, error) { - if extraHosts == nil { - return nil, nil - } - - byHostnames := map[string]string{} - for _, host := range extraHosts { - split := strings.SplitN(host, ":", 2) - if len(split) != 2 { - return nil, errors.Errorf("malformed host %s", host) - } - byHostnames[split[0]] = split[1] - } - - byIPs := map[string][]string{} - for k, v := range byHostnames { - byIPs[v] = append(byIPs[v], k) - } - - aliases := make([]apiv1.HostAlias, len(byIPs)) - i := 0 - for key, hosts := range byIPs { - sort.Strings(hosts) - aliases[i] = apiv1.HostAlias{ - IP: key, - Hostnames: hosts, - } - i++ - } - sort.Slice(aliases, func(i, j int) bool { return aliases[i].IP < aliases[j].IP }) - return aliases, nil -} - -func toHostPID(pid string) bool { - return "host" == pid -} - -func toHostIPC(ipc string) bool { - return "host" == ipc -} - -func toTerminationGracePeriodSeconds(duration *types.Duration) *int64 { - if duration == nil { - return nil - } - gracePeriod := int64(time.Duration(*duration).Seconds()) - return &gracePeriod -} - -func toLivenessProbe(hc *types.HealthCheckConfig) *apiv1.Probe { - if hc == nil || len(hc.Test) < 1 || hc.Test[0] == "NONE" { - return nil - } - - command := hc.Test[1:] - if hc.Test[0] == "CMD-SHELL" { - command = append([]string{"sh", "-c"}, command...) - } - - return &apiv1.Probe{ - TimeoutSeconds: toSecondsOrDefault(hc.Timeout, 1), - PeriodSeconds: toSecondsOrDefault(hc.Interval, 1), - FailureThreshold: int32(defaultUint64(hc.Retries, 3)), - Handler: apiv1.Handler{ - Exec: &apiv1.ExecAction{ - Command: command, - }, - }, - } -} - -func toEnv(env map[string]*string) ([]apiv1.EnvVar, error) { - var envVars []apiv1.EnvVar - - for k, v := range env { - if v == nil { - return nil, errors.Errorf("%s has no value, unsetting an environment variable is not supported", k) - } - envVars = append(envVars, toEnvVar(k, *v)) - } - sort.Slice(envVars, func(i, j int) bool { return envVars[i].Name < envVars[j].Name }) - return envVars, nil -} - -func toEnvVar(key, value string) apiv1.EnvVar { - return apiv1.EnvVar{ - Name: key, - Value: value, - } -} - -func toPorts(list []types.ServicePortConfig) []apiv1.ContainerPort { - var ports []apiv1.ContainerPort - - for _, v := range list { - ports = append(ports, apiv1.ContainerPort{ - ContainerPort: int32(v.Target), - Protocol: toProtocol(v.Protocol), - }) - } - - return ports -} - -func toProtocol(value string) apiv1.Protocol { - if value == "udp" { - return apiv1.ProtocolUDP - } - return apiv1.ProtocolTCP -} - -func toRestartPolicy(s types.ServiceConfig) (apiv1.RestartPolicy, error) { - if s.Deploy == nil || s.Deploy.RestartPolicy == nil { - return apiv1.RestartPolicyAlways, nil - } - policy := s.Deploy.RestartPolicy - - switch policy.Condition { - case string(swarm.RestartPolicyConditionAny): - return apiv1.RestartPolicyAlways, nil - case string(swarm.RestartPolicyConditionNone): - return apiv1.RestartPolicyNever, nil - case string(swarm.RestartPolicyConditionOnFailure): - return apiv1.RestartPolicyOnFailure, nil - default: - return "", errors.Errorf("unsupported restart policy %s", policy.Condition) - } -} - -func toResource(res *types.Resource) (apiv1.ResourceList, error) { - list := make(apiv1.ResourceList) - if res.NanoCPUs != "" { - cpus, err := resource.ParseQuantity(res.NanoCPUs) - if err != nil { - return nil, err - } - list[apiv1.ResourceCPU] = cpus - } - if res.MemoryBytes != 0 { - memory, err := resource.ParseQuantity(fmt.Sprintf("%v", res.MemoryBytes)) - if err != nil { - return nil, err - } - list[apiv1.ResourceMemory] = memory - } - return list, nil -} - -func toSecurityContext(s types.ServiceConfig) *apiv1.SecurityContext { - isPrivileged := toBoolPointer(s.Privileged) - isReadOnly := toBoolPointer(s.ReadOnly) - - var capabilities *apiv1.Capabilities - if s.CapAdd != nil || s.CapDrop != nil { - capabilities = &apiv1.Capabilities{ - Add: toCapabilities(s.CapAdd), - Drop: toCapabilities(s.CapDrop), - } - } - - var userID *int64 - if s.User != "" { - numerical, err := strconv.Atoi(s.User) - if err == nil { - unixUserID := int64(numerical) - userID = &unixUserID - } - } - - if isPrivileged == nil && isReadOnly == nil && capabilities == nil && userID == nil { - return nil - } - - return &apiv1.SecurityContext{ - RunAsUser: userID, - Privileged: isPrivileged, - ReadOnlyRootFilesystem: isReadOnly, - Capabilities: capabilities, - } -} - -func toBoolPointer(value bool) *bool { - if value { - return &value - } - - return nil -} - -func defaultUint64(v *uint64, defaultValue uint64) uint64 { //nolint: unparam - if v == nil { - return defaultValue - } - - return *v -} - -func toCapabilities(list []string) (capabilities []apiv1.Capability) { - for _, c := range list { - capabilities = append(capabilities, apiv1.Capability(c)) - } - return -} - -//nolint: unparam -func forceRestartPolicy(podTemplate apiv1.PodTemplateSpec, forcedRestartPolicy apiv1.RestartPolicy) apiv1.PodTemplateSpec { - if podTemplate.Spec.RestartPolicy != "" { - podTemplate.Spec.RestartPolicy = forcedRestartPolicy - } - - return podTemplate -} diff --git a/kube/compose/internal/kube/pod_test.go b/kube/compose/internal/kube/pod_test.go deleted file mode 100644 index 7d9cc1b67..000000000 --- a/kube/compose/internal/kube/pod_test.go +++ /dev/null @@ -1,990 +0,0 @@ -package kube - -import ( - "fmt" - "os" - "runtime" - "testing" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/stretchr/testify/assert" - apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func loadYAML(yaml string) (*loader.Config, error) { - dict, err := loader.ParseYAML([]byte(yaml)) - if err != nil { - return nil, err - } - workingDir, err := os.Getwd() - if err != nil { - panic(err) - } - configs := ConfigFiles: []types.ConfigFile{ - {Filename: "compose.yaml", Config: dict}, - }, - - config := types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: configs, - Environment: utils.Environment(), - } - model, err := loader.Load(config) - return model -} - -func podTemplate(t *testing.T, yaml string) apiv1.PodTemplateSpec { - res, err := podTemplateWithError(yaml) - assert.NoError(t, err) - return res -} - -func podTemplateWithError(yaml string) (apiv1.PodTemplateSpec, error) { - model, err := loadYAML(yaml) - if err != nil { - return apiv1.PodTemplateSpec{}, err - } - - return toPodTemplate(model.Services[0], nil, model) -} - -func TestToPodWithDockerSocket(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") - return - } - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" -`) - - expectedVolume := apiv1.Volume{ - Name: "mount-0", - VolumeSource: apiv1.VolumeSource{ - HostPath: &apiv1.HostPathVolumeSource{ - Path: "/var/run", - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "mount-0", - MountPath: "/var/run/docker.sock", - SubPath: "docker.sock", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func TestToPodWithFunkyCommand(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: basi/node-exporter - command: ["-collector.procfs", "/host/proc", "-collector.sysfs", "/host/sys"] -`) - - expectedArgs := []string{ - `-collector.procfs`, - `/host/proc`, // ? - `-collector.sysfs`, - `/host/sys`, // ? - } - assert.Equal(t, expectedArgs, podTemplate.Spec.Containers[0].Args) -} - -func TestToPodWithGlobalVolume(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - db: - image: "postgres:9.4" - volumes: - - dbdata:/var/lib/postgresql/data -`) - - expectedMount := apiv1.VolumeMount{ - Name: "dbdata", - MountPath: "/var/lib/postgresql/data", - } - assert.Len(t, podTemplate.Spec.Volumes, 0) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func TestToPodWithResources(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - db: - image: "postgres:9.4" - deploy: - resources: - limits: - cpus: "0.001" - memory: 50Mb - reservations: - cpus: "0.0001" - memory: 20Mb -`) - - expectedResourceRequirements := apiv1.ResourceRequirements{ - Limits: map[apiv1.ResourceName]resource.Quantity{ - apiv1.ResourceCPU: resource.MustParse("0.001"), - apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 50*1024*1024)), - }, - Requests: map[apiv1.ResourceName]resource.Quantity{ - apiv1.ResourceCPU: resource.MustParse("0.0001"), - apiv1.ResourceMemory: resource.MustParse(fmt.Sprintf("%d", 20*1024*1024)), - }, - } - assert.Equal(t, expectedResourceRequirements, podTemplate.Spec.Containers[0].Resources) -} - -func TestToPodWithCapabilities(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - cap_add: - - ALL - cap_drop: - - NET_ADMIN - - SYS_ADMIN -`) - - expectedSecurityContext := &apiv1.SecurityContext{ - Capabilities: &apiv1.Capabilities{ - Add: []apiv1.Capability{"ALL"}, - Drop: []apiv1.Capability{"NET_ADMIN", "SYS_ADMIN"}, - }, - } - - assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) -} - -func TestToPodWithReadOnly(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - read_only: true -`) - - yes := true - expectedSecurityContext := &apiv1.SecurityContext{ - ReadOnlyRootFilesystem: &yes, - } - assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) -} - -func TestToPodWithPrivileged(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - privileged: true -`) - - yes := true - expectedSecurityContext := &apiv1.SecurityContext{ - Privileged: &yes, - } - assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) -} - -func TestToPodWithEnvNilShouldErrorOut(t *testing.T) { - _, err := podTemplateWithError(` -version: "3" -services: - redis: - image: "redis:alpine" - environment: - - SESSION_SECRET -`) - assert.Error(t, err) -} - -func TestToPodWithEnv(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - environment: - - RACK_ENV=development - - SHOW=true -`) - - expectedEnv := []apiv1.EnvVar{ - { - Name: "RACK_ENV", - Value: "development", - }, - { - Name: "SHOW", - Value: "true", - }, - } - - assert.Equal(t, expectedEnv, podTemplate.Spec.Containers[0].Env) -} - -func TestToPodWithVolume(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") - return - } - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - volumes: - - /ignore:/ignore - - /opt/data:/var/lib/mysql:ro -`) - - assert.Len(t, podTemplate.Spec.Volumes, 2) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 2) -} - -func /*FIXME Test*/ ToPodWithRelativeVolumes(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("on windows, source path validation is broken (and actually, source validation for windows workload is broken too). Skip it for now, as we don't support it yet") - return - } - _, err := podTemplateWithError(` -version: "3" -services: - nginx: - image: nginx - volumes: - - ./fail:/ignore -`) - - assert.Error(t, err) -} - -func TestToPodWithHealthCheck(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost"] - interval: 90s - timeout: 10s - retries: 3 -`) - - expectedLivenessProbe := &apiv1.Probe{ - TimeoutSeconds: 10, - PeriodSeconds: 90, - FailureThreshold: 3, - Handler: apiv1.Handler{ - Exec: &apiv1.ExecAction{ - Command: []string{"curl", "-f", "http://localhost"}, - }, - }, - } - - assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) -} - -func TestToPodWithShellHealthCheck(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost"] -`) - - expectedLivenessProbe := &apiv1.Probe{ - TimeoutSeconds: 1, - PeriodSeconds: 1, - FailureThreshold: 3, - Handler: apiv1.Handler{ - Exec: &apiv1.ExecAction{ - Command: []string{"sh", "-c", "curl -f http://localhost"}, - }, - }, - } - - assert.Equal(t, expectedLivenessProbe, podTemplate.Spec.Containers[0].LivenessProbe) -} - -func TestToPodWithTargetlessExternalSecret(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - secrets: - - my_secret -`) - - expectedVolume := apiv1.Volume{ - Name: "secret-0", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: "my_secret", - Items: []apiv1.KeyToPath{ - { - Key: "file", // TODO: This is the key we assume external secrets use - Path: "secret-0", - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "secret-0", - ReadOnly: true, - MountPath: "/run/secrets/my_secret", - SubPath: "secret-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func TestToPodWithExternalSecret(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - secrets: - - source: my_secret - target: nginx_secret -`) - - expectedVolume := apiv1.Volume{ - Name: "secret-0", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: "my_secret", - Items: []apiv1.KeyToPath{ - { - Key: "file", // TODO: This is the key we assume external secrets use - Path: "secret-0", - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "secret-0", - ReadOnly: true, - MountPath: "/run/secrets/nginx_secret", - SubPath: "secret-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func /*FIXME Test*/ ToPodWithFileBasedSecret(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - secrets: - - source: my_secret -secrets: - my_secret: - file: ./secret.txt -`) - - expectedVolume := apiv1.Volume{ - Name: "secret-0", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: "my_secret", - Items: []apiv1.KeyToPath{ - { - Key: "secret.txt", - Path: "secret-0", - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "secret-0", - ReadOnly: true, - MountPath: "/run/secrets/my_secret", - SubPath: "secret-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func /*FIXME Test*/ ToPodWithTwoFileBasedSecrets(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - secrets: - - source: my_secret1 - - source: my_secret2 - target: secret2 -secrets: - my_secret1: - file: ./secret1.txt - my_secret2: - file: ./secret2.txt -`) - - expectedVolumes := []apiv1.Volume{ - { - Name: "secret-0", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: "my_secret1", - Items: []apiv1.KeyToPath{ - { - Key: "secret1.txt", - Path: "secret-0", - }, - }, - }, - }, - }, - { - Name: "secret-1", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: "my_secret2", - Items: []apiv1.KeyToPath{ - { - Key: "secret2.txt", - Path: "secret-1", - }, - }, - }, - }, - }, - } - - expectedMounts := []apiv1.VolumeMount{ - { - Name: "secret-0", - ReadOnly: true, - MountPath: "/run/secrets/my_secret1", - SubPath: "secret-0", - }, - { - Name: "secret-1", - ReadOnly: true, - MountPath: "/run/secrets/secret2", - SubPath: "secret-1", - }, - } - - assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) - assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) -} - -func TestToPodWithTerminationGracePeriod(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - stop_grace_period: 100s -`) - - expected := int64(100) - assert.Equal(t, &expected, podTemplate.Spec.TerminationGracePeriodSeconds) -} - -func TestToPodWithTmpfs(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - tmpfs: - - /tmp -`) - - expectedVolume := apiv1.Volume{ - Name: "tmp-0", - VolumeSource: apiv1.VolumeSource{ - EmptyDir: &apiv1.EmptyDirVolumeSource{ - Medium: "Memory", - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "tmp-0", - MountPath: "/tmp", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func TestToPodWithNumericalUser(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - user: "1000" -`) - - userID := int64(1000) - - expectedSecurityContext := &apiv1.SecurityContext{ - RunAsUser: &userID, - } - - assert.Equal(t, expectedSecurityContext, podTemplate.Spec.Containers[0].SecurityContext) -} - -func TestToPodWithGitVolume(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - volumes: - - source: "git@github.com:moby/moby.git" - target: /sources - type: git -`) - - expectedVolume := apiv1.Volume{ - Name: "mount-0", - VolumeSource: apiv1.VolumeSource{ - GitRepo: &apiv1.GitRepoVolumeSource{ - Repository: "git@github.com:moby/moby.git", - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "mount-0", - ReadOnly: false, - MountPath: "/sources", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func /*FIXME Test*/ ToPodWithFileBasedConfig(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - configs: - - source: my_config - target: /usr/share/nginx/html/index.html - uid: "103" - gid: "103" - mode: 0440 -configs: - my_config: - file: ./file.html -`) - - mode := int32(0440) - - expectedVolume := apiv1.Volume{ - Name: "config-0", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "my_config", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file.html", - Path: "config-0", - Mode: &mode, - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "config-0", - ReadOnly: true, - MountPath: "/usr/share/nginx/html/index.html", - SubPath: "config-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func /*FIXME Test*/ ToPodWithTargetlessFileBasedConfig(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - configs: - - my_config -configs: - my_config: - file: ./file.html -`) - - expectedVolume := apiv1.Volume{ - Name: "config-0", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "myconfig", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file.html", - Path: "config-0", - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "config-0", - ReadOnly: true, - MountPath: "/myconfig", - SubPath: "config-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func TestToPodWithExternalConfig(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - redis: - image: "redis:alpine" - configs: - - source: my_config - target: /usr/share/nginx/html/index.html - uid: "103" - gid: "103" - mode: 0440 -configs: - my_config: - external: true -`) - - mode := int32(0440) - - expectedVolume := apiv1.Volume{ - Name: "config-0", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "my_config", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file", // TODO: This is the key we assume external config use - Path: "config-0", - Mode: &mode, - }, - }, - }, - }, - } - - expectedMount := apiv1.VolumeMount{ - Name: "config-0", - ReadOnly: true, - MountPath: "/usr/share/nginx/html/index.html", - SubPath: "config-0", - } - - assert.Len(t, podTemplate.Spec.Volumes, 1) - assert.Len(t, podTemplate.Spec.Containers[0].VolumeMounts, 1) - assert.Equal(t, expectedVolume, podTemplate.Spec.Volumes[0]) - assert.Equal(t, expectedMount, podTemplate.Spec.Containers[0].VolumeMounts[0]) -} - -func /*FIXME Test*/ ToPodWithTwoConfigsSameMountPoint(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - configs: - - source: first - target: /data/first.json - mode: "0440" - - source: second - target: /data/second.json - mode: "0550" -configs: - first: - file: ./file1 - secondv: - file: ./file2 -`) - - mode0440 := int32(0440) - mode0550 := int32(0550) - - expectedVolumes := []apiv1.Volume{ - { - Name: "config-0", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "first", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file1", - Path: "config-0", - Mode: &mode0440, - }, - }, - }, - }, - }, - { - Name: "config-1", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "second", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file2", - Path: "config-1", - Mode: &mode0550, - }, - }, - }, - }, - }, - } - - expectedMounts := []apiv1.VolumeMount{ - { - Name: "config-0", - ReadOnly: true, - MountPath: "/data/first.json", - SubPath: "config-0", - }, - { - Name: "config-1", - ReadOnly: true, - MountPath: "/data/second.json", - SubPath: "config-1", - }, - } - - assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) - assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) -} - -func TestToPodWithTwoExternalConfigsSameMountPoint(t *testing.T) { - podTemplate := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - configs: - - source: first - target: /data/first.json - - source: second - target: /data/second.json -configs: - first: - file: ./file1 - second: - file: ./file2 -`) - - expectedVolumes := []apiv1.Volume{ - { - Name: "config-0", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "first", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file", - Path: "config-0", - }, - }, - }, - }, - }, - { - Name: "config-1", - VolumeSource: apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: "second", - }, - Items: []apiv1.KeyToPath{ - { - Key: "file", - Path: "config-1", - }, - }, - }, - }, - }, - } - - expectedMounts := []apiv1.VolumeMount{ - { - Name: "config-0", - ReadOnly: true, - MountPath: "/data/first.json", - SubPath: "config-0", - }, - { - Name: "config-1", - ReadOnly: true, - MountPath: "/data/second.json", - SubPath: "config-1", - }, - } - - assert.Equal(t, expectedVolumes, podTemplate.Spec.Volumes) - assert.Equal(t, expectedMounts, podTemplate.Spec.Containers[0].VolumeMounts) -} - -func /*FIXME Test*/ ToPodWithPullSecret(t *testing.T) { - podTemplateWithSecret := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx - x-kubernetes.pull-secret: test-pull-secret -`) - - assert.Equal(t, 1, len(podTemplateWithSecret.Spec.ImagePullSecrets)) - assert.Equal(t, "test-pull-secret", podTemplateWithSecret.Spec.ImagePullSecrets[0].Name) - - podTemplateNoSecret := podTemplate(t, ` -version: "3" -services: - nginx: - image: nginx -`) - - assert.Nil(t, podTemplateNoSecret.Spec.ImagePullSecrets) -} - -func /*FIXME Test*/ ToPodWithPullPolicy(t *testing.T) { - cases := []struct { - name string - stack string - expectedPolicy apiv1.PullPolicy - expectedError string - }{ - { - name: "specific tag", - stack: ` -version: "3" -services: - nginx: - image: nginx:specific -`, - expectedPolicy: apiv1.PullIfNotPresent, - }, - { - name: "latest tag", - stack: ` -version: "3" -services: - nginx: - image: nginx:latest -`, - expectedPolicy: apiv1.PullAlways, - }, - { - name: "explicit policy", - stack: ` -version: "3" -services: - nginx: - image: nginx:specific - x-kubernetes.pull-policy: Never -`, - expectedPolicy: apiv1.PullNever, - }, - { - name: "invalid policy", - stack: ` -version: "3" -services: - nginx: - image: nginx:specific - x-kubernetes.pull-policy: Invalid -`, - expectedError: `invalid pull policy "Invalid", must be "Always", "IfNotPresent" or "Never"`, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - pod, err := podTemplateWithError(c.stack) - if c.expectedError != "" { - assert.EqualError(t, err, c.expectedError) - } else { - assert.NoError(t, err) - assert.Equal(t, pod.Spec.Containers[0].ImagePullPolicy, c.expectedPolicy) - } - }) - } -} diff --git a/kube/compose/internal/kube/volumes.go b/kube/compose/internal/kube/volumes.go deleted file mode 100644 index 3646de1e9..000000000 --- a/kube/compose/internal/kube/volumes.go +++ /dev/null @@ -1,228 +0,0 @@ -package kube - -import ( - "fmt" - "path" - "path/filepath" - "strings" - - "github.com/compose-spec/compose-go/types" - - "github.com/pkg/errors" - apiv1 "k8s.io/api/core/v1" -) - -const dockerSock = "/var/run/docker.sock" - -type volumeSpec struct { - mount apiv1.VolumeMount - source *apiv1.VolumeSource -} - -func hasPersistentVolumes(s types.ServiceConfig) bool { - for _, volume := range s.Volumes { - if volume.Type == "volume" { - return true - } - } - - return false -} - -func toVolumeSpecs(s types.ServiceConfig, model *types.Config) ([]volumeSpec, error) { - var specs []volumeSpec - for i, m := range s.Volumes { - var source *apiv1.VolumeSource - name := fmt.Sprintf("mount-%d", i) - subpath := "" - if m.Source == dockerSock && m.Target == dockerSock { - subpath = "docker.sock" - source = hostPathVolume("/var/run") - } else if strings.HasSuffix(m.Source, ".git") { - source = gitVolume(m.Source) - } else if m.Type == "volume" { - if m.Source != "" { - name = strings.ReplaceAll(m.Source, "_", "-") - } - } else { - // bind mount - if !filepath.IsAbs(m.Source) { - return nil, errors.Errorf("%s: only absolute paths can be specified in mount source", m.Source) - } - if m.Source == "/" { - source = hostPathVolume("/") - } else { - parent, file := filepath.Split(m.Source) - if parent != "/" { - parent = strings.TrimSuffix(parent, "/") - } - source = hostPathVolume(parent) - subpath = file - } - } - - specs = append(specs, volumeSpec{ - source: source, - mount: volumeMount(name, m.Target, m.ReadOnly, subpath), - }) - } - - for i, m := range s.Tmpfs { - name := fmt.Sprintf("tmp-%d", i) - - specs = append(specs, volumeSpec{ - source: emptyVolumeInMemory(), - mount: volumeMount(name, m, false, ""), - }) - } - - for i, s := range s.Secrets { - name := fmt.Sprintf("secret-%d", i) - - target := path.Join("/run/secrets", or(s.Target, s.Source)) - subPath := name - readOnly := true - - specs = append(specs, volumeSpec{ - source: secretVolume(s, model.Secrets[name], subPath), - mount: volumeMount(name, target, readOnly, subPath), - }) - } - - for i, c := range s.Configs { - name := fmt.Sprintf("config-%d", i) - - target := or(c.Target, "/"+c.Source) - subPath := name - readOnly := true - - specs = append(specs, volumeSpec{ - source: configVolume(c, model.Configs[name], subPath), - mount: volumeMount(name, target, readOnly, subPath), - }) - } - - return specs, nil -} - -func or(v string, defaultValue string) string { - if v != "" && v != "." { - return v - } - - return defaultValue -} - -func toVolumeMounts(s types.ServiceConfig, model *types.Config) ([]apiv1.VolumeMount, error) { - var mounts []apiv1.VolumeMount - specs, err := toVolumeSpecs(s, model) - if err != nil { - return nil, err - } - for _, spec := range specs { - mounts = append(mounts, spec.mount) - } - return mounts, nil -} - -func toVolumes(s types.ServiceConfig, model *types.Config) ([]apiv1.Volume, error) { - var volumes []apiv1.Volume - specs, err := toVolumeSpecs(s, model) - if err != nil { - return nil, err - } - for _, spec := range specs { - if spec.source == nil { - spec.source = emptyVolumeInMemory() - } - volumes = append(volumes, apiv1.Volume{ - Name: spec.mount.Name, - VolumeSource: *spec.source, - }) - } - return volumes, nil -} - -func gitVolume(path string) *apiv1.VolumeSource { - return &apiv1.VolumeSource{ - GitRepo: &apiv1.GitRepoVolumeSource{ - Repository: filepath.ToSlash(path), - }, - } -} - -func hostPathVolume(path string) *apiv1.VolumeSource { - return &apiv1.VolumeSource{ - HostPath: &apiv1.HostPathVolumeSource{ - Path: path, - }, - } -} - -func defaultMode(mode *uint32) *int32 { - var defaultMode *int32 - - if mode != nil { - signedMode := int32(*mode) - defaultMode = &signedMode - } - - return defaultMode -} - -func secretVolume(config types.ServiceSecretConfig, topLevelConfig types.SecretConfig, subPath string) *apiv1.VolumeSource { - return &apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: config.Source, - Items: []apiv1.KeyToPath{ - { - Key: toKey(topLevelConfig.File), - Path: subPath, - Mode: defaultMode(config.Mode), - }, - }, - }, - } -} - -func volumeMount(name, path string, readOnly bool, subPath string) apiv1.VolumeMount { - return apiv1.VolumeMount{ - Name: name, - MountPath: path, - ReadOnly: readOnly, - SubPath: subPath, - } -} - -func configVolume(config types.ServiceConfigObjConfig, topLevelConfig types.ConfigObjConfig, subPath string) *apiv1.VolumeSource { - return &apiv1.VolumeSource{ - ConfigMap: &apiv1.ConfigMapVolumeSource{ - LocalObjectReference: apiv1.LocalObjectReference{ - Name: config.Source, - }, - Items: []apiv1.KeyToPath{ - { - Key: toKey(topLevelConfig.File), - Path: subPath, - Mode: defaultMode(config.Mode), - }, - }, - }, - } -} - -func toKey(file string) string { - if file != "" { - return path.Base(file) - } - - return "file" // TODO: hard-coded key for external configs -} - -func emptyVolumeInMemory() *apiv1.VolumeSource { - return &apiv1.VolumeSource{ - EmptyDir: &apiv1.EmptyDirVolumeSource{ - Medium: apiv1.StorageMediumMemory, - }, - } -} diff --git a/kube/compose/internal/utils/config.go b/kube/compose/internal/utils/config.go deleted file mode 100644 index 9db651b43..000000000 --- a/kube/compose/internal/utils/config.go +++ /dev/null @@ -1,124 +0,0 @@ -package utils - -import ( - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/prometheus/common/log" -) - -var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} - -func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, error) { - configPath, err := getConfigPaths(configPaths) - if err != nil { - return "", nil, err - } - if configPath == nil { - return "", nil, nil - } - workingDir := filepath.Dir(configPath[0]) - - if name == "" { - name = os.Getenv("COMPOSE_PROJECT_NAME") - } - if name == "" { - r := regexp.MustCompile(`[^a-z0-9\\-_]+`) - name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") - } - - configs, err := parseConfigs(configPath) - if err != nil { - return "", nil, err - } - return workingDir, configs, nil -} - -func getConfigPaths(configPaths []string) ([]string, error) { - paths := []string{} - pwd, err := os.Getwd() - if err != nil { - return nil, err - } - - if len(configPaths) != 0 { - for _, f := range configPaths { - if f == "-" { - paths = append(paths, f) - continue - } - if !filepath.IsAbs(f) { - f = filepath.Join(pwd, f) - } - if _, err := os.Stat(f); err != nil { - return nil, err - } - paths = append(paths, f) - } - return paths, nil - } - - sep := os.Getenv("COMPOSE_FILE_SEPARATOR") - if sep == "" { - sep = string(os.PathListSeparator) - } - f := os.Getenv("COMPOSE_FILE") - if f != "" { - return strings.Split(f, sep), nil - } - - for { - candidates := []string{} - for _, n := range SupportedFilenames { - f := filepath.Join(pwd, n) - if _, err := os.Stat(f); err == nil { - candidates = append(candidates, f) - } - } - if len(candidates) > 0 { - winner := candidates[0] - if len(candidates) > 1 { - log.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) - log.Warnf("Using %s\n", winner) - } - return []string{winner}, nil - } - parent := filepath.Dir(pwd) - if parent == pwd { - return nil, nil - } - pwd = parent - } -} - -func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { - files := []types.ConfigFile{} - for _, f := range configPaths { - var ( - b []byte - err error - ) - if f == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - if _, err := os.Stat(f); err != nil { - return nil, err - } - b, err = ioutil.ReadFile(f) - } - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(b) - if err != nil { - return nil, err - } - files = append(files, types.ConfigFile{Filename: f, Config: config}) - } - return files, nil -} diff --git a/kube/compose/internal/utils/errors.go b/kube/compose/internal/utils/errors.go deleted file mode 100644 index 07a903e6c..000000000 --- a/kube/compose/internal/utils/errors.go +++ /dev/null @@ -1,38 +0,0 @@ -package utils - -import ( - "fmt" - "strings" -) - -func CombineErrors(errors []error) error { - if len(errors) == 0 { - return nil - } - if len(errors) == 1 { - return errors[0] - } - err := combinedError{} - for _, e := range errors { - if c, ok := e.(combinedError); ok { - err.errors = append(err.errors, c.errors...) - } else { - err.errors = append(err.errors, e) - } - } - return combinedError{errors} -} - -type combinedError struct { - errors []error -} - -func (c combinedError) Error() string { - points := make([]string, len(c.errors)) - for i, err := range c.errors { - points[i] = fmt.Sprintf("* %s", err.Error()) - } - return fmt.Sprintf( - "%d errors occurred:\n\t%s", - len(c.errors), strings.Join(points, "\n\t")) -} diff --git a/kube/compose/internal/utils/labels.go b/kube/compose/internal/utils/labels.go deleted file mode 100644 index 4ff180c54..000000000 --- a/kube/compose/internal/utils/labels.go +++ /dev/null @@ -1,17 +0,0 @@ -package utils - -const ( - LabelDockerComposePrefix = "com.docker.compose" - LabelService = LabelDockerComposePrefix + ".service" - LabelVersion = LabelDockerComposePrefix + ".version" - LabelContainerNumber = LabelDockerComposePrefix + ".container-number" - LabelOneOff = LabelDockerComposePrefix + ".oneoff" - LabelNetwork = LabelDockerComposePrefix + ".network" - LabelSlug = LabelDockerComposePrefix + ".slug" - LabelVolume = LabelDockerComposePrefix + ".volume" - LabelConfigHash = LabelDockerComposePrefix + ".config-hash" - LabelProject = LabelDockerComposePrefix + ".project" - LabelWorkingDir = LabelDockerComposePrefix + ".working_dir" - LabelConfigFiles = LabelDockerComposePrefix + ".config_files" - LabelEnvironmentFile = LabelDockerComposePrefix + ".environment_file" -) From f09a57351f2cef91ec4ba902717c1b9912cce916 Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 21 Jan 2021 14:23:52 +0100 Subject: [PATCH 23/24] remove unused utils pkg Signed-off-by: aiordache --- go.mod | 1 - kube/charts/charts.go | 14 ++++- kube/utils/config.go | 142 ------------------------------------------ kube/utils/errors.go | 56 ----------------- kube/utils/labels.go | 35 ----------- kube/utils/utils.go | 34 ---------- 6 files changed, 12 insertions(+), 270 deletions(-) delete mode 100644 kube/utils/config.go delete mode 100644 kube/utils/errors.go delete mode 100644 kube/utils/labels.go delete mode 100644 kube/utils/utils.go diff --git a/go.mod b/go.mod index 074731c7c..50aeb9e05 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/common v0.10.0 github.com/prometheus/tsdb v0.10.0 github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b github.com/sirupsen/logrus v1.7.0 diff --git a/kube/charts/charts.go b/kube/charts/charts.go index df035eca7..8a74789ea 100644 --- a/kube/charts/charts.go +++ b/kube/charts/charts.go @@ -20,6 +20,7 @@ package charts import ( "context" + "os" "path/filepath" "strings" @@ -28,7 +29,6 @@ import ( "github.com/docker/compose-cli/api/context/store" "github.com/docker/compose-cli/kube/charts/helm" "github.com/docker/compose-cli/kube/charts/kubernetes" - kubeutils "github.com/docker/compose-cli/kube/utils" chart "helm.sh/helm/v3/pkg/chart" util "helm.sh/helm/v3/pkg/chartutil" helmenv "helm.sh/helm/v3/pkg/cli" @@ -57,7 +57,7 @@ var _ API = sdk{} func NewSDK(ctx store.KubeContext) (sdk, error) { return sdk{ - environment: kubeutils.Environment(), + environment: environment(), h: helm.NewHelmActions(nil), }, nil } @@ -124,3 +124,13 @@ func (s sdk) GenerateChart(project *types.Project, dirname string) error { dirname = filepath.Dir(dirname) return s.SaveChart(project, dirname) } + +func environment() map[string]string { + vars := make(map[string]string) + env := os.Environ() + for _, v := range env { + k := strings.SplitN(v, "=", 2) + vars[k[0]] = k[1] + } + return vars +} diff --git a/kube/utils/config.go b/kube/utils/config.go deleted file mode 100644 index 408558923..000000000 --- a/kube/utils/config.go +++ /dev/null @@ -1,142 +0,0 @@ -// +build kube - -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package utils - -import ( - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/prometheus/common/log" -) - -var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} - -func GetConfigs(name string, configPaths []string) (string, []types.ConfigFile, error) { - configPath, err := getConfigPaths(configPaths) - if err != nil { - return "", nil, err - } - if configPath == nil { - return "", nil, nil - } - workingDir := filepath.Dir(configPath[0]) - - if name == "" { - name = os.Getenv("COMPOSE_PROJECT_NAME") - } - if name == "" { - r := regexp.MustCompile(`[^a-z0-9\\-_]+`) - name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") - } - - configs, err := parseConfigs(configPath) - if err != nil { - return "", nil, err - } - return workingDir, configs, nil -} - -func getConfigPaths(configPaths []string) ([]string, error) { - paths := []string{} - pwd, err := os.Getwd() - if err != nil { - return nil, err - } - - if len(configPaths) != 0 { - for _, f := range configPaths { - if f == "-" { - paths = append(paths, f) - continue - } - if !filepath.IsAbs(f) { - f = filepath.Join(pwd, f) - } - if _, err := os.Stat(f); err != nil { - return nil, err - } - paths = append(paths, f) - } - return paths, nil - } - - sep := os.Getenv("COMPOSE_FILE_SEPARATOR") - if sep == "" { - sep = string(os.PathListSeparator) - } - f := os.Getenv("COMPOSE_FILE") - if f != "" { - return strings.Split(f, sep), nil - } - - for { - candidates := []string{} - for _, n := range SupportedFilenames { - f := filepath.Join(pwd, n) - if _, err := os.Stat(f); err == nil { - candidates = append(candidates, f) - } - } - if len(candidates) > 0 { - winner := candidates[0] - if len(candidates) > 1 { - log.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) - log.Warnf("Using %s\n", winner) - } - return []string{winner}, nil - } - parent := filepath.Dir(pwd) - if parent == pwd { - return nil, nil - } - pwd = parent - } -} - -func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { - files := []types.ConfigFile{} - for _, f := range configPaths { - var ( - b []byte - err error - ) - if f == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - if _, err := os.Stat(f); err != nil { - return nil, err - } - b, err = ioutil.ReadFile(f) - } - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(b) - if err != nil { - return nil, err - } - files = append(files, types.ConfigFile{Filename: f, Config: config}) - } - return files, nil -} diff --git a/kube/utils/errors.go b/kube/utils/errors.go deleted file mode 100644 index 0b58b9a54..000000000 --- a/kube/utils/errors.go +++ /dev/null @@ -1,56 +0,0 @@ -// +build kube - -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package utils - -import ( - "fmt" - "strings" -) - -func CombineErrors(errors []error) error { - if len(errors) == 0 { - return nil - } - if len(errors) == 1 { - return errors[0] - } - err := combinedError{} - for _, e := range errors { - if c, ok := e.(combinedError); ok { - err.errors = append(err.errors, c.errors...) - } else { - err.errors = append(err.errors, e) - } - } - return combinedError{errors} -} - -type combinedError struct { - errors []error -} - -func (c combinedError) Error() string { - points := make([]string, len(c.errors)) - for i, err := range c.errors { - points[i] = fmt.Sprintf("* %s", err.Error()) - } - return fmt.Sprintf( - "%d errors occurred:\n\t%s", - len(c.errors), strings.Join(points, "\n\t")) -} diff --git a/kube/utils/labels.go b/kube/utils/labels.go deleted file mode 100644 index 0ef3ecc07..000000000 --- a/kube/utils/labels.go +++ /dev/null @@ -1,35 +0,0 @@ -// +build kube - -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package utils - -const ( - LabelDockerComposePrefix = "com.docker.compose" - LabelService = LabelDockerComposePrefix + ".service" - LabelVersion = LabelDockerComposePrefix + ".version" - LabelContainerNumber = LabelDockerComposePrefix + ".container-number" - LabelOneOff = LabelDockerComposePrefix + ".oneoff" - LabelNetwork = LabelDockerComposePrefix + ".network" - LabelSlug = LabelDockerComposePrefix + ".slug" - LabelVolume = LabelDockerComposePrefix + ".volume" - LabelConfigHash = LabelDockerComposePrefix + ".config-hash" - LabelProject = LabelDockerComposePrefix + ".project" - LabelWorkingDir = LabelDockerComposePrefix + ".working_dir" - LabelConfigFiles = LabelDockerComposePrefix + ".config_files" - LabelEnvironmentFile = LabelDockerComposePrefix + ".environment_file" -) diff --git a/kube/utils/utils.go b/kube/utils/utils.go deleted file mode 100644 index dab4cd5fd..000000000 --- a/kube/utils/utils.go +++ /dev/null @@ -1,34 +0,0 @@ -// +build kube - -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package utils - -import ( - "os" - "strings" -) - -func Environment() map[string]string { - vars := make(map[string]string) - env := os.Environ() - for _, v := range env { - k := strings.SplitN(v, "=", 2) - vars[k[0]] = k[1] - } - return vars -} From 60aed923670f123d1524869ec6ccec86baac48fd Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 21 Jan 2021 14:55:31 +0100 Subject: [PATCH 24/24] remove redundant API interface Signed-off-by: aiordache --- kube/charts/charts.go | 41 ++++++++++------------------------------- kube/compose.go | 6 +++--- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/kube/charts/charts.go b/kube/charts/charts.go index 8a74789ea..b09608ca6 100644 --- a/kube/charts/charts.go +++ b/kube/charts/charts.go @@ -19,7 +19,6 @@ package charts import ( - "context" "os" "path/filepath" "strings" @@ -34,40 +33,20 @@ import ( helmenv "helm.sh/helm/v3/pkg/cli" ) -// API defines management methods for helm charts -type API interface { - GetDefaultEnv() *helmenv.EnvSettings - Connect(ctx context.Context) error - GenerateChart(project *types.Project, dirname string) error - GetChartInMemory(project *types.Project) (*chart.Chart, error) - SaveChart(project *types.Project, dest string) error - - Install(project *types.Project) error - Uninstall(projectName string) error - List(projectName string) ([]compose.Stack, error) -} - -type sdk struct { +type SDK struct { h *helm.HelmActions environment map[string]string } -// sdk implement API -var _ API = sdk{} - -func NewSDK(ctx store.KubeContext) (sdk, error) { - return sdk{ +func NewSDK(ctx store.KubeContext) (SDK, error) { + return SDK{ environment: environment(), h: helm.NewHelmActions(nil), }, nil } -func (s sdk) Connect(ctx context.Context) error { - return nil -} - // Install deploys a Compose stack -func (s sdk) Install(project *types.Project) error { +func (s SDK) Install(project *types.Project) error { chart, err := s.GetChartInMemory(project) if err != nil { return err @@ -76,21 +55,21 @@ func (s sdk) Install(project *types.Project) error { } // Uninstall removes a runnign compose stack -func (s sdk) Uninstall(projectName string) error { +func (s SDK) Uninstall(projectName string) error { return s.h.Uninstall(projectName) } // List returns a list of compose stacks -func (s sdk) List(projectName string) ([]compose.Stack, error) { +func (s SDK) List(projectName string) ([]compose.Stack, error) { return s.h.ListReleases() } // GetDefault initializes Helm EnvSettings -func (s sdk) GetDefaultEnv() *helmenv.EnvSettings { +func (s SDK) GetDefaultEnv() *helmenv.EnvSettings { return helmenv.New() } -func (s sdk) GetChartInMemory(project *types.Project) (*chart.Chart, error) { +func (s SDK) GetChartInMemory(project *types.Project) (*chart.Chart, error) { // replace _ with - in volume names for k, v := range project.Volumes { volumeName := strings.ReplaceAll(k, "_", "-") @@ -107,7 +86,7 @@ func (s sdk) GetChartInMemory(project *types.Project) (*chart.Chart, error) { return helm.ConvertToChart(project.Name, objects) } -func (s sdk) SaveChart(project *types.Project, dest string) error { +func (s SDK) SaveChart(project *types.Project, dest string) error { chart, err := s.GetChartInMemory(project) if err != nil { return err @@ -115,7 +94,7 @@ func (s sdk) SaveChart(project *types.Project, dest string) error { return util.SaveDir(chart, dest) } -func (s sdk) GenerateChart(project *types.Project, dirname string) error { +func (s SDK) GenerateChart(project *types.Project, dirname string) error { if strings.Contains(dirname, ".") { splits := strings.SplitN(dirname, ".", 2) dirname = splits[0] diff --git a/kube/compose.go b/kube/compose.go index 33fb069df..b9e55c322 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -30,19 +30,19 @@ import ( // NewComposeService create a kubernetes implementation of the compose.Service API func NewComposeService(ctx store.KubeContext) (compose.Service, error) { - apiclient, err := charts.NewSDK(ctx) + chartsApi, err := charts.NewSDK(ctx) if err != nil { return nil, err } return &composeService{ ctx: ctx, - sdk: apiclient, + sdk: chartsApi, }, nil } type composeService struct { ctx store.KubeContext - sdk charts.API + sdk charts.SDK } // Up executes the equivalent to a `compose up`