diff --git a/docs/windows.md b/docs/windows.md index a233a05c82a3..4d0de559b9f7 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -2,21 +2,11 @@ The Argo server and the workflow controller currently only run on Linux. The workflow executor however also runs on Windows nodes, meaning you can use Windows containers inside your workflows! Here are the steps to get started. -## Requirements +## Requirements * Kubernetes 1.14 or later, supporting Windows nodes * Hybrid cluster containing Linux and Windows nodes like described in the [Kubernetes docs](https://kubernetes.io/docs/setup/production-environment/windows/user-guide-windows-containers/) * Argo configured and running like described [here](quick-start.md) -## Setting up the workflow executor - -Currently the worflow controller configuration doesn't support different configurations for the `dockerSockPath` based on the host OS. This means that the workflow executor, running in a Windows container can't use Docker for now. - -You therefore need to use `kubelet` or `k8sapi` instead in your workflow controller configmap: -```yaml -containerRuntimeExecutor: kubelet -kubeletInsecure: true # you can disable TLS verification of the kubelet executor for testing -``` - ## Schedule workflows with Windows containers If you're running workflows in your hybrid Kubernetes cluster, always make sure to include a `nodeSelector` to run the steps on the correct host OS: @@ -45,7 +35,7 @@ $ argo logs hello-windows-s9kk5 hello-windows-s9kk5: "Hello from Windows Container!" ``` -## Bonus: Hybrid workflows +## Schedule hybrid workflows You can also run different steps on different host OSs. This can for example be very helpful when you need to compile your application on Windows and Linux. @@ -74,7 +64,7 @@ spec: args: ["echo", "Hello from Windows Container!"] - name: hello-linux nodeSelector: - beta.kubernetes.io/os: linux + kubernetes.io/os: linux container: image: alpine command: [echo] diff --git a/examples/hello-hybrid.yaml b/examples/hello-hybrid.yaml index 381b5a36b4ec..485195ec052e 100644 --- a/examples/hello-hybrid.yaml +++ b/examples/hello-hybrid.yaml @@ -21,7 +21,7 @@ spec: args: ["echo", "Hello from Windows Container!"] - name: hello-linux nodeSelector: - beta.kubernetes.io/os: linux + kubernetes.io/os: linux container: image: alpine command: [echo] diff --git a/workflow/controller/workflowpod.go b/workflow/controller/workflowpod.go index 55d98d5c30a1..063bdaf74a9d 100644 --- a/workflow/controller/workflowpod.go +++ b/workflow/controller/workflowpod.go @@ -53,16 +53,48 @@ var ( hostPathSocket = apiv1.HostPathSocket ) -func (woc *wfOperationCtx) getVolumeMountDockerSock() apiv1.VolumeMount { +func (woc *wfOperationCtx) getVolumeMountDockerSock(tmpl *wfv1.Template) apiv1.VolumeMount { return apiv1.VolumeMount{ Name: common.DockerSockVolumeName, - MountPath: "/var/run/docker.sock", - ReadOnly: true, + MountPath: getDockerSockPath(tmpl), + ReadOnly: getDockerSockReadOnly(tmpl), } } -func (woc *wfOperationCtx) getVolumeDockerSock() apiv1.Volume { - dockerSockPath := "/var/run/docker.sock" +func getDockerSockReadOnly(tmpl *wfv1.Template) bool { + return !hasWindowsOSNodeSelector(tmpl.NodeSelector) +} + +func getDockerSockPath(tmpl *wfv1.Template) string { + if hasWindowsOSNodeSelector(tmpl.NodeSelector) { + return "\\\\.\\pipe\\docker_engine" + } + + return "/var/run/docker.sock" +} + +func getVolumeHostPathType(tmpl *wfv1.Template) *apiv1.HostPathType { + if hasWindowsOSNodeSelector(tmpl.NodeSelector) { + return nil + } + + return &hostPathSocket +} + +func hasWindowsOSNodeSelector(nodeSelector map[string]string) bool { + if nodeSelector == nil { + return false + } + + if os, keyExists := nodeSelector["kubernetes.io/os"]; keyExists && os == "windows" { + return true + } + + return false +} + +func (woc *wfOperationCtx) getVolumeDockerSock(tmpl *wfv1.Template) apiv1.Volume { + dockerSockPath := getDockerSockPath(tmpl) if woc.controller.Config.DockerSockPath != "" { dockerSockPath = woc.controller.Config.DockerSockPath @@ -77,7 +109,7 @@ func (woc *wfOperationCtx) getVolumeDockerSock() apiv1.Volume { VolumeSource: apiv1.VolumeSource{ HostPath: &apiv1.HostPathVolumeSource{ Path: dockerSockPath, - Type: &hostPathSocket, + Type: getVolumeHostPathType(tmpl), }, }, } @@ -133,7 +165,7 @@ func (woc *wfOperationCtx) createWorkflowPod(nodeName string, mainCtr apiv1.Cont }, Spec: apiv1.PodSpec{ RestartPolicy: apiv1.RestartPolicyNever, - Volumes: woc.createVolumes(), + Volumes: woc.createVolumes(tmpl), ActiveDeadlineSeconds: activeDeadlineSeconds, ImagePullSecrets: woc.wfSpec.ImagePullSecrets, }, @@ -363,7 +395,7 @@ func (woc *wfOperationCtx) newWaitContainer(tmpl *wfv1.Template) (*apiv1.Contain ctr.SecurityContext.Privileged = pointer.BoolPtr(true) } case "", common.ContainerRuntimeExecutorDocker: - ctr.VolumeMounts = append(ctr.VolumeMounts, woc.getVolumeMountDockerSock()) + ctr.VolumeMounts = append(ctr.VolumeMounts, woc.getVolumeMountDockerSock(tmpl)) } return ctr, nil } @@ -444,7 +476,7 @@ func (woc *wfOperationCtx) createEnvVars() []apiv1.EnvVar { return execEnvVars } -func (woc *wfOperationCtx) createVolumes() []apiv1.Volume { +func (woc *wfOperationCtx) createVolumes(tmpl *wfv1.Template) []apiv1.Volume { volumes := []apiv1.Volume{ volumePodMetadata, } @@ -466,7 +498,7 @@ func (woc *wfOperationCtx) createVolumes() []apiv1.Volume { case common.ContainerRuntimeExecutorKubelet, common.ContainerRuntimeExecutorK8sAPI, common.ContainerRuntimeExecutorPNS: return volumes default: - return append(volumes, woc.getVolumeDockerSock()) + return append(volumes, woc.getVolumeDockerSock(tmpl)) } } diff --git a/workflow/controller/workflowpod_test.go b/workflow/controller/workflowpod_test.go index 7cbddfe2ade8..7f6e4aa630c2 100644 --- a/workflow/controller/workflowpod_test.go +++ b/workflow/controller/workflowpod_test.go @@ -1112,6 +1112,62 @@ func TestPodSpecPatch(t *testing.T) { } +var helloWindowsWf = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: hello-hybrid-win +spec: + entrypoint: hello-win + templates: + - name: hello-win + nodeSelector: + kubernetes.io/os: windows + container: + image: mcr.microsoft.com/windows/nanoserver:1809 + command: ["cmd", "/c"] + args: ["echo", "Hello from Windows Container!"] +` + +var helloLinuxWf = ` +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + name: hello-hybrid-lin +spec: + entrypoint: hello-linux + templates: + - name: hello-linux + nodeSelector: + kubernetes.io/os: linux + container: + image: alpine + command: [echo] + args: ["Hello from Linux Container!"] +` + +func TestHybridWfVolumesWindows(t *testing.T) { + wf := unmarshalWF(helloWindowsWf) + woc := newWoc(*wf) + + mainCtr := woc.wfSpec.Templates[0].Container + pod, _ := woc.createWorkflowPod(wf.Name, *mainCtr, &wf.Spec.Templates[0], &createWorkflowPodOpts{}) + assert.Equal(t, "\\\\.\\pipe\\docker_engine", pod.Spec.Containers[0].VolumeMounts[1].MountPath) + assert.Equal(t, false, pod.Spec.Containers[0].VolumeMounts[1].ReadOnly) + assert.Equal(t, (*apiv1.HostPathType)(nil), pod.Spec.Volumes[1].HostPath.Type) +} + +func TestHybridWfVolumesLinux(t *testing.T) { + wf := unmarshalWF(helloLinuxWf) + woc := newWoc(*wf) + + mainCtr := woc.wfSpec.Templates[0].Container + pod, _ := woc.createWorkflowPod(wf.Name, *mainCtr, &wf.Spec.Templates[0], &createWorkflowPodOpts{}) + assert.Equal(t, "/var/run/docker.sock", pod.Spec.Containers[0].VolumeMounts[1].MountPath) + assert.Equal(t, true, pod.Spec.Containers[0].VolumeMounts[1].ReadOnly) + assert.Equal(t, &hostPathSocket, pod.Spec.Volumes[1].HostPath.Type) +} + var propagateMaxDuration = ` name: retry-backoff retryStrategy: