diff --git a/README.md b/README.md index a3629f7..14c1243 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,10 @@ hckctl box parrot --provider cloud Prerequisites * start the retired [Postman](https://app.hackthebox.com/machines/Postman) machine in your account -* edit your local configurations +* edit your vpn network config ```bash vim ${HOME}/.config/hck/config.yml - # edit vpn network network: vpn: - name: htb @@ -67,8 +66,11 @@ Prerequisites Start the auto-exploitation box ```bash +# TODO +hckctl box parrot-sec + # exploit the machine and spawn a reverse shell -hckctl task htb-postman --network-vpn htb +hckctl box htb-postman --network-vpn htb ``` ### Lab (preview) @@ -108,7 +110,7 @@ tail -F ${HOME}/.local/state/hck/task/log/task-rustscan-* Prerequisites * start the retired [Lame](https://app.hackthebox.com/machines/Lame) and [Knife](https://app.hackthebox.com/machines/Knife) machines in your account -* edit your network vpn config (see box example above) +* edit your vpn network config (see box example above) Run tasks against the vulnerable machine ```bash @@ -281,6 +283,7 @@ TODO - update directories to exclude in `resolvePath` e.g. charts - add filters and review output e.g. table * box + - print/event shared directory - review tty resize - implement copy ??? - kube: add distroless support diff --git a/internal/command/box/common.go b/internal/command/box/common.go index 34ab76b..c853a1e 100644 --- a/internal/command/box/common.go +++ b/internal/command/box/common.go @@ -160,5 +160,6 @@ func newCreateOptions(info *template.TemplateInfo[boxModel.BoxV1], labels common Size: size, Labels: allLabels, NetworkInfo: networkInfo, + ShareDir: configRef.Config.Common.ShareDir, }, nil } diff --git a/pkg/box/docker/client.go b/pkg/box/docker/client.go index b776bb6..c19f1d8 100644 --- a/pkg/box/docker/client.go +++ b/pkg/box/docker/client.go @@ -4,13 +4,13 @@ import ( "github.com/pkg/errors" boxModel "github.com/hckops/hckctl/pkg/box/model" - "github.com/hckops/hckctl/pkg/client/docker" + commonDocker "github.com/hckops/hckctl/pkg/common/docker" commonModel "github.com/hckops/hckctl/pkg/common/model" "github.com/hckops/hckctl/pkg/event" ) type DockerBoxClient struct { - client *docker.DockerClient + docker *commonDocker.DockerCommonClient clientOpts *commonModel.DockerOptions eventBus *event.EventBus } diff --git a/pkg/box/docker/docker.go b/pkg/box/docker/docker.go index 4477337..b496791 100644 --- a/pkg/box/docker/docker.go +++ b/pkg/box/docker/docker.go @@ -9,69 +9,61 @@ import ( boxModel "github.com/hckops/hckctl/pkg/box/model" "github.com/hckops/hckctl/pkg/client/docker" + commonDocker "github.com/hckops/hckctl/pkg/common/docker" commonModel "github.com/hckops/hckctl/pkg/common/model" "github.com/hckops/hckctl/pkg/schema" ) func newDockerBoxClient(commonOpts *boxModel.CommonBoxOptions, dockerOpts *commonModel.DockerOptions) (*DockerBoxClient, error) { - commonOpts.EventBus.Publish(newInitDockerClientEvent()) - dockerClient, err := docker.NewDockerClient() + dockerClient, err := commonDocker.NewDockerCommonClient(commonOpts.EventBus, dockerOpts) if err != nil { - return nil, errors.Wrap(err, "error docker box") + return nil, errors.Wrap(err, "error docker box client") } return &DockerBoxClient{ - client: dockerClient, + docker: dockerClient, clientOpts: dockerOpts, eventBus: commonOpts.EventBus, }, nil } func (box *DockerBoxClient) close() error { - box.eventBus.Publish(newCloseDockerClientEvent()) - box.eventBus.Close() - return box.client.Close() + return box.docker.Client.Close() } // TODO limit resources by size? func (box *DockerBoxClient) createBox(opts *boxModel.CreateOptions) (*boxModel.BoxInfo, error) { - imageName := opts.Template.Image.Name() - imagePullOpts := &docker.ImagePullOpts{ - ImageName: imageName, - OnImagePullCallback: func() { - box.eventBus.Publish(newImagePullDockerLoaderEvent(imageName)) - }, - } - box.eventBus.Publish(newImagePullDockerEvent(imageName)) - if err := box.client.ImagePull(imagePullOpts); err != nil { - // try to use an existing image if exists - if box.clientOpts.IgnoreImagePullError { - box.eventBus.Publish(newImagePullIgnoreDockerEvent(imageName)) - } else { - // do not allow offline + // boxName + containerName := opts.Template.GenerateName() + + // vpn sidecar + var hostname string + var networkMode string + if opts.NetworkInfo.Vpn != nil { + if sidecarContainerId, err := box.docker.StartVpnSidecar(containerName, opts.NetworkInfo.Vpn); err != nil { return nil, err + } else { + // fixes conflicting options: hostname and the network mode + hostname = "" + networkMode = docker.ContainerNetworkMode(sidecarContainerId) + // remove sidecar on exit + // TODO defer box.docker.Client.ContainerRemove(sidecarContainerId) } + } else { + hostname = containerName + networkMode = docker.DefaultNetworkMode() } - // cleanup obsolete nightly images - imageRemoveOpts := &docker.ImageRemoveOpts{ - OnImageRemoveCallback: func(imageId string) { - box.eventBus.Publish(newImageRemoveDockerEvent(imageId)) - }, - OnImageRemoveErrorCallback: func(imageId string, err error) { - // ignore error: keep images used by existing containers - box.eventBus.Publish(newImageRemoveIgnoreDockerEvent(imageId, err)) - }, - } - if err := box.client.ImageRemoveDangling(imageRemoveOpts); err != nil { + imageName := opts.Template.Image.Name() + if err := box.docker.PullImageOffline(imageName, func() { + box.eventBus.Publish(newImagePullDockerLoaderEvent(imageName)) + }); err != nil { return nil, err } - // boxName - containerName := opts.Template.GenerateName() - + // TODO https://github.com/dperson/openvpn-client/issues/210 var containerEnv []docker.ContainerEnv for _, e := range opts.Template.EnvironmentVariables() { containerEnv = append(containerEnv, docker.ContainerEnv{Key: e.Key, Value: e.Value}) @@ -84,7 +76,7 @@ func (box *DockerBoxClient) createBox(opts *boxModel.CreateOptions) (*boxModel.B containerConfig, err := docker.BuildContainerConfig(&docker.ContainerConfigOpts{ ImageName: imageName, - Hostname: containerName, + Hostname: hostname, Env: containerEnv, Ports: containerPorts, Labels: opts.Labels, @@ -99,13 +91,13 @@ func (box *DockerBoxClient) createBox(opts *boxModel.CreateOptions) (*boxModel.B box.publishPortInfo(networkMap, containerName, port) } hostConfig, err := docker.BuildHostConfig(&docker.ContainerHostConfigOpts{ - NetworkMode: docker.DefaultNetworkMode(), + NetworkMode: networkMode, Ports: containerPorts, OnPortBindCallback: onPortBindCallback, Volumes: []docker.ContainerVolume{ { HostDir: opts.ShareDir, - ContainerDir: commonModel.MountedShareDir, + ContainerDir: commonModel.MountShareDir, }, }, }) @@ -114,20 +106,22 @@ func (box *DockerBoxClient) createBox(opts *boxModel.CreateOptions) (*boxModel.B } networkName := box.clientOpts.NetworkName - networkId, err := box.client.NetworkUpsert(networkName) + networkId, err := box.docker.Client.NetworkUpsert(networkName) if err != nil { return nil, err } box.eventBus.Publish(newNetworkUpsertDockerEvent(networkName, networkId)) containerOpts := &docker.ContainerCreateOpts{ - ContainerName: containerName, - ContainerConfig: containerConfig, - HostConfig: hostConfig, - NetworkingConfig: docker.BuildNetworkingConfig(networkName, networkId), // all on the same network - WaitStatus: false, - OnContainerCreateCallback: func(string) error { return nil }, - OnContainerWaitCallback: func(string) error { return nil }, + ContainerName: containerName, + ContainerConfig: containerConfig, + HostConfig: hostConfig, + NetworkingConfig: docker.BuildNetworkingConfig(networkName, networkId), // all on the same network + WaitStatus: false, + CaptureInterrupt: false, // TODO ??? + OnContainerInterruptCallback: func(string) {}, // TODO ??? + OnContainerCreateCallback: func(string) error { return nil }, + OnContainerWaitCallback: func(string) error { return nil }, OnContainerStatusCallback: func(status string) { box.eventBus.Publish(newContainerCreateStatusDockerEvent(status)) }, @@ -139,7 +133,7 @@ func (box *DockerBoxClient) createBox(opts *boxModel.CreateOptions) (*boxModel.B }, } // boxId - containerId, err := box.client.ContainerCreate(containerOpts) + containerId, err := box.docker.Client.ContainerCreate(containerOpts) if err != nil { return nil, err } @@ -182,7 +176,7 @@ func (box *DockerBoxClient) execBox(template *boxModel.BoxV1, info *boxModel.Box box.eventBus.Publish(newContainerRestartDockerEvent(info.Id, status)) }, } - if err := box.client.ContainerRestart(restartsOpts); err != nil { + if err := box.docker.Client.ContainerRestart(restartsOpts); err != nil { return err } @@ -196,7 +190,7 @@ func (box *DockerBoxClient) execBox(template *boxModel.BoxV1, info *boxModel.Box // already printed for temporary box if !deleteOnExit { - containerDetails, err := box.client.ContainerInspect(info.Id) + containerDetails, err := box.docker.Client.ContainerInspect(info.Id) if err != nil { return err } @@ -230,7 +224,7 @@ func (box *DockerBoxClient) execBox(template *boxModel.BoxV1, info *boxModel.Box box.eventBus.Publish(newContainerExecExitDockerEvent(info.Id)) if deleteOnExit { - if err := box.client.ContainerRemove(info.Id); err != nil { + if err := box.docker.Client.ContainerRemove(info.Id); err != nil { box.eventBus.Publish(newContainerExecErrorDockerEvent(info.Id, errors.Wrap(err, "error container exec remove"))) } } @@ -240,7 +234,7 @@ func (box *DockerBoxClient) execBox(template *boxModel.BoxV1, info *boxModel.Box }, } - return box.client.ContainerExec(execOpts) + return box.docker.Client.ContainerExec(execOpts) } func (box *DockerBoxClient) publishPortInfo(networkMap map[string]boxModel.BoxPort, containerName string, containerPort docker.ContainerPort) { @@ -266,7 +260,7 @@ func (box *DockerBoxClient) logsBox(containerId string, streamOpts *commonModel. box.eventBus.Publish(newContainerExecErrorDockerEvent(containerId, err)) }, } - return box.client.ContainerLogs(opts) + return box.docker.Client.ContainerLogs(opts) } func (box *DockerBoxClient) describeBox(name string) (*boxModel.BoxDetails, error) { @@ -276,7 +270,7 @@ func (box *DockerBoxClient) describeBox(name string) (*boxModel.BoxDetails, erro } box.eventBus.Publish(newContainerInspectDockerEvent(info.Id)) - containerInfo, err := box.client.ContainerInspect(info.Id) + containerInfo, err := box.docker.Client.ContainerInspect(info.Id) if err != nil { return nil, err } @@ -345,7 +339,7 @@ func boxLabel() string { func (box *DockerBoxClient) listBoxes() ([]boxModel.BoxInfo, error) { - containers, err := box.client.ContainerList(boxModel.BoxPrefixName, boxLabel()) + containers, err := box.docker.Client.ContainerList(boxModel.BoxPrefixName, boxLabel()) if err != nil { return nil, err } @@ -371,7 +365,7 @@ func (box *DockerBoxClient) deleteBoxes(names []string) ([]string, error) { // all or filter if len(names) == 0 || slices.Contains(names, boxInfo.Name) { - if err := box.client.ContainerRemove(boxInfo.Id); err == nil { + if err := box.docker.Client.ContainerRemove(boxInfo.Id); err == nil { deleted = append(deleted, boxInfo.Name) box.eventBus.Publish(newContainerRemoveDockerEvent(boxInfo.Id)) } else { diff --git a/pkg/box/docker/events.go b/pkg/box/docker/events.go index e8e033a..8c47925 100644 --- a/pkg/box/docker/events.go +++ b/pkg/box/docker/events.go @@ -24,14 +24,6 @@ func (e *dockerBoxEvent) String() string { return e.value } -func newInitDockerClientEvent() *dockerBoxEvent { - return &dockerBoxEvent{kind: event.LogDebug, value: "init docker client"} -} - -func newCloseDockerClientEvent() *dockerBoxEvent { - return &dockerBoxEvent{kind: event.LogDebug, value: "close docker client"} -} - func newImagePullDockerEvent(imageName string) *dockerBoxEvent { return &dockerBoxEvent{kind: event.LogInfo, value: fmt.Sprintf("image pull: imageName=%s", imageName)} } diff --git a/pkg/box/model/provider.go b/pkg/box/model/provider.go index 326ffc5..b362529 100644 --- a/pkg/box/model/provider.go +++ b/pkg/box/model/provider.go @@ -1,11 +1,15 @@ package model +import ( + "github.com/hckops/hckctl/pkg/common/model" +) + type BoxProvider string const ( - Docker BoxProvider = "docker" - Kubernetes BoxProvider = "kube" - Cloud BoxProvider = "cloud" + Docker BoxProvider = model.DockerProvider + Kubernetes BoxProvider = model.KubernetesProvider + Cloud BoxProvider = model.CloudProvider ) func (p BoxProvider) String() string { diff --git a/pkg/client/docker/builder.go b/pkg/client/docker/builder.go index 67b4dee..86df451 100644 --- a/pkg/client/docker/builder.go +++ b/pkg/client/docker/builder.go @@ -77,7 +77,7 @@ func BuildHostConfig(opts *ContainerHostConfigOpts) (*container.HostConfig, erro if opts.Volumes != nil { for _, volume := range opts.Volumes { - if err := util.CreateDir(volume.HostDir); err != nil { + if err := util.CreateBaseDir(volume.HostDir); err != nil { return nil, errors.Wrap(err, "error docker local volume") } diff --git a/pkg/common/docker/client.go b/pkg/common/docker/client.go new file mode 100644 index 0000000..6d9a807 --- /dev/null +++ b/pkg/common/docker/client.go @@ -0,0 +1,118 @@ +package docker + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/hckops/hckctl/pkg/client/docker" + commonModel "github.com/hckops/hckctl/pkg/common/model" + "github.com/hckops/hckctl/pkg/event" +) + +type DockerCommonClient struct { + Client *docker.DockerClient + clientOpts *commonModel.DockerOptions + eventBus *event.EventBus +} + +func NewDockerCommonClient(eventBus *event.EventBus, dockerOpts *commonModel.DockerOptions) (*DockerCommonClient, error) { + eventBus.Publish(newInitDockerClientEvent()) + + dockerClient, err := docker.NewDockerClient() + if err != nil { + return nil, errors.Wrap(err, "error docker common client") + } + + return &DockerCommonClient{ + Client: dockerClient, + clientOpts: dockerOpts, + eventBus: eventBus, + }, nil +} + +func (common *DockerCommonClient) Close() error { + common.eventBus.Publish(newCloseDockerClientEvent()) + common.eventBus.Close() + return common.Client.Close() +} + +func (common *DockerCommonClient) PullImageOffline(imageName string, onImagePullCallback func()) error { + + imagePullOpts := &docker.ImagePullOpts{ + ImageName: imageName, + OnImagePullCallback: onImagePullCallback, + } + common.eventBus.Publish(newImagePullDockerEvent(imageName)) + if err := common.Client.ImagePull(imagePullOpts); err != nil { + // ignore error and try to use an existing image if exists + if common.clientOpts.IgnoreImagePullError { + common.eventBus.Publish(newImagePullIgnoreDockerEvent(imageName)) + } else { + // do not allow offline + return err + } + } + + // cleanup obsolete nightly images + imageRemoveOpts := &docker.ImageRemoveOpts{ + OnImageRemoveCallback: func(imageId string) { + common.eventBus.Publish(newImageRemoveDockerEvent(imageId)) + }, + OnImageRemoveErrorCallback: func(imageId string, err error) { + // ignore error: keep images used by existing containers + common.eventBus.Publish(newImageRemoveIgnoreDockerEvent(imageId, err)) + }, + } + if err := common.Client.ImageRemoveDangling(imageRemoveOpts); err != nil { + return err + } + + return nil +} + +func buildVpnSidecarName(containerName string) string { + // expect valid name always + tokens := strings.Split(containerName, "-") + return fmt.Sprintf("sidecar-vpn-%s", tokens[len(tokens)-1]) +} + +func (common *DockerCommonClient) StartVpnSidecar(baseContainerName string, vpnInfo *commonModel.VpnNetworkInfo) (string, error) { + + containerName := buildVpnSidecarName(baseContainerName) + imageName := commonModel.SidecarVpnImageName + // base directory "/usr/share" must exist + vpnConfigPath := "/usr/share/client.ovpn" + + if err := common.PullImageOffline(imageName, func() { + common.eventBus.Publish(newVpnSidecarConnectDockerEvent(vpnInfo.Name)) + common.eventBus.Publish(newVpnSidecarConnectDockerLoaderEvent(vpnInfo.Name)) + }); err != nil { + return "", err + } + + containerOpts := &docker.ContainerCreateOpts{ + ContainerName: containerName, + ContainerConfig: docker.BuildVpnContainerConfig(imageName, vpnConfigPath), + HostConfig: docker.BuildVpnHostConfig(), + WaitStatus: false, + CaptureInterrupt: false, // edge case: killing this while creating will produce an orphan sidecar container + OnContainerCreateCallback: func(containerId string) error { + // upload openvpn config file + return common.Client.CopyFileToContainer(containerId, vpnInfo.LocalPath, vpnConfigPath) + }, + OnContainerStatusCallback: func(status string) { + common.eventBus.Publish(newVpnSidecarCreateStatusDockerEvent(status)) + }, + OnContainerStartCallback: func() {}, + } + // sidecarId + containerId, err := common.Client.ContainerCreate(containerOpts) + if err != nil { + return "", err + } + common.eventBus.Publish(newVpnSidecarCreateDockerEvent(containerName, containerId)) + + return containerId, nil +} diff --git a/pkg/task/docker/docker_test.go b/pkg/common/docker/client_test.go similarity index 100% rename from pkg/task/docker/docker_test.go rename to pkg/common/docker/client_test.go diff --git a/pkg/common/docker/events.go b/pkg/common/docker/events.go new file mode 100644 index 0000000..c486f33 --- /dev/null +++ b/pkg/common/docker/events.go @@ -0,0 +1,64 @@ +package docker + +import ( + "fmt" + "github.com/hckops/hckctl/pkg/common/model" + "github.com/hckops/hckctl/pkg/event" +) + +type dockerCommonEvent struct { + kind event.EventKind + value string +} + +func (e *dockerCommonEvent) Source() string { + return model.DockerProvider +} + +func (e *dockerCommonEvent) Kind() event.EventKind { + return e.kind +} + +func (e *dockerCommonEvent) String() string { + return e.value +} + +func newInitDockerClientEvent() *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogDebug, value: "init docker client"} +} + +func newCloseDockerClientEvent() *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogDebug, value: "close docker client"} +} + +func newImagePullDockerEvent(imageName string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogInfo, value: fmt.Sprintf("image pull: imageName=%s", imageName)} +} + +func newImagePullIgnoreDockerEvent(imageName string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogWarning, value: fmt.Sprintf("image pull ignored: imageName=%s", imageName)} +} + +func newImageRemoveDockerEvent(imageId string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogInfo, value: fmt.Sprintf("image remove: imageId=%s", imageId)} +} + +func newImageRemoveIgnoreDockerEvent(imageId string, err error) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogWarning, value: fmt.Sprintf("image remove ignored: imageId=%s error=%v", imageId, err)} +} + +func newVpnSidecarCreateDockerEvent(containerName string, containerId string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogInfo, value: fmt.Sprintf("sidecar-vpn create: containerName=%s containerId=%s", containerName, containerId)} +} + +func newVpnSidecarCreateStatusDockerEvent(status string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogDebug, value: status} +} + +func newVpnSidecarConnectDockerEvent(vpnName string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LogInfo, value: fmt.Sprintf("sidecar-vpn connect: vpnName=%s", vpnName)} +} + +func newVpnSidecarConnectDockerLoaderEvent(vpnName string) *dockerCommonEvent { + return &dockerCommonEvent{kind: event.LoaderUpdate, value: fmt.Sprintf("connecting to %s", vpnName)} +} diff --git a/pkg/common/model/constant.go b/pkg/common/model/constant.go index 1806620..cda8f46 100644 --- a/pkg/common/model/constant.go +++ b/pkg/common/model/constant.go @@ -1,6 +1,10 @@ package model const ( + DockerProvider = "docker" + KubernetesProvider = "kube" + CloudProvider = "cloud" + SidecarVpnImageName = "hckops/alpine-openvpn:latest" - MountedShareDir = "/hck/share" + MountShareDir = "/hck/share" ) diff --git a/pkg/task/docker/client.go b/pkg/task/docker/client.go index fb7e55c..37e018a 100644 --- a/pkg/task/docker/client.go +++ b/pkg/task/docker/client.go @@ -1,14 +1,14 @@ package docker import ( - "github.com/hckops/hckctl/pkg/client/docker" + commonDocker "github.com/hckops/hckctl/pkg/common/docker" commonModel "github.com/hckops/hckctl/pkg/common/model" "github.com/hckops/hckctl/pkg/event" taskModel "github.com/hckops/hckctl/pkg/task/model" ) type DockerTaskClient struct { - client *docker.DockerClient + docker *commonDocker.DockerCommonClient clientOpts *commonModel.DockerOptions eventBus *event.EventBus } @@ -26,5 +26,6 @@ func (task *DockerTaskClient) Events() *event.EventBus { } func (task *DockerTaskClient) Run(opts *taskModel.RunOptions) error { + // TODO defer task.close() return task.runTask(opts) } diff --git a/pkg/task/docker/docker.go b/pkg/task/docker/docker.go index 7017931..9a95543 100644 --- a/pkg/task/docker/docker.go +++ b/pkg/task/docker/docker.go @@ -1,36 +1,32 @@ package docker import ( - "fmt" "path" - "strings" "github.com/pkg/errors" "github.com/hckops/hckctl/pkg/client/docker" + commonDocker "github.com/hckops/hckctl/pkg/common/docker" commonModel "github.com/hckops/hckctl/pkg/common/model" taskModel "github.com/hckops/hckctl/pkg/task/model" ) func newDockerTaskClient(commonOpts *taskModel.CommonTaskOptions, dockerOpts *commonModel.DockerOptions) (*DockerTaskClient, error) { - commonOpts.EventBus.Publish(newInitDockerClientEvent()) - dockerClient, err := docker.NewDockerClient() + dockerClient, err := commonDocker.NewDockerCommonClient(commonOpts.EventBus, dockerOpts) if err != nil { - return nil, errors.Wrap(err, "error docker task") + return nil, errors.Wrap(err, "error docker task client") } return &DockerTaskClient{ - client: dockerClient, + docker: dockerClient, clientOpts: dockerOpts, eventBus: commonOpts.EventBus, }, nil } func (task *DockerTaskClient) close() error { - task.eventBus.Publish(newCloseDockerClientEvent()) - task.eventBus.Close() - return task.client.Close() + return task.docker.Client.Close() } func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { @@ -41,20 +37,21 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { // vpn sidecar var networkMode string if opts.NetworkInfo.Vpn != nil { - sidecarContainerName := buildVpnSidecarName(containerName) - if sidecarContainerId, err := task.startVpnSidecar(sidecarContainerName, opts.NetworkInfo.Vpn); err != nil { + if sidecarContainerId, err := task.docker.StartVpnSidecar(containerName, opts.NetworkInfo.Vpn); err != nil { return err } else { networkMode = docker.ContainerNetworkMode(sidecarContainerId) // remove sidecar on exit - defer task.client.ContainerRemove(sidecarContainerId) + defer task.docker.Client.ContainerRemove(sidecarContainerId) } } else { networkMode = docker.DefaultNetworkMode() } imageName := opts.Template.Image.Name() - if err := task.pullImage(imageName, newImagePullDockerLoaderEvent(imageName)); err != nil { + if err := task.docker.PullImageOffline(imageName, func() { + task.eventBus.Publish(newImagePullDockerLoaderEvent(imageName)) + }); err != nil { return err } @@ -78,7 +75,7 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { Volumes: []docker.ContainerVolume{ { HostDir: opts.ShareDir, - ContainerDir: commonModel.MountedShareDir, + ContainerDir: commonModel.MountShareDir, }, }, }) @@ -87,14 +84,14 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { } networkName := task.clientOpts.NetworkName - networkId, err := task.client.NetworkUpsert(networkName) + networkId, err := task.docker.Client.NetworkUpsert(networkName) if err != nil { return err } task.eventBus.Publish(newNetworkUpsertDockerEvent(networkName, networkId)) task.eventBus.Publish(newContainerCreateDockerLoaderEvent()) - // TODO prefix file name with unix timestamp to order file? + // TODO prefix log file name with unix timestamp to order files? logFileName := path.Join(opts.LogDir, containerName) containerOpts := &docker.ContainerCreateOpts{ ContainerName: containerName, @@ -106,11 +103,11 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { OnContainerInterruptCallback: func(containerId string) { // returns control to runTask, it will correctly invoke defer to remove the sidecar // unless it's interrupted while the sidecar is being created - task.client.ContainerRemove(containerId) + task.docker.Client.ContainerRemove(containerId) }, OnContainerCreateCallback: func(string) error { return nil }, OnContainerWaitCallback: func(containerId string) error { - task.eventBus.Publish(newVolumeMountDockerEvent(containerId, opts.ShareDir, commonModel.MountedShareDir)) + task.eventBus.Publish(newVolumeMountDockerEvent(containerId, opts.ShareDir, commonModel.MountShareDir)) // stop loader task.eventBus.Publish(newContainerWaitDockerLoaderEvent()) @@ -119,7 +116,7 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { // TODO add flag to TaskV1 template to use "ContainerLogsStd" if command is "help" or "version" // tail logs before blocking task.eventBus.Publish(newContainerLogDockerEvent(logFileName)) - return task.client.ContainerLogsTee(containerId, logFileName) + return task.docker.Client.ContainerLogsTee(containerId, logFileName) }, OnContainerStatusCallback: func(status string) { task.eventBus.Publish(newContainerCreateStatusDockerEvent(status)) @@ -127,7 +124,7 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { OnContainerStartCallback: func() {}, } // taskId - containerId, err := task.client.ContainerCreate(containerOpts) + containerId, err := task.docker.Client.ContainerCreate(containerOpts) if err != nil { return err } @@ -135,80 +132,5 @@ func (task *DockerTaskClient) runTask(opts *taskModel.RunOptions) error { task.eventBus.Publish(newContainerLogDockerConsoleEvent(logFileName)) // remove temporary container - return task.client.ContainerRemove(containerId) -} - -func (task *DockerTaskClient) pullImage(imageName string, pullEvent *dockerTaskEvent) error { - imagePullOpts := &docker.ImagePullOpts{ - ImageName: imageName, - OnImagePullCallback: func() { - task.eventBus.Publish(pullEvent) - }, - } - task.eventBus.Publish(newImagePullDockerEvent(imageName)) - if err := task.client.ImagePull(imagePullOpts); err != nil { - // try to use an existing image if exists - if task.clientOpts.IgnoreImagePullError { - task.eventBus.Publish(newImagePullIgnoreDockerEvent(imageName)) - } else { - // do not allow offline - return err - } - } - - // cleanup obsolete nightly images - imageRemoveOpts := &docker.ImageRemoveOpts{ - OnImageRemoveCallback: func(imageId string) { - task.eventBus.Publish(newImageRemoveDockerEvent(imageId)) - }, - OnImageRemoveErrorCallback: func(imageId string, err error) { - // ignore error: keep images used by existing containers - task.eventBus.Publish(newImageRemoveIgnoreDockerEvent(imageId, err)) - }, - } - if err := task.client.ImageRemoveDangling(imageRemoveOpts); err != nil { - return err - } - - return nil -} - -func buildVpnSidecarName(taskName string) string { - tokens := strings.Split(taskName, "-") - return fmt.Sprintf("sidecar-vpn-%s", tokens[len(tokens)-1]) -} - -func (task *DockerTaskClient) startVpnSidecar(containerName string, vpnInfo *commonModel.VpnNetworkInfo) (string, error) { - - imageName := commonModel.SidecarVpnImageName - // base directory "/usr/share" must exist - vpnConfigPath := "/usr/share/client.ovpn" - - if err := task.pullImage(imageName, newVpnConnectDockerLoaderEvent(vpnInfo.Name)); err != nil { - return "", err - } - - containerOpts := &docker.ContainerCreateOpts{ - ContainerName: containerName, - ContainerConfig: docker.BuildVpnContainerConfig(imageName, vpnConfigPath), - HostConfig: docker.BuildVpnHostConfig(), - WaitStatus: false, - CaptureInterrupt: false, // edge case: killing this while creating will produce an orphan sidecar container - OnContainerCreateCallback: func(containerId string) error { - // upload openvpn config file - return task.client.CopyFileToContainer(containerId, vpnInfo.LocalPath, vpnConfigPath) - }, - OnContainerStatusCallback: func(status string) { - task.eventBus.Publish(newContainerCreateStatusDockerEvent(status)) - }, - OnContainerStartCallback: func() {}, - } - // sidecarId - containerId, err := task.client.ContainerCreate(containerOpts) - if err != nil { - return "", err - } - task.eventBus.Publish(newContainerCreateDockerEvent("sidecar-vpn", containerName, containerId)) - - return containerId, nil + return task.docker.Client.ContainerRemove(containerId) } diff --git a/pkg/task/docker/events.go b/pkg/task/docker/events.go index d4d03ee..68e98e0 100644 --- a/pkg/task/docker/events.go +++ b/pkg/task/docker/events.go @@ -24,34 +24,10 @@ func (e *dockerTaskEvent) String() string { return e.value } -func newInitDockerClientEvent() *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogDebug, value: "init docker client"} -} - -func newCloseDockerClientEvent() *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogDebug, value: "close docker client"} -} - -func newImagePullDockerEvent(imageName string) *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogInfo, value: fmt.Sprintf("image pull: imageName=%s", imageName)} -} - func newImagePullDockerLoaderEvent(imageName string) *dockerTaskEvent { return &dockerTaskEvent{kind: event.LoaderUpdate, value: fmt.Sprintf("pulling image %s", imageName)} } -func newImagePullIgnoreDockerEvent(imageName string) *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogWarning, value: fmt.Sprintf("image pull ignored: imageName=%s", imageName)} -} - -func newImageRemoveDockerEvent(imageId string) *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogInfo, value: fmt.Sprintf("image remove: imageId=%s", imageId)} -} - -func newImageRemoveIgnoreDockerEvent(imageId string, err error) *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LogWarning, value: fmt.Sprintf("image remove ignored: imageId=%s error=%v", imageId, err)} -} - func newNetworkUpsertDockerEvent(networkName string, networkId string) *dockerTaskEvent { return &dockerTaskEvent{kind: event.LogInfo, value: fmt.Sprintf("network upsert: networkName=%s networkId=%s", networkName, networkId)} } @@ -83,7 +59,3 @@ func newContainerCreateDockerLoaderEvent() *dockerTaskEvent { func newContainerWaitDockerLoaderEvent() *dockerTaskEvent { return &dockerTaskEvent{kind: event.LoaderStop, value: "waiting"} } - -func newVpnConnectDockerLoaderEvent(vpnName string) *dockerTaskEvent { - return &dockerTaskEvent{kind: event.LoaderUpdate, value: fmt.Sprintf("connecting to %s", vpnName)} -} diff --git a/pkg/task/model/provider.go b/pkg/task/model/provider.go index 6118d95..3aa7133 100644 --- a/pkg/task/model/provider.go +++ b/pkg/task/model/provider.go @@ -1,11 +1,15 @@ package model +import ( + "github.com/hckops/hckctl/pkg/common/model" +) + type TaskProvider string const ( - Docker TaskProvider = "docker" - Kubernetes TaskProvider = "kube" - Cloud TaskProvider = "cloud" + Docker TaskProvider = model.DockerProvider + Kubernetes TaskProvider = model.KubernetesProvider + Cloud TaskProvider = model.CloudProvider ) func (p TaskProvider) String() string {