Verified Commit afa41f0b authored by Steve Azzopardi's avatar Steve Azzopardi 🌴 Committed by Steve Azzopardi

Refactor helpers/docker/helperimage package

The helper image is being used for both Kubernetes and Docker executor.
But the code does not represent this, instead it duplicates the logic,
such as creating the Linux tag and the name of the image.

Refactor the helperimage package so that it can be used for both, by
removing specific structs from the docker API, by defining helperimage
own Config struct.

The need for the refactor is for
!1243, because
we need to define different commands to run on the helper image, which
is located in `common/container.go` which needs to change depending on
the OS Type of docker, and we can't change it before we change how
Kubernetes uses the helper image.
parent 9d3538ff
......@@ -278,49 +278,46 @@ func (e *executor) getPrebuiltImage() (*types.ImageInspect, error) {
return e.getDockerImage(imageNameFromConfig)
}
helperImageInfo, err := helperimage.GetInfo(e.info)
if err != nil {
return nil, err
}
revision := "latest"
if common.REVISION != "HEAD" {
revision = common.REVISION
}
tag, err := helperImageInfo.Tag(revision)
helperImageInfo, err := helperimage.Get(revision, helperimage.Config{
OSType: e.info.OSType,
Architecture: e.info.Architecture,
OperatingSystem: e.info.OperatingSystem,
})
if err != nil {
return nil, err
}
// Try to find already loaded prebuilt image
imageName := fmt.Sprintf("%s:%s", prebuiltImageName, tag)
e.Debugln("Looking for prebuilt image", imageName, "...")
image, _, err := e.client.ImageInspectWithRaw(e.Context, imageName)
e.Debugln(fmt.Sprintf("Looking for prebuilt image %s...", helperImageInfo))
image, _, err := e.client.ImageInspectWithRaw(e.Context, helperImageInfo.String())
if err == nil {
return &image, nil
}
// Try to load prebuilt image from local filesystem
loadedImage := e.getLocalDockerImage(helperImageInfo, tag)
loadedImage := e.getLocalDockerImage(helperImageInfo)
if loadedImage != nil {
return loadedImage, nil
}
// Fallback to getting image from DockerHub
e.Debugln("Loading image from registry:", imageName)
return e.getDockerImage(imageName)
e.Debugln(fmt.Sprintf("Loading image form registry: %s", helperImageInfo))
return e.getDockerImage(helperImageInfo.String())
}
func (e *executor) getLocalDockerImage(helperImageInfo helperimage.Info, tag string) *types.ImageInspect {
if !helperImageInfo.IsSupportingLocalImport() {
func (e *executor) getLocalDockerImage(helperImageInfo helperimage.Info) *types.ImageInspect {
if !helperImageInfo.IsSupportingLocalImport {
return nil
}
architecture := helperImageInfo.Architecture()
architecture := helperImageInfo.Architecture
for _, dockerPrebuiltImagesPath := range DockerPrebuiltImagesPaths {
dockerPrebuiltImageFilePath := filepath.Join(dockerPrebuiltImagesPath, "prebuilt-"+architecture+prebuiltImageExtension)
image, err := e.loadPrebuiltImage(dockerPrebuiltImageFilePath, prebuiltImageName, tag)
image, err := e.loadPrebuiltImage(dockerPrebuiltImageFilePath, prebuiltImageName, helperImageInfo.Tag)
if err != nil {
e.Debugln("Failed to load prebuilt image from:", dockerPrebuiltImageFilePath, "error:", err)
continue
......
package helperimage
import (
"github.com/docker/docker/api/types"
"fmt"
"gitlab.com/gitlab-org/gitlab-runner/helpers/docker/errors"
)
......@@ -9,28 +9,43 @@ import (
const (
OSTypeLinux = "linux"
OSTypeWindows = "windows"
name = "gitlab/gitlab-runner-helper"
)
// Info provides information about the helper image that can be used to
// pull from Docker Hub.
type Info interface {
Architecture() string
Tag(revision string) (string, error)
IsSupportingLocalImport() bool
type Info struct {
Architecture string
Name string
Tag string
IsSupportingLocalImport bool
}
func (i Info) String() string {
return fmt.Sprintf("%s:%s", i.Name, i.Tag)
}
type infoFactory func(info types.Info) Info
// Config specifies details about the consumer of this package that need to be
// taken in consideration when building Container.
type Config struct {
OSType string
Architecture string
OperatingSystem string
}
type creator interface {
Create(revision string, cfg Config) (Info, error)
}
var supportedOsTypesFactories = map[string]infoFactory{
OSTypeWindows: newWindowsInfo,
OSTypeLinux: newLinuxInfo,
var supportedOsTypesFactories = map[string]creator{
OSTypeWindows: new(windowsInfo),
OSTypeLinux: new(linuxInfo),
}
func GetInfo(info types.Info) (Info, error) {
factory, ok := supportedOsTypesFactories[info.OSType]
func Get(revision string, cfg Config) (Info, error) {
factory, ok := supportedOsTypesFactories[cfg.OSType]
if !ok {
return nil, errors.NewErrOSNotSupported(info.OSType)
return Info{}, errors.NewErrOSNotSupported(cfg.OSType)
}
return factory(info), nil
return factory.Create(revision, cfg)
}
......@@ -3,7 +3,6 @@ package helperimage
import (
"testing"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/gitlab-runner/helpers/docker/errors"
......@@ -15,17 +14,25 @@ func TestGetInfo(t *testing.T) {
expectedHelperImageType interface{}
expectedError interface{}
}{
{osType: OSTypeLinux, expectedHelperImageType: &linuxInfo{}, expectedError: nil},
{osType: OSTypeWindows, expectedHelperImageType: &windowsInfo{}, expectedError: nil},
{osType: "unsupported", expectedHelperImageType: nil, expectedError: errors.NewErrOSNotSupported("unsupported")},
{osType: OSTypeLinux, expectedError: nil},
{osType: OSTypeWindows, expectedError: ErrUnsupportedOSVersion},
{osType: "unsupported", expectedError: errors.NewErrOSNotSupported("unsupported")},
}
for _, testCase := range testCases {
t.Run(testCase.osType, func(t *testing.T) {
i, err := GetInfo(types.Info{OSType: testCase.osType})
_, err := Get("HEAD", Config{OSType: testCase.osType})
assert.IsType(t, testCase.expectedHelperImageType, i)
assert.Equal(t, testCase.expectedError, err)
})
}
}
func TestContainerImage_String(t *testing.T) {
image := Info{
Name: "abc",
Tag: "1234",
}
assert.Equal(t, "abc:1234", image.String())
}
......@@ -3,24 +3,32 @@ package helperimage
import (
"fmt"
"runtime"
"github.com/docker/docker/api/types"
)
type linuxInfo struct {
dockerArch string
type linuxInfo struct{}
func (l *linuxInfo) Create(revision string, cfg Config) (Info, error) {
arch := l.architecture(cfg.Architecture)
return Info{
Architecture: arch,
Name: name,
Tag: fmt.Sprintf("%s-%s", arch, revision),
IsSupportingLocalImport: true,
}, nil
}
func (u *linuxInfo) Architecture() string {
switch u.dockerArch {
func (l *linuxInfo) architecture(arch string) string {
switch arch {
case "armv6l", "armv7l", "aarch64":
return "arm"
case "amd64":
return "x86_64"
}
if u.dockerArch != "" {
return u.dockerArch
if arch != "" {
return arch
}
switch runtime.GOARCH {
......@@ -30,17 +38,3 @@ func (u *linuxInfo) Architecture() string {
return runtime.GOARCH
}
}
func (u *linuxInfo) Tag(revision string) (string, error) {
return fmt.Sprintf("%s-%s", u.Architecture(), revision), nil
}
func (u *linuxInfo) IsSupportingLocalImport() bool {
return true
}
func newLinuxInfo(info types.Info) Info {
return &linuxInfo{
dockerArch: info.Architecture,
}
}
......@@ -5,45 +5,59 @@ import (
"runtime"
"testing"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func Test_linuxInfo_Tag(t *testing.T) {
func Test_linuxInfo_create(t *testing.T) {
cases := []struct {
name string
dockerArch string
revision string
expectedTag string
name string
dockerArch string
revision string
expectedInfo Info
}{
{
name: "When dockerArch not specified we fallback to runtime arch",
dockerArch: "",
revision: "2923a43",
expectedTag: fmt.Sprintf("%s-2923a43", getExpectedArch()),
name: "When dockerArch not specified we fallback to runtime arch",
dockerArch: "",
revision: "2923a43",
expectedInfo: Info{
Architecture: getExpectedArch(),
Name: name,
Tag: fmt.Sprintf("%s-2923a43", getExpectedArch()),
IsSupportingLocalImport: true,
},
},
{
name: "Docker runs on armv6l",
dockerArch: "armv6l",
revision: "2923a43",
expectedTag: "arm-2923a43",
name: "Docker runs on armv6l",
dockerArch: "armv6l",
revision: "2923a43",
expectedInfo: Info{
Architecture: "arm",
Name: name,
Tag: "arm-2923a43",
IsSupportingLocalImport: true,
},
},
{
name: "Docker runs on amd64",
dockerArch: "amd64",
revision: "2923a43",
expectedTag: "x86_64-2923a43",
name: "Docker runs on amd64",
dockerArch: "amd64",
revision: "2923a43",
expectedInfo: Info{
Architecture: "x86_64",
Name: name,
Tag: "x86_64-2923a43",
IsSupportingLocalImport: true,
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
u := newLinuxInfo(types.Info{Architecture: c.dockerArch})
l := new(linuxInfo)
tag, err := u.Tag(c.revision)
image, err := l.Create(c.revision, Config{Architecture: c.dockerArch})
assert.NoError(t, err)
assert.Equal(t, c.expectedTag, tag)
assert.Equal(t, c.expectedInfo, image)
})
}
}
......@@ -58,8 +72,3 @@ func getExpectedArch() string {
return runtime.GOARCH
}
func Test_linuxInfo_IsSupportingLocalImport(t *testing.T) {
u := newLinuxInfo(types.Info{})
assert.True(t, u.IsSupportingLocalImport())
}
......@@ -4,8 +4,6 @@ import (
"errors"
"fmt"
"strings"
"github.com/docker/docker/api/types"
)
const (
......@@ -25,39 +23,29 @@ var supportedOSVersions = map[string]string{
var ErrUnsupportedOSVersion = errors.New("could not determine windows version")
type windowsInfo struct {
operatingSystem string
}
type windowsInfo struct{}
func (*windowsInfo) Architecture() string {
return windowsSupportedArchitecture
}
func (u *windowsInfo) Tag(revision string) (string, error) {
osVersion, err := u.osVersion()
func (w *windowsInfo) Create(revision string, cfg Config) (Info, error) {
osVersion, err := w.osVersion(cfg.OperatingSystem)
if err != nil {
return "", err
return Info{}, err
}
return fmt.Sprintf("%s-%s-%s", u.Architecture(), revision, osVersion), nil
return Info{
Architecture: windowsSupportedArchitecture,
Name: name,
Tag: fmt.Sprintf("%s-%s-%s", windowsSupportedArchitecture, revision, osVersion),
IsSupportingLocalImport: false,
}, nil
}
func (u *windowsInfo) osVersion() (string, error) {
for operatingSystem, osVersion := range supportedOSVersions {
if strings.Contains(u.operatingSystem, fmt.Sprintf(" %s ", operatingSystem)) {
return osVersion, nil
func (w *windowsInfo) osVersion(operatingSystem string) (string, error) {
for osVersion, baseImage := range supportedOSVersions {
if strings.Contains(operatingSystem, fmt.Sprintf(" %s ", osVersion)) {
return baseImage, nil
}
}
return "", ErrUnsupportedOSVersion
}
func (u *windowsInfo) IsSupportingLocalImport() bool {
return false
}
func newWindowsInfo(info types.Info) Info {
return &windowsInfo{
operatingSystem: info.OperatingSystem,
}
}
......@@ -4,31 +4,45 @@ import (
"fmt"
"testing"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func Test_windowsInfo_Tag(t *testing.T) {
func Test_windowsInfo_create(t *testing.T) {
revision := "4011f186"
cases := []struct {
operatingSystem string
expectedVersion string
expectedInfo Info
expectedErr error
}{
{
operatingSystem: "Windows Server 2019 Datacenter Evaluation Version 1809 (OS Build 17763.316)",
expectedVersion: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1809),
expectedErr: nil,
expectedInfo: Info{
Architecture: windowsSupportedArchitecture,
Name: name,
Tag: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1809),
IsSupportingLocalImport: false,
},
expectedErr: nil,
},
{
operatingSystem: "Windows Server Datacenter Version 1809 (OS Build 1803.590)",
expectedVersion: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1809),
expectedErr: nil,
expectedInfo: Info{
Architecture: windowsSupportedArchitecture,
Name: name,
Tag: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1809),
IsSupportingLocalImport: false,
},
expectedErr: nil,
},
{
operatingSystem: "Windows Server Datacenter Version 1803 (OS Build 17134.590)",
expectedVersion: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1803),
expectedErr: nil,
expectedInfo: Info{
Architecture: windowsSupportedArchitecture,
Name: name,
Tag: fmt.Sprintf("%s-%s-%s", "x86_64", revision, baseImage1803),
IsSupportingLocalImport: false,
},
expectedErr: nil,
},
{
operatingSystem: "some random string",
......@@ -38,17 +52,12 @@ func Test_windowsInfo_Tag(t *testing.T) {
for _, c := range cases {
t.Run(c.operatingSystem, func(t *testing.T) {
w := newWindowsInfo(types.Info{OperatingSystem: c.operatingSystem})
w := new(windowsInfo)
tag, err := w.Tag(revision)
image, err := w.Create(revision, Config{OperatingSystem: c.operatingSystem})
assert.Equal(t, c.expectedVersion, tag)
assert.Equal(t, c.expectedInfo, image)
assert.Equal(t, c.expectedErr, err)
})
}
}
func Test_windowsInfo_IsSupportingLocalImport(t *testing.T) {
u := newWindowsInfo(types.Info{})
assert.False(t, u.IsSupportingLocalImport())
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment