2022-10-30 23:26:49 +00:00
// Copyright 2022 Woodpecker 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.
2017-03-05 07:56:08 +00:00
package docker
import (
"context"
"io"
2023-08-07 19:13:26 +00:00
"net/http"
2019-04-07 12:58:40 +00:00
"os"
2023-08-07 19:13:26 +00:00
"path/filepath"
2022-03-08 15:21:43 +00:00
"strings"
2017-03-05 07:56:08 +00:00
2021-09-26 12:43:14 +00:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
2023-08-07 19:13:26 +00:00
"github.com/docker/go-connections/tlsconfig"
2021-09-26 19:51:59 +00:00
"github.com/moby/moby/client"
"github.com/moby/moby/pkg/jsonmessage"
"github.com/moby/moby/pkg/stdcopy"
"github.com/moby/term"
2021-11-23 14:36:52 +00:00
"github.com/rs/zerolog/log"
2023-03-19 19:24:43 +00:00
"github.com/urfave/cli/v2"
2021-09-26 12:43:14 +00:00
2023-12-08 07:15:08 +00:00
backend "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
2017-03-05 07:56:08 +00:00
)
2021-11-27 01:29:14 +00:00
type docker struct {
2022-04-29 13:15:32 +00:00
client client . APIClient
enableIPv6 bool
network string
2022-09-26 14:59:26 +00:00
volumes [ ] string
2023-11-01 14:38:37 +00:00
info types . Info
2017-03-05 07:56:08 +00:00
}
2023-07-09 19:03:19 +00:00
const (
networkDriverNAT = "nat"
networkDriverBridge = "bridge"
volumeDriver = "local"
)
2023-12-14 18:20:47 +00:00
// New returns a new Docker Backend.
func New ( ) backend . Backend {
2021-11-27 01:29:14 +00:00
return & docker {
2021-11-26 02:34:48 +00:00
client : nil ,
2017-03-05 07:56:08 +00:00
}
}
2021-11-27 01:29:14 +00:00
func ( e * docker ) Name ( ) string {
2021-11-26 02:34:48 +00:00
return "docker"
}
2023-03-19 19:24:43 +00:00
func ( e * docker ) IsAvailable ( context . Context ) bool {
2022-02-08 23:08:20 +00:00
if os . Getenv ( "DOCKER_HOST" ) != "" {
return true
}
2021-11-27 01:29:14 +00:00
_ , err := os . Stat ( "/var/run/docker.sock" )
return err == nil
2021-11-26 02:34:48 +00:00
}
2023-08-07 19:13:26 +00:00
func httpClientOfOpts ( dockerCertPath string , verifyTLS bool ) * http . Client {
if dockerCertPath == "" {
return nil
}
options := tlsconfig . Options {
CAFile : filepath . Join ( dockerCertPath , "ca.pem" ) ,
CertFile : filepath . Join ( dockerCertPath , "cert.pem" ) ,
KeyFile : filepath . Join ( dockerCertPath , "key.pem" ) ,
InsecureSkipVerify : ! verifyTLS ,
}
tlsConf , err := tlsconfig . Client ( options )
2017-03-05 07:56:08 +00:00
if err != nil {
2023-08-07 19:13:26 +00:00
log . Error ( ) . Err ( err ) . Msg ( "could not create http client out of docker backend options" )
return nil
2017-03-05 07:56:08 +00:00
}
2021-11-26 02:34:48 +00:00
2023-08-07 19:13:26 +00:00
return & http . Client {
Transport : & http . Transport { TLSClientConfig : tlsConf } ,
CheckRedirect : client . CheckRedirect ,
}
}
2023-12-14 18:20:47 +00:00
// Load new client for Docker Backend using environment variables.
func ( e * docker ) Load ( ctx context . Context ) ( * backend . BackendInfo , error ) {
2023-03-19 19:24:43 +00:00
c , ok := ctx . Value ( backend . CliContext ) . ( * cli . Context )
if ! ok {
2023-11-01 14:38:37 +00:00
return nil , backend . ErrNoCliContextFound
2023-03-19 19:24:43 +00:00
}
2022-04-29 13:15:32 +00:00
2023-08-07 19:13:26 +00:00
var dockerClientOpts [ ] client . Opt
if httpClient := httpClientOfOpts ( c . String ( "backend-docker-cert" ) , c . Bool ( "backend-docker-tls-verify" ) ) ; httpClient != nil {
dockerClientOpts = append ( dockerClientOpts , client . WithHTTPClient ( httpClient ) )
}
if dockerHost := c . String ( "backend-docker-host" ) ; dockerHost != "" {
dockerClientOpts = append ( dockerClientOpts , client . WithHost ( dockerHost ) )
}
if dockerAPIVersion := c . String ( "backend-docker-api-version" ) ; dockerAPIVersion != "" {
dockerClientOpts = append ( dockerClientOpts , client . WithVersion ( dockerAPIVersion ) )
} else {
dockerClientOpts = append ( dockerClientOpts , client . WithAPIVersionNegotiation ( ) )
}
cl , err := client . NewClientWithOpts ( dockerClientOpts ... )
if err != nil {
2023-11-01 14:38:37 +00:00
return nil , err
2023-08-07 19:13:26 +00:00
}
e . client = cl
2023-11-01 14:38:37 +00:00
e . info , err = cl . Info ( ctx )
if err != nil {
return nil , err
}
2023-08-07 19:13:26 +00:00
e . enableIPv6 = c . Bool ( "backend-docker-ipv6" )
2023-03-19 19:24:43 +00:00
e . network = c . String ( "backend-docker-network" )
2022-04-29 13:15:32 +00:00
2023-03-19 19:24:43 +00:00
volumes := strings . Split ( c . String ( "backend-docker-volumes" ) , "," )
2022-09-26 14:59:26 +00:00
e . volumes = make ( [ ] string , 0 , len ( volumes ) )
// Validate provided volume definitions
for _ , v := range volumes {
2022-09-27 09:43:35 +00:00
if v == "" {
continue
}
2022-09-26 14:59:26 +00:00
parts , err := splitVolumeParts ( v )
if err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "invalid volume '%s' provided in WOODPECKER_BACKEND_DOCKER_VOLUMES" , v )
continue
}
e . volumes = append ( e . volumes , strings . Join ( parts , ":" ) )
}
2023-12-14 18:20:47 +00:00
return & backend . BackendInfo {
2023-11-01 14:38:37 +00:00
Platform : e . info . OSType + "/" + normalizeArchType ( e . info . Architecture ) ,
} , nil
2017-03-05 07:56:08 +00:00
}
2023-07-20 18:39:20 +00:00
func ( e * docker ) SetupWorkflow ( _ context . Context , conf * backend . Config , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msg ( "create workflow environment" )
2017-03-05 07:56:08 +00:00
for _ , vol := range conf . Volumes {
2023-10-14 10:39:45 +00:00
_ , err := e . client . VolumeCreate ( noContext , volume . CreateOptions {
2023-07-09 19:03:19 +00:00
Name : vol . Name ,
Driver : volumeDriver ,
2017-03-05 07:56:08 +00:00
} )
if err != nil {
return err
}
}
2023-07-09 19:03:19 +00:00
networkDriver := networkDriverBridge
2023-11-01 14:38:37 +00:00
if e . info . OSType == "windows" {
2023-07-09 19:03:19 +00:00
networkDriver = networkDriverNAT
}
2021-09-24 14:29:26 +00:00
for _ , n := range conf . Networks {
_ , err := e . client . NetworkCreate ( noContext , n . Name , types . NetworkCreate {
2023-07-09 19:03:19 +00:00
Driver : networkDriver ,
2022-04-29 13:15:32 +00:00
EnableIPv6 : e . enableIPv6 ,
2017-03-05 07:56:08 +00:00
} )
if err != nil {
return err
}
}
return nil
}
2023-07-20 18:39:20 +00:00
func ( e * docker ) StartStep ( ctx context . Context , step * backend . Step , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "start step %s" , step . Name )
2023-11-01 14:38:37 +00:00
config := e . toConfig ( step )
2022-10-28 15:38:53 +00:00
hostConfig := toHostConfig ( step )
2023-07-10 15:59:25 +00:00
containerName := toContainerName ( step )
2017-03-05 07:56:08 +00:00
// create pull options with encoded authorization credentials.
pullopts := types . ImagePullOptions { }
2022-10-28 15:38:53 +00:00
if step . AuthConfig . Username != "" && step . AuthConfig . Password != "" {
pullopts . RegistryAuth , _ = encodeAuthToBase64 ( step . AuthConfig )
2017-03-05 07:56:08 +00:00
}
// automatically pull the latest version of the image if requested
// by the process configuration.
2022-10-28 15:38:53 +00:00
if step . Pull {
2019-04-07 12:58:40 +00:00
responseBody , perr := e . client . ImagePull ( ctx , config . Image , pullopts )
2017-03-05 07:56:08 +00:00
if perr == nil {
2023-07-10 15:59:25 +00:00
// TODO(1936): show image pull progress in web-ui
2019-04-07 12:58:40 +00:00
fd , isTerminal := term . GetFdInfo ( os . Stdout )
2021-11-23 14:36:52 +00:00
if err := jsonmessage . DisplayJSONMessagesStream ( responseBody , os . Stdout , fd , isTerminal , nil ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "DisplayJSONMessagesStream" )
}
2023-01-31 20:33:40 +00:00
responseBody . Close ( )
2017-03-05 07:56:08 +00:00
}
2022-09-26 12:52:28 +00:00
// Fix "Show warning when fail to auth to docker registry"
// (https://web.archive.org/web/20201023145804/https://github.com/drone/drone/issues/1917)
2022-10-28 15:38:53 +00:00
if perr != nil && step . AuthConfig . Password != "" {
2017-03-05 07:56:08 +00:00
return perr
}
}
2022-09-26 14:59:26 +00:00
// add default volumes to the host configuration
2023-01-31 20:33:40 +00:00
hostConfig . Binds = utils . DedupStrings ( append ( hostConfig . Binds , e . volumes ... ) )
2022-09-26 14:59:26 +00:00
2023-07-10 15:59:25 +00:00
_ , err := e . client . ContainerCreate ( ctx , config , hostConfig , nil , nil , containerName )
2021-09-26 12:43:14 +00:00
if client . IsErrNotFound ( err ) {
2017-03-05 07:56:08 +00:00
// automatically pull and try to re-create the image if the
// failure is caused because the image does not exist.
2019-04-07 12:58:40 +00:00
responseBody , perr := e . client . ImagePull ( ctx , config . Image , pullopts )
2017-03-05 07:56:08 +00:00
if perr != nil {
return perr
}
2023-07-10 15:59:25 +00:00
// TODO(1936): show image pull progress in web-ui
2019-04-07 12:58:40 +00:00
fd , isTerminal := term . GetFdInfo ( os . Stdout )
2021-11-23 14:36:52 +00:00
if err := jsonmessage . DisplayJSONMessagesStream ( responseBody , os . Stdout , fd , isTerminal , nil ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "DisplayJSONMessagesStream" )
}
2023-01-31 20:33:40 +00:00
responseBody . Close ( )
2017-03-05 07:56:08 +00:00
2023-07-10 15:59:25 +00:00
_ , err = e . client . ContainerCreate ( ctx , config , hostConfig , nil , nil , containerName )
2017-03-05 07:56:08 +00:00
}
if err != nil {
return err
}
2022-10-28 15:38:53 +00:00
if len ( step . NetworkMode ) == 0 {
for _ , net := range step . Networks {
2023-07-10 15:59:25 +00:00
err = e . client . NetworkConnect ( ctx , net . Name , containerName , & network . EndpointSettings {
2017-06-03 15:29:02 +00:00
Aliases : net . Aliases ,
} )
if err != nil {
return err
}
2017-03-05 07:56:08 +00:00
}
2022-04-29 13:15:32 +00:00
// join the container to an existing network
if e . network != "" {
2023-07-10 15:59:25 +00:00
err = e . client . NetworkConnect ( ctx , e . network , containerName , & network . EndpointSettings { } )
2022-04-29 13:15:32 +00:00
if err != nil {
return err
}
}
}
2017-03-05 07:56:08 +00:00
2023-07-10 15:59:25 +00:00
return e . client . ContainerStart ( ctx , containerName , startOpts )
2017-03-05 07:56:08 +00:00
}
2023-07-20 18:39:20 +00:00
func ( e * docker ) WaitStep ( ctx context . Context , step * backend . Step , taskUUID string ) ( * backend . State , error ) {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "wait for step %s" , step . Name )
2023-07-10 15:59:25 +00:00
containerName := toContainerName ( step )
wait , errc := e . client . ContainerWait ( ctx , containerName , "" )
2020-05-18 14:46:13 +00:00
select {
case <- wait :
case <- errc :
2017-03-05 07:56:08 +00:00
}
2023-07-10 15:59:25 +00:00
info , err := e . client . ContainerInspect ( ctx , containerName )
2017-03-05 07:56:08 +00:00
if err != nil {
return nil , err
}
return & backend . State {
Exited : true ,
ExitCode : info . State . ExitCode ,
OOMKilled : info . State . OOMKilled ,
} , nil
}
2023-07-20 18:39:20 +00:00
func ( e * docker ) TailStep ( ctx context . Context , step * backend . Step , taskUUID string ) ( io . ReadCloser , error ) {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "tail logs of step %s" , step . Name )
2023-07-10 15:59:25 +00:00
logs , err := e . client . ContainerLogs ( ctx , toContainerName ( step ) , logsOpts )
2017-03-05 07:56:08 +00:00
if err != nil {
return nil , err
}
rc , wc := io . Pipe ( )
2021-11-23 14:36:52 +00:00
// de multiplex 'logs' who contains two streams, previously multiplexed together using StdWriter
2017-03-05 07:56:08 +00:00
go func ( ) {
2021-11-23 14:36:52 +00:00
_ , _ = stdcopy . StdCopy ( wc , wc , logs )
_ = logs . Close ( )
_ = wc . Close ( )
2017-03-05 07:56:08 +00:00
} ( )
return rc , nil
}
2023-11-01 08:35:11 +00:00
func ( e * docker ) DestroyStep ( ctx context . Context , step * backend . Step , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "stop step %s" , step . Name )
containerName := toContainerName ( step )
if err := e . client . ContainerKill ( ctx , containerName , "9" ) ; err != nil && ! isErrContainerNotFoundOrNotRunning ( err ) {
return err
}
if err := e . client . ContainerRemove ( ctx , containerName , removeOpts ) ; err != nil && ! isErrContainerNotFoundOrNotRunning ( err ) {
return err
}
return nil
}
2023-07-20 18:39:20 +00:00
func ( e * docker ) DestroyWorkflow ( _ context . Context , conf * backend . Config , taskUUID string ) error {
log . Trace ( ) . Str ( "taskUUID" , taskUUID ) . Msgf ( "delete workflow environment" )
2017-03-05 07:56:08 +00:00
for _ , stage := range conf . Stages {
for _ , step := range stage . Steps {
2023-07-10 15:59:25 +00:00
containerName := toContainerName ( step )
if err := e . client . ContainerKill ( noContext , containerName , "9" ) ; err != nil && ! isErrContainerNotFoundOrNotRunning ( err ) {
2021-11-23 14:36:52 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "could not kill container '%s'" , stage . Name )
}
2023-07-10 15:59:25 +00:00
if err := e . client . ContainerRemove ( noContext , containerName , removeOpts ) ; err != nil && ! isErrContainerNotFoundOrNotRunning ( err ) {
2021-11-23 14:36:52 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "could not remove container '%s'" , stage . Name )
}
2017-03-05 07:56:08 +00:00
}
}
2021-09-24 14:29:26 +00:00
for _ , v := range conf . Volumes {
2021-11-23 14:36:52 +00:00
if err := e . client . VolumeRemove ( noContext , v . Name , true ) ; err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "could not remove volume '%s'" , v . Name )
}
2017-03-05 07:56:08 +00:00
}
2021-09-24 14:29:26 +00:00
for _ , n := range conf . Networks {
2021-11-23 14:36:52 +00:00
if err := e . client . NetworkRemove ( noContext , n . Name ) ; err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "could not remove network '%s'" , n . Name )
}
2017-03-05 07:56:08 +00:00
}
return nil
}
var (
noContext = context . Background ( )
startOpts = types . ContainerStartOptions { }
removeOpts = types . ContainerRemoveOptions {
RemoveVolumes : true ,
RemoveLinks : false ,
Force : false ,
}
logsOpts = types . ContainerLogsOptions {
Follow : true ,
ShowStdout : true ,
ShowStderr : true ,
Details : false ,
Timestamps : false ,
}
)
2022-03-08 15:21:43 +00:00
func isErrContainerNotFoundOrNotRunning ( err error ) bool {
// Error response from daemon: Cannot kill container: ...: No such container: ...
// Error response from daemon: Cannot kill container: ...: Container ... is not running"
2023-12-02 19:22:23 +00:00
// Error response from podman daemon: can only kill running containers. ... is in state exited
2022-03-08 15:21:43 +00:00
// Error: No such container: ...
2023-12-02 19:22:23 +00:00
return err != nil && ( strings . Contains ( err . Error ( ) , "No such container" ) || strings . Contains ( err . Error ( ) , "is not running" ) || strings . Contains ( err . Error ( ) , "can only kill running containers" ) )
2022-03-08 15:21:43 +00:00
}
2023-11-01 14:38:37 +00:00
// normalizeArchType converts the arch type reported by docker info into
// the runtime.GOARCH format
// TODO: find out if we we need to convert other arch types too
func normalizeArchType ( s string ) string {
switch s {
case "x86_64" :
return "amd64"
default :
return s
}
}