Skip to content

Commit

Permalink
Merge pull request #13 from FMotalleb/docker-connection
Browse files Browse the repository at this point in the history
feat: Docker-connection
  • Loading branch information
FMotalleb authored Jun 12, 2024
2 parents d5736c4 + f52ed00 commit 99fac0e
Show file tree
Hide file tree
Showing 14 changed files with 601 additions and 69 deletions.
File renamed without changes.
10 changes: 8 additions & 2 deletions config.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
jobs:
- name: Test Job
tasks:
- command: ip a -pt
retries: 5
- command: php -v
retry-delay: 5s
connections:
- image: docker-mysql-database-phpmyadmin
volumes:
- "/home/motalleb/Downloads:/var/local/test"
env:
SHELL: /bin/bash

schedulers:
- on-init: true
- interval: 10m10s
7 changes: 6 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ type (
OnFail []Task `mapstructure:"on-fail" json:"on_fail,omitempty"`
}
TaskConnection struct {
Local bool
Local bool `mapstructure:"local" json:"local,omitempty"`
DockerConnection string `mapstructure:"docker" json:"docker,omitempty"`
ContainerName string `mapstructure:"container" json:"container,omitempty"`
ImageName string `mapstructure:"image" json:"image,omitempty"`
Volumes []string `mapstructure:"volumes" json:"volumes,omitempty"`
Networks []string `mapstructure:"networks" json:"networks,omitempty"`
}
)
2 changes: 1 addition & 1 deletion config/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (t *Task) Validate(log *logrus.Entry) error {
)
}
if err := credential.Validate(log, t.UserName, t.GroupName); err != nil {
return err
log.WithError(err).Warn("Be careful when using credentials, in local mode you cant use credentials unless running as root")
}
if t.Command != "" && (t.Data != nil || t.Headers != nil) {
return fmt.Errorf("command cannot have data or headers field, violating command: `%s`", t.Command)
Expand Down
17 changes: 13 additions & 4 deletions core/cmd_connection/compiler.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
// Package connection
package connection

import (
"log"

"github.com/sirupsen/logrus"

"github.com/FMotalleb/crontab-go/abstraction"
"github.com/FMotalleb/crontab-go/config"
)

// CompileConnection compiles the task connection based on the provided configuration and logger.
// It returns an abstraction.CmdConnection interface based on the type of connection specified in the configuration.
// If the connection type is not recognized or invalid, it logs a fatal error and returns nil.
func CompileConnection(conn *config.TaskConnection, logger *logrus.Entry) abstraction.CmdConnection {
if conn.Local {
logger.Warn(conn)
switch {
case conn.Local:
return NewLocalCMDConn(logger)
case conn.ContainerName != "" && conn.ImageName == "":
return NewDockerAttachConnection(logger, conn)
case conn.ImageName != "":
return NewDockerCreateConnection(logger, conn)
}
log.Fatalln("cannot compile given taskConnection", conn)

logger.WithField("taskConnection", conn).Error("cannot compile given taskConnection")
return nil
}
125 changes: 125 additions & 0 deletions core/cmd_connection/docker_attach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package connection

import (
"bytes"
"context"
"io"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"

"github.com/FMotalleb/crontab-go/abstraction"
"github.com/FMotalleb/crontab-go/config"
)

type DockerAttachConnection struct {
conn *config.TaskConnection
log *logrus.Entry
cli *client.Client
execCFG *types.ExecConfig
containerID string
ctx context.Context
}

// NewDockerAttachConnection creates a new DockerAttachConnection instance.
// It initializes the connection configuration and logging fields.
// Parameters:
// - log: A logrus.Entry instance for logging purposes.
// - conn: A TaskConnection instance containing the connection configuration.
// Returns:
// - A new instance of DockerAttachConnection implementing the CmdConnection interface.
func NewDockerAttachConnection(log *logrus.Entry, conn *config.TaskConnection) abstraction.CmdConnection {
return &DockerAttachConnection{
conn: conn,
log: log.WithFields(
logrus.Fields{
"connection": "docker",
"docker-mode": "attach",
},
),
}
}

// Prepare sets up the DockerAttachConnection for executing a task.
// It reshapes the environment variables, sets the context, and creates an exec configuration.
// Parameters:
// - ctx: A context.Context instance for managing the request lifetime.
// - task: A Task instance containing the task configuration.
// Returns:
// - An error if the preparation fails, otherwise nil.
func (d *DockerAttachConnection) Prepare(ctx context.Context, task *config.Task) error {
shell, shellArgs, env := reshapeEnviron(task, d.log)
d.ctx = ctx
// Specify the container ID or name
d.containerID = d.conn.ContainerName
if d.conn.DockerConnection == "" {
d.log.Debug("No explicit docker connection specified, using default: `unix:///var/run/docker.sock`")
d.conn.DockerConnection = "unix:///var/run/docker.sock"
}
cmd := append(
[]string{shell},
append(shellArgs, task.Command)...,
)
// Create an exec configuration
d.execCFG = &types.ExecConfig{
AttachStdout: true,
AttachStderr: true,
Privileged: true,
Env: env,
WorkingDir: task.WorkingDirectory,
User: task.UserName,
Cmd: cmd,
}
return nil
}

// Connect establishes a connection to the Docker daemon.
// It initializes the Docker client with the specified connection settings.
// Returns:
// - An error if the connection fails, otherwise nil.
func (d *DockerAttachConnection) Connect() error {
cli, err := client.NewClientWithOpts(
client.WithHost(d.conn.DockerConnection),
)
if err != nil {
return err
}
d.cli = cli
return nil
}

// Execute runs the command in the Docker container and captures the output.
// It creates an exec instance, attaches to it, and reads the command output.
// Returns:
// - A byte slice containing the command output.
// - An error if the execution fails, otherwise nil.
func (d *DockerAttachConnection) Execute() ([]byte, error) {
// Create the exec instance
exec, err := d.cli.ContainerExecCreate(d.ctx, d.containerID, *d.execCFG)
if err != nil {
return nil, err
}

// Attach to the exec instance
resp, err := d.cli.ContainerExecAttach(d.ctx, exec.ID, types.ExecStartCheck{})
if err != nil {
return nil, err
}
defer resp.Close()

writer := bytes.NewBuffer([]byte{})
// Print the command output
_, err = io.Copy(writer, resp.Reader)
if err != nil {
return nil, err
}
return writer.Bytes(), nil
}

// Disconnect closes the connection to the Docker daemon.
// Returns:
// - An error if the disconnection fails, otherwise nil.
func (d *DockerAttachConnection) Disconnect() error {
return d.cli.Close()
}
180 changes: 180 additions & 0 deletions core/cmd_connection/docker_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package connection

import (
"bytes"
"context"
"io"
"strings"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"

"github.com/FMotalleb/crontab-go/abstraction"
"github.com/FMotalleb/crontab-go/config"
)

// DockerCreateConnection is a struct that manages the creation and execution of Docker containers.
type DockerCreateConnection struct {
conn *config.TaskConnection
log *logrus.Entry
cli *client.Client
imageName string

Check failure on line 23 in core/cmd_connection/docker_create.go

View workflow job for this annotation

GitHub Actions / analyze (go)

field `imageName` is unused (unused)
containerConfig *container.Config
hostConfig *container.HostConfig
networkConfig *network.NetworkingConfig
ctx context.Context
}

// NewDockerCreateConnection initializes a new DockerCreateConnection instance.
// Parameters:
// - log: A logrus.Entry instance for logging.
// - conn: A TaskConnection instance containing the connection configuration.
// Returns:
// - A new instance of DockerCreateConnection.
func NewDockerCreateConnection(log *logrus.Entry, conn *config.TaskConnection) abstraction.CmdConnection {
return &DockerCreateConnection{
conn: conn,
log: log.WithFields(
logrus.Fields{
"connection": "docker",
"docker-mode": "create",
},
),
}
}

// Prepare sets up the Docker container configuration based on the provided task.
// Parameters:
// - ctx: A context.Context instance for managing the lifecycle of the container.
// - task: A Task instance containing the task configuration.
// Returns:
// - An error if the preparation fails, otherwise nil.
func (d *DockerCreateConnection) Prepare(ctx context.Context, task *config.Task) error {
shell, shellArgs, env := reshapeEnviron(task, d.log)
d.ctx = ctx
if d.conn.DockerConnection == "" {
d.log.Debug("No explicit docker connection specified, using default: `unix:///var/run/docker.sock`")
d.conn.DockerConnection = "unix:///var/run/docker.sock"
}
cmd := append(
[]string{shell},
append(shellArgs, task.Command)...,
)
volumes := make(map[string]struct{})
for _, volume := range d.conn.Volumes {
inContainer := strings.Split(volume, ":")[1]
volumes[inContainer] = struct{}{}
}
// Create an exec configuration
d.containerConfig = &container.Config{
AttachStdout: true,
AttachStderr: true,
Env: env,
WorkingDir: task.WorkingDirectory,
User: task.UserName,
Cmd: cmd,
Image: d.conn.ImageName,
Volumes: volumes,
Entrypoint: []string{},
Shell: []string{},
}
d.hostConfig = &container.HostConfig{
Binds: d.conn.Volumes,
// AutoRemove: true,
}
endpointsConfig := make(map[string]*network.EndpointSettings)
for _, networkName := range d.conn.Networks {
endpointsConfig[networkName] = &network.EndpointSettings{}
}
d.networkConfig = &network.NetworkingConfig{
EndpointsConfig: endpointsConfig,
}
return nil
}

// Connect establishes a connection to the Docker daemon.
// Returns:
// - An error if the connection fails, otherwise nil.
func (d *DockerCreateConnection) Connect() error {
cli, err := client.NewClientWithOpts(
client.WithHost(d.conn.DockerConnection),
)
if err != nil {
return err
}
d.cli = cli
return nil
}

// Execute creates, starts, and logs the output of the Docker container.
// Returns:
// - A byte slice containing the command output.
// - An error if the execution fails, otherwise nil.
func (d *DockerCreateConnection) Execute() ([]byte, error) {
ctx := d.ctx
// Create the exec instance
exec, err := d.cli.ContainerCreate(
ctx,
d.containerConfig,
d.hostConfig,
d.networkConfig,
nil,
d.conn.ContainerName,
)
d.log.Debugf("container created: %v, warnings: %v", exec, exec.Warnings)
if err != nil {
return nil, err
}
defer d.cli.ContainerRemove(ctx, exec.ID,

Check failure on line 130 in core/cmd_connection/docker_create.go

View workflow job for this annotation

GitHub Actions / analyze (go)

Error return value of `d.cli.ContainerRemove` is not checked (errcheck)
container.RemoveOptions{
Force: true,
},
)
err = d.cli.ContainerStart(d.log.Context, exec.ID,
container.StartOptions{},
)
d.log.Debugf("container started: %v", exec)
if err != nil {
return nil, err
}
starting := true
for starting {
_, err := d.cli.ContainerStats(ctx, exec.ID, false)
if err == nil {
starting = false
}
}
d.log.Debugf("container ready to attach: %v", exec)
// Attach to the exec instance
resp, err := d.cli.ContainerLogs(
ctx,
exec.ID,
container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: false,
Details: true,
},
)
if err != nil {
return nil, err
}
defer resp.Close()

Check failure on line 164 in core/cmd_connection/docker_create.go

View workflow job for this annotation

GitHub Actions / analyze (go)

Error return value of `resp.Close` is not checked (errcheck)

writer := bytes.NewBuffer([]byte{})
// Print the command output
_, err = io.Copy(writer, resp)
if err != nil {
return writer.Bytes(), err
}
return writer.Bytes(), nil
}

// Disconnect closes the connection to the Docker daemon.
// Returns:
// - An error if the disconnection fails, otherwise nil.
func (d *DockerCreateConnection) Disconnect() error {
return d.cli.Close()
}
Loading

0 comments on commit 99fac0e

Please sign in to comment.