Fix Docker Supervisord Vol Initialization (#3129)

* apply stashed supervisord docker changes from PR

Signed-off-by: Maysun J Faisal <maysun.j.faisal@ibm.com>

* rebase 1 with master

Signed-off-by: Maysun J Faisal <maysun.j.faisal@ibm.com>
This commit is contained in:
Maysun J Faisal
2020-06-13 21:29:30 -04:00
committed by GitHub
parent e5df06fb68
commit 6cbe763c6b
10 changed files with 1032 additions and 677 deletions

View File

@@ -37,17 +37,21 @@ type Adapter struct {
devfileBuildCmd string
devfileRunCmd string
supervisordVolumeName string
projectVolumeName string
}
// Push updates the component if a matching component exists or creates one if it doesn't exist
func (a Adapter) Push(parameters common.PushParameters) (err error) {
componentExists := utils.ComponentExists(a.Client, a.ComponentName)
componentExists, err := utils.ComponentExists(a.Client, a.Devfile.Data, a.ComponentName)
if err != nil {
return errors.Wrapf(err, "unable to determine if component %s exists", a.ComponentName)
}
// Process the volumes defined in the devfile
a.componentAliasToVolumes = common.GetVolumes(a.Devfile)
a.uniqueStorage, a.volumeNameToDockerVolName, err = storage.ProcessVolumes(&a.Client, a.ComponentName, a.componentAliasToVolumes)
if err != nil {
return errors.Wrapf(err, "Unable to process volumes for component %s", a.ComponentName)
return errors.Wrapf(err, "unable to process volumes for component %s", a.ComponentName)
}
a.devfileInitCmd = parameters.DevfileInitCmd
@@ -64,19 +68,14 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
}
s.End(true)
// Get the supervisord volume
supervisordLabels := utils.GetSupervisordVolumeLabels()
supervisordVolumes, err := a.Client.GetVolumesByLabel(supervisordLabels)
a.supervisordVolumeName, err = a.createAndInitSupervisordVolumeIfReqd(componentExists)
if err != nil {
return errors.Wrapf(err, "unable to retrieve supervisord volume for component %s", a.ComponentName)
return errors.Wrapf(err, "unable to create supervisord volume for component %s", a.ComponentName)
}
if len(supervisordVolumes) == 0 {
a.supervisordVolumeName, err = utils.CreateAndInitSupervisordVolume(a.Client)
if err != nil {
return errors.Wrapf(err, "unable to create supervisord volume for component %s", a.ComponentName)
}
} else {
a.supervisordVolumeName = supervisordVolumes[0].Name
a.projectVolumeName, err = a.createProjectVolumeIfReqd()
if err != nil {
return errors.Wrapf(err, "unable to determine the project source volume for component %s", a.ComponentName)
}
if componentExists {
@@ -131,7 +130,8 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
// DoesComponentExist returns true if a component with the specified name exists, false otherwise
func (a Adapter) DoesComponentExist(cmpName string) bool {
return utils.ComponentExists(a.Client, cmpName)
componentExists, _ := utils.ComponentExists(a.Client, a.Devfile.Data, cmpName)
return componentExists
}
// getFirstContainerWithSourceVolume returns the first container that set mountSources: true
@@ -199,10 +199,10 @@ func (a Adapter) Delete(labels map[string]string) error {
if snVal := vol.Labels["storage-name"]; len(strings.TrimSpace(snVal)) > 0 {
vols = append(vols, vol)
} else {
if typeVal := vol.Labels["type"]; typeVal == "projects" {
vols = append(vols, vol)
}
} else if typeVal := vol.Labels["type"]; typeVal == utils.ProjectsVolume {
vols = append(vols, vol)
} else if typeVal := vol.Labels["type"]; typeVal == utils.SupervisordVolume {
vols = append(vols, vol)
}
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/golang/mock/gomock"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
devfileParser "github.com/openshift/odo/pkg/devfile/parser"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
"github.com/openshift/odo/pkg/testingutil"
@@ -127,32 +128,39 @@ func TestDoesComponentExist(t *testing.T) {
tests := []struct {
name string
client *lclient.Client
componentType versionsCommon.DevfileComponentType
components []common.DevfileComponent
componentName string
getComponentName string
want bool
}{
{
name: "Case 1: Valid component name",
client: fakeClient,
componentType: versionsCommon.ContainerComponentType,
name: "Case 1: Valid component name",
client: fakeClient,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
testingutil.GetFakeComponent("alias2"),
},
componentName: "golang",
getComponentName: "golang",
want: true,
},
{
name: "Case 2: Non-existent component name",
componentType: versionsCommon.ContainerComponentType,
client: fakeClient,
componentName: "test-name",
name: "Case 2: Non-existent component name",
client: fakeClient,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
},
componentName: "test",
getComponentName: "fake-component",
want: false,
},
{
name: "Case 3: Docker client error",
componentType: versionsCommon.ContainerComponentType,
client: fakeErrorClient,
componentName: "test-name",
name: "Case 3: Docker client error",
client: fakeErrorClient,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
},
componentName: "test",
getComponentName: "fake-component",
want: false,
},
@@ -161,11 +169,7 @@ func TestDoesComponentExist(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
Components: []versionsCommon.DevfileComponent{
{
Type: tt.componentType,
},
},
Components: tt.components,
},
}
@@ -571,6 +575,44 @@ func TestAdapterDeleteVolumes(t *testing.T) {
},
expectToDelete: []string{},
},
{
name: "Case 7: Should delete both storage and supervisord mount",
containers: []types.Container{
containerWithMount(componentName,
[]types.MountPoint{
{
Name: "my-supervisord-mount",
Type: mount.TypeVolume,
},
{
Name: "my-storage-mount",
Type: mount.TypeVolume,
},
}),
},
volumes: []*types.Volume{
{
Name: "my-supervisord-mount",
Labels: map[string]string{
"component": componentName,
"type": "supervisord",
"image": "supervisordimage",
"version": "supervisordversion",
},
},
{
Name: "my-storage-mount",
Labels: map[string]string{
"component": componentName,
"storage-name": "anyval",
},
},
},
expectToDelete: []string{
"my-supervisord-mount",
"my-storage-mount",
},
},
}
for _, tt := range tests {

View File

@@ -25,7 +25,9 @@ import (
)
const (
LocalhostIP = "127.0.0.1"
// LocalhostIP is the IP address for localhost
LocalhostIP = "127.0.0.1"
projectSourceVolumeName = "odo-project-source"
)
@@ -34,39 +36,16 @@ func (a Adapter) createComponent() (err error) {
log.Infof("\nCreating Docker resources for component %s", a.ComponentName)
// Get or create the project source volume
var projectVolumeName string
projectVolumeLabels := utils.GetProjectVolumeLabels(componentName)
projectVols, err := a.Client.GetVolumesByLabel(projectVolumeLabels)
if err != nil {
return errors.Wrapf(err, "Unable to retrieve source volume for component "+componentName)
}
if len(projectVols) == 0 {
// A source volume needs to be created
projectVolumeName, err = storage.GenerateVolName(projectSourceVolumeName, a.ComponentName)
if err != nil {
return errors.Wrapf(err, "Unable to generate project source volume name for component %s", componentName)
}
_, err := a.Client.CreateVolume(projectVolumeName, projectVolumeLabels)
if err != nil {
return errors.Wrapf(err, "Unable to create project source volume for component %s", componentName)
}
} else if len(projectVols) == 1 {
projectVolumeName = projectVols[0].Name
} else if len(projectVols) > 1 {
return errors.Wrapf(err, "Error, multiple source volumes found for component %s", componentName)
}
supportedComponents := common.GetSupportedComponents(a.Devfile.Data)
if len(supportedComponents) == 0 {
return fmt.Errorf("No valid components found in the devfile")
return fmt.Errorf("no valid components found in the devfile")
}
// Get the storage adapter and create the volumes if it does not exist
stoAdapter := storage.New(a.AdapterContext, a.Client)
err = stoAdapter.Create(a.uniqueStorage)
if err != nil {
return errors.Wrapf(err, "Unable to create Docker storage adapter for component %s", componentName)
return errors.Wrapf(err, "unable to create Docker storage adapter for component %s", componentName)
}
// Loop over each component and start a container for it
@@ -81,7 +60,7 @@ func (a Adapter) createComponent() (err error) {
}
dockerVolumeMounts = append(dockerVolumeMounts, volMount)
}
err = a.pullAndStartContainer(dockerVolumeMounts, projectVolumeName, comp)
err = a.pullAndStartContainer(dockerVolumeMounts, comp)
if err != nil {
return errors.Wrapf(err, "unable to pull and start container %s for component %s", comp.Container.Name, componentName)
}
@@ -96,26 +75,13 @@ func (a Adapter) updateComponent() (componentExists bool, err error) {
componentExists = true
componentName := a.ComponentName
// Get the project source volume
volumeLabels := utils.GetProjectVolumeLabels(componentName)
projectVols, err := a.Client.GetVolumesByLabel(volumeLabels)
if err != nil {
return componentExists, errors.Wrapf(err, "Unable to retrieve source volume for component "+componentName)
}
if len(projectVols) == 0 {
return componentExists, fmt.Errorf("Unable to find source volume for component %s", componentName)
} else if len(projectVols) > 1 {
return componentExists, errors.Wrapf(err, "Error, multiple source volumes found for component %s", componentName)
}
projectVolumeName := projectVols[0].Name
// Get the storage adapter and create the volumes if it does not exist
stoAdapter := storage.New(a.AdapterContext, a.Client)
err = stoAdapter.Create(a.uniqueStorage)
supportedComponents := common.GetSupportedComponents(a.Devfile.Data)
if len(supportedComponents) == 0 {
return componentExists, fmt.Errorf("No valid components found in the devfile")
return componentExists, fmt.Errorf("no valid components found in the devfile")
}
for _, comp := range supportedComponents {
@@ -139,7 +105,7 @@ func (a Adapter) updateComponent() (componentExists bool, err error) {
log.Infof("\nCreating Docker resources for component %s", a.ComponentName)
// Container doesn't exist, so need to pull its image (to be safe) and start a new container
err = a.pullAndStartContainer(dockerVolumeMounts, projectVolumeName, comp)
err = a.pullAndStartContainer(dockerVolumeMounts, comp)
if err != nil {
return false, errors.Wrapf(err, "unable to pull and start container %s for component %s", comp.Container.Name, componentName)
}
@@ -174,13 +140,13 @@ func (a Adapter) updateComponent() (componentExists bool, err error) {
// Remove the container
err := a.Client.RemoveContainer(containerID)
if err != nil {
return componentExists, errors.Wrapf(err, "Unable to remove container %s for component %s", containerID, comp.Container.Name)
return componentExists, errors.Wrapf(err, "unable to remove container %s for component %s", containerID, comp.Container.Name)
}
// Start the container
err = a.startComponent(dockerVolumeMounts, projectVolumeName, comp)
err = a.startComponent(dockerVolumeMounts, comp)
if err != nil {
return false, errors.Wrapf(err, "Unable to start container for devfile component %s", comp.Container.Name)
return false, errors.Wrapf(err, "unable to start container for devfile component %s", comp.Container.Name)
}
klog.V(3).Infof("Successfully created container %s for component %s", comp.Container.Image, componentName)
@@ -192,14 +158,14 @@ func (a Adapter) updateComponent() (componentExists bool, err error) {
} else {
// Multiple containers were returned with the specified label (which should be unique)
// Error out, as this isn't expected
return true, fmt.Errorf("Found multiple running containers for devfile component %s and cannot push changes", comp.Container.Name)
return true, fmt.Errorf("found multiple running containers for devfile component %s and cannot push changes", comp.Container.Name)
}
}
return
}
func (a Adapter) pullAndStartContainer(mounts []mount.Mount, projectVolumeName string, comp versionsCommon.DevfileComponent) error {
func (a Adapter) pullAndStartContainer(mounts []mount.Mount, comp versionsCommon.DevfileComponent) error {
// Container doesn't exist, so need to pull its image (to be safe) and start a new container
s := log.Spinnerf("Pulling image %s", comp.Container.Image)
@@ -211,16 +177,16 @@ func (a Adapter) pullAndStartContainer(mounts []mount.Mount, projectVolumeName s
s.End(true)
// Start the component container
err = a.startComponent(mounts, projectVolumeName, comp)
err = a.startComponent(mounts, comp)
if err != nil {
return errors.Wrapf(err, "Unable to start container for devfile component %s", comp.Container.Name)
return errors.Wrapf(err, "unable to start container for devfile component %s", comp.Container.Name)
}
klog.V(3).Infof("Successfully created container %s for component %s", comp.Container.Image, a.ComponentName)
return nil
}
func (a Adapter) startComponent(mounts []mount.Mount, projectVolumeName string, comp versionsCommon.DevfileComponent) error {
func (a Adapter) startComponent(mounts []mount.Mount, comp versionsCommon.DevfileComponent) error {
hostConfig, namePortMapping, err := a.generateAndGetHostConfig(comp.Container.Endpoints)
hostConfig.Mounts = mounts
if err != nil {
@@ -232,11 +198,11 @@ func (a Adapter) startComponent(mounts []mount.Mount, projectVolumeName string,
if err != nil {
return err
}
utils.UpdateComponentWithSupervisord(&comp, runCommand, a.supervisordVolumeName, &hostConfig)
updateComponentWithSupervisord(&comp, runCommand, a.supervisordVolumeName, &hostConfig)
// If the component set `mountSources` to true, add the source volume and env CHE_PROJECTS_ROOT to it
if comp.Container.MountSources {
utils.AddVolumeToContainer(projectVolumeName, lclient.OdoSourceVolumeMount, &hostConfig)
utils.AddVolumeToContainer(a.projectVolumeName, lclient.OdoSourceVolumeMount, &hostConfig)
if !common.IsEnvPresent(comp.Container.Env, common.EnvCheProjectsRoot) {
envName := common.EnvCheProjectsRoot
@@ -257,7 +223,7 @@ func (a Adapter) startComponent(mounts []mount.Mount, projectVolumeName string,
// Create the docker container
s := log.Spinner("Starting container for " + comp.Container.Image)
defer s.End(false)
err = a.Client.StartContainer(&containerConfig, &hostConfig, nil)
_, err = a.Client.StartContainer(&containerConfig, &hostConfig, nil)
if err != nil {
return err
}
@@ -335,7 +301,7 @@ func getPortMap(context string, endpoints []versionsCommon.Endpoint, show bool)
log.Successf("URL %v:%v created", LocalhostIP, url.ExposedPort)
}
} else if url.ExposedPort > 0 && len(endpoints) > 0 && !common.IsPortPresent(endpoints, url.Port) {
return nil, nil, fmt.Errorf("Error creating url: odo url config's port is not present in the devfile. Please re-create odo url with the new devfile port")
return nil, nil, fmt.Errorf("error creating url: odo url config's port is not present in the devfile. Please re-create odo url with the new devfile port")
}
}
@@ -385,7 +351,7 @@ func (a Adapter) execDevfile(commandsMap common.PushCommandsMap, componentExists
// Check if the devfile run component containers have supervisord as the entrypoint.
// Start the supervisord if the odo component does not exist
if !componentExists {
err = a.InitRunContainerSupervisord(command.Exec.Component, containers)
err = a.initRunContainerSupervisord(command.Exec.Component, containers)
if err != nil {
return
}
@@ -404,9 +370,9 @@ func (a Adapter) execDevfile(commandsMap common.PushCommandsMap, componentExists
return
}
// InitRunContainerSupervisord initializes the supervisord in the container if
// initRunContainerSupervisord initializes the supervisord in the container if
// the container has entrypoint that is not supervisord
func (a Adapter) InitRunContainerSupervisord(component string, containers []types.Container) (err error) {
func (a Adapter) initRunContainerSupervisord(component string, containers []types.Container) (err error) {
for _, container := range containers {
if container.Labels["alias"] == component && !strings.Contains(container.Command, common.SupervisordBinaryPath) {
command := []string{common.SupervisordBinaryPath, "-c", common.SupervisordConfFile, "-d"}
@@ -419,3 +385,171 @@ func (a Adapter) InitRunContainerSupervisord(component string, containers []type
return
}
// createProjectVolumeIfReqd creates a project volume if absent and returns the
// name of the created project volume
func (a Adapter) createProjectVolumeIfReqd() (string, error) {
var projectVolumeName string
componentName := a.ComponentName
// Get the project source volume
projectVolumeLabels := utils.GetProjectVolumeLabels(componentName)
projectVols, err := a.Client.GetVolumesByLabel(projectVolumeLabels)
if err != nil {
return "", errors.Wrapf(err, "unable to retrieve source volume for component "+componentName)
}
if len(projectVols) == 0 {
// A source volume needs to be created
projectVolumeName, err = storage.GenerateVolName(projectSourceVolumeName, componentName)
if err != nil {
return "", errors.Wrapf(err, "unable to generate project source volume name for component %s", componentName)
}
_, err := a.Client.CreateVolume(projectVolumeName, projectVolumeLabels)
if err != nil {
return "", errors.Wrapf(err, "unable to create project source volume for component %s", componentName)
}
} else if len(projectVols) == 1 {
projectVolumeName = projectVols[0].Name
} else if len(projectVols) > 1 {
return "", errors.New(fmt.Sprintf("multiple source volumes found for component %s", componentName))
}
return projectVolumeName, nil
}
// createAndInitSupervisordVolumeIfReqd creates the supervisord volume and initializes
// it with supervisord bootstrap image - assembly files and supervisord binary
// returns the name of the supervisord volume and an error if present
func (a Adapter) createAndInitSupervisordVolumeIfReqd(componentExists bool) (string, error) {
var supervisordVolumeName string
componentName := a.ComponentName
supervisordLabels := utils.GetSupervisordVolumeLabels(componentName)
supervisordVolumes, err := a.Client.GetVolumesByLabel(supervisordLabels)
if err != nil {
return "", errors.Wrapf(err, "unable to retrieve supervisord volume for component")
}
if len(supervisordVolumes) == 0 {
supervisordVolumeName, err = storage.GenerateVolName(common.SupervisordVolumeName, componentName)
if err != nil {
return "", errors.Wrapf(err, "unable to generate volume name for supervisord")
}
_, err := a.Client.CreateVolume(supervisordVolumeName, supervisordLabels)
if err != nil {
return "", errors.Wrapf(err, "unable to create supervisord volume for component")
}
} else {
supervisordVolumeName = supervisordVolumes[0].Name
}
if !componentExists {
log.Info("\nInitialization")
s := log.Spinner("Initializing the component")
defer s.End(false)
err = a.startBootstrapSupervisordInitContainer(supervisordVolumeName)
if err != nil {
return "", errors.Wrapf(err, "unable to start supervisord container for component")
}
s.End(true)
}
return supervisordVolumeName, nil
}
// startBootstrapSupervisordInitContainer pulls the supervisord bootstrap image, mounts the supervisord
// volume, starts the bootstrap container and initializes the supervisord volume via its entrypoint
func (a Adapter) startBootstrapSupervisordInitContainer(supervisordVolumeName string) error {
componentName := a.ComponentName
supervisordLabels := utils.GetSupervisordVolumeLabels(componentName)
image := common.GetBootstrapperImage()
command := []string{"/usr/bin/cp"}
args := []string{
"-r",
common.OdoInitImageContents,
common.SupervisordMountPath,
}
var s *log.Status
if log.IsDebug() {
s = log.Spinnerf("Pulling image %s", image)
defer s.End(false)
}
err := a.Client.PullImage(image)
if err != nil {
return errors.Wrapf(err, "unable to pull %s image", image)
}
if log.IsDebug() {
s.End(true)
}
containerConfig := a.Client.GenerateContainerConfig(image, command, args, nil, supervisordLabels, nil)
hostConfig := container.HostConfig{}
utils.AddVolumeToContainer(supervisordVolumeName, common.SupervisordMountPath, &hostConfig)
// Create the docker container
if log.IsDebug() {
s = log.Spinnerf("Starting container for %s", image)
defer s.End(false)
}
containerID, err := a.Client.StartContainer(&containerConfig, &hostConfig, nil)
if err != nil {
return err
}
if log.IsDebug() {
s.End(true)
}
// Wait for the container to exit before removing it
err = a.Client.WaitForContainer(containerID, container.WaitConditionNotRunning)
if err != nil {
return errors.Wrapf(err, "supervisord init container %s failed to complete", containerID)
}
err = a.Client.RemoveContainer(containerID)
if err != nil {
return errors.Wrapf(err, "unable to remove supervisord init container %s", containerID)
}
return nil
}
// UpdateComponentWithSupervisord updates the devfile component's
// 1. command and args with supervisord, if absent
// 2. env with ODO_COMMAND_RUN and ODO_COMMAND_RUN_WORKING_DIR, if absent
func updateComponentWithSupervisord(comp *versionsCommon.DevfileComponent, runCommand versionsCommon.DevfileCommand, supervisordVolumeName string, hostConfig *container.HostConfig) {
// Mount the supervisord volume for the run command container
if runCommand.Exec.Component == comp.Container.Name {
utils.AddVolumeToContainer(supervisordVolumeName, common.SupervisordMountPath, hostConfig)
if len(comp.Container.Command) == 0 && len(comp.Container.Args) == 0 {
klog.V(4).Infof("Updating container %v entrypoint with supervisord", comp.Container.Name)
comp.Container.Command = append(comp.Container.Command, common.SupervisordBinaryPath)
comp.Container.Args = append(comp.Container.Args, "-c", common.SupervisordConfFile)
}
if !common.IsEnvPresent(comp.Container.Env, common.EnvOdoCommandRun) {
envName := common.EnvOdoCommandRun
envValue := runCommand.Exec.CommandLine
comp.Container.Env = append(comp.Container.Env, versionsCommon.Env{
Name: envName,
Value: envValue,
})
}
if !common.IsEnvPresent(comp.Container.Env, common.EnvOdoCommandRunWorkingDir) && runCommand.Exec.WorkingDir != "" {
envName := common.EnvOdoCommandRunWorkingDir
envValue := runCommand.Exec.WorkingDir
comp.Container.Env = append(comp.Container.Env, versionsCommon.Env{
Name: envName,
Value: envValue,
})
}
}
}

View File

@@ -1,11 +1,14 @@
package component
import (
"reflect"
"strings"
"testing"
"github.com/docker/go-connections/nat"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
devfileParser "github.com/openshift/odo/pkg/devfile/parser"
@@ -106,17 +109,18 @@ func TestUpdateComponent(t *testing.T) {
wantErr: true,
},
{
name: "Case 3: Valid devfile, missing component",
name: "Case 4: Valid devfile, missing component",
components: []versionsCommon.DevfileComponent{
{
Container: &versionsCommon.Container{
Name: "fakecomponent",
Name: "alias1",
Image: "someimage",
},
},
},
componentName: "fakecomponent",
client: fakeClient,
wantErr: true,
wantErr: false,
},
}
for _, tt := range tests {
@@ -220,7 +224,8 @@ func TestPullAndStartContainer(t *testing.T) {
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.pullAndStartContainer(tt.mounts, testVolumeName, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
componentAdapter.projectVolumeName = testVolumeName
err := componentAdapter.pullAndStartContainer(tt.mounts, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
@@ -301,7 +306,8 @@ func TestStartContainer(t *testing.T) {
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.startComponent(tt.mounts, testVolumeName, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
componentAdapter.projectVolumeName = testVolumeName
err := componentAdapter.startComponent(tt.mounts, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
@@ -667,10 +673,527 @@ func TestInitRunContainerSupervisord(t *testing.T) {
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.InitRunContainerSupervisord(tt.component, containers)
err := componentAdapter.initRunContainerSupervisord(tt.component, containers)
if !tt.wantErr && err != nil {
t.Errorf("TestInitRunContainerSupervisord error: unexpected error during init supervisord: %v", err)
}
})
}
}
func TestCreateProjectVolumeIfReqd(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentName string
client *lclient.Client
wantVolumeName string
wantErr bool
}{
{
name: "Case 1: Volume does not exist",
componentName: "somecomponent",
client: fakeClient,
wantVolumeName: projectSourceVolumeName + "-somecomponent",
wantErr: false,
},
{
name: "Case 2: Volume exist",
componentName: "test",
client: fakeClient,
wantVolumeName: projectSourceVolumeName + "-test",
wantErr: false,
},
{
name: "Case 3: More than one project volume exist",
componentName: "duplicate",
client: fakeClient,
wantVolumeName: "",
wantErr: true,
},
{
name: "Case 4: Client error",
componentName: "random",
client: fakeErrorClient,
wantVolumeName: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
Components: []versionsCommon.DevfileComponent{
{
Type: versionsCommon.ContainerComponentType,
},
},
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: tt.componentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
volumeName, err := componentAdapter.createProjectVolumeIfReqd()
if !tt.wantErr && err != nil {
t.Errorf("TestCreateAndGetProjectVolume error: Unexpected error: %v", err)
} else if !tt.wantErr && !strings.Contains(volumeName, tt.wantVolumeName) {
t.Errorf("TestCreateAndGetProjectVolume error: project volume name did not match, expected: %v got: %v", tt.wantVolumeName, volumeName)
}
})
}
}
func TestStartBootstrapSupervisordInitContainer(t *testing.T) {
supervisordVolumeName := "supervisord"
componentName := "myComponent"
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully create a bootstrap container",
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Failed to create a bootstrap container ",
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
Components: []versionsCommon.DevfileComponent{
{
Type: versionsCommon.ContainerComponentType,
},
},
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: componentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.startBootstrapSupervisordInitContainer(supervisordVolumeName)
if !tt.wantErr && err != nil {
t.Errorf("TestStartBootstrapSupervisordInitContainer: unexpected error got: %v wanted: %v", err, tt.wantErr)
}
})
}
}
func TestCreateAndInitSupervisordVolumeIfReqd(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
componentName := "myComponent"
tests := []struct {
name string
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully create a bootstrap vol and container",
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Failed to create a bootstrap vol and container ",
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
Components: []versionsCommon.DevfileComponent{
{
Type: versionsCommon.ContainerComponentType,
},
},
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: componentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
volName, err := componentAdapter.createAndInitSupervisordVolumeIfReqd(false)
if !tt.wantErr && err != nil {
t.Errorf("TestCreateAndInitSupervisordVolume: unexpected error %v, wanted %v", err, tt.wantErr)
} else if !tt.wantErr && !strings.Contains(volName, adaptersCommon.SupervisordVolumeName+"-"+componentName) {
t.Errorf("TestCreateAndInitSupervisordVolume: unexpected supervisord vol name, expected: %v got: %v", adaptersCommon.SupervisordVolumeName, volName)
}
})
}
}
func TestUpdateComponentWithSupervisord(t *testing.T) {
command := "ls -la"
component := "alias1"
workDir := "/"
emptyString := ""
garbageString := "garbageString"
supervisordVolumeName := "supervisordVolumeName"
defaultWorkDirEnv := adaptersCommon.EnvOdoCommandRunWorkingDir
defaultCommandEnv := adaptersCommon.EnvOdoCommandRun
tests := []struct {
name string
commandExecs []versionsCommon.Exec
commandName string
comp versionsCommon.DevfileComponent
supervisordVolumeName string
hostConfig container.HostConfig
wantHostConfig container.HostConfig
wantCommand []string
wantArgs []string
wantEnv []versionsCommon.Env
}{
{
name: "Case 1: No component commands, args, env",
commandExecs: []versionsCommon.Exec{
{
CommandLine: command,
Component: component,
Group: &versionsCommon.Group{
Kind: versionsCommon.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: versionsCommon.DevfileComponent{
Container: &versionsCommon.Container{
Command: []string{},
Args: []string{},
Env: []versionsCommon.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{adaptersCommon.SupervisordBinaryPath},
wantArgs: []string{"-c", adaptersCommon.SupervisordConfFile},
wantEnv: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 2: Existing component command and no args, env",
commandExecs: []versionsCommon.Exec{
{
CommandLine: command,
Component: component,
Group: &versionsCommon.Group{
Kind: versionsCommon.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: versionsCommon.DevfileComponent{
Container: &versionsCommon.Container{
Command: []string{"some", "command"},
Args: []string{},
Env: []versionsCommon.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{},
wantEnv: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 3: Existing component command and args and no env",
commandExecs: []versionsCommon.Exec{
{
CommandLine: command,
Component: component,
Group: &versionsCommon.Group{
Kind: versionsCommon.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: versionsCommon.DevfileComponent{
Container: &versionsCommon.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []versionsCommon.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 4: Existing component command, args and env",
commandExecs: []versionsCommon.Exec{
{
CommandLine: command,
Component: component,
Group: &versionsCommon.Group{
Kind: versionsCommon.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: versionsCommon.DevfileComponent{
Container: &versionsCommon.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
},
{
name: "Case 5: Existing host config, should append to it",
commandExecs: []versionsCommon.Exec{
{
CommandLine: command,
Component: component,
Group: &versionsCommon.Group{
Kind: versionsCommon.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: versionsCommon.DevfileComponent{
Container: &versionsCommon.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: garbageString,
Target: garbageString,
},
},
},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
{
Type: mount.TypeVolume,
Source: garbageString,
Target: garbageString,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []versionsCommon.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ExecCommands: tt.commandExecs,
Components: []versionsCommon.DevfileComponent{
{
Container: &versionsCommon.Container{
Name: tt.comp.Container.Name,
},
},
},
},
}
runCommand, err := adaptersCommon.GetRunCommand(devObj.Data, tt.commandName)
if err != nil {
t.Errorf("TestUpdateComponentWithSupervisord: error getting the run command: %v", err)
}
updateComponentWithSupervisord(&tt.comp, runCommand, tt.supervisordVolumeName, &tt.hostConfig)
// Check the container host config
for _, containerHostConfigMount := range tt.hostConfig.Mounts {
matched := false
for _, wantHostConfigMount := range tt.wantHostConfig.Mounts {
if reflect.DeepEqual(wantHostConfigMount, containerHostConfigMount) {
matched = true
}
}
if !matched {
t.Errorf("TestUpdateComponentWithSupervisord: host configs source: %v target:%v do not match wanted host config", containerHostConfigMount.Source, containerHostConfigMount.Target)
}
}
// Check the component command
if !reflect.DeepEqual(tt.comp.Container.Command, tt.wantCommand) {
t.Errorf("TestUpdateComponentWithSupervisord: component commands dont match actual: %v wanted: %v", tt.comp.Container.Command, tt.wantCommand)
}
// Check the component args
if !reflect.DeepEqual(tt.comp.Container.Args, tt.wantArgs) {
t.Errorf("TestUpdateComponentWithSupervisord: component args dont match actual: %v wanted: %v", tt.comp.Container.Args, tt.wantArgs)
}
// Check the component env
for _, compEnv := range tt.comp.Container.Env {
matched := false
for _, wantEnv := range tt.wantEnv {
if reflect.DeepEqual(wantEnv, compEnv) {
matched = true
}
}
if !matched {
t.Errorf("TestUpdateComponentWithSupervisord: component env dont match env: %v:%v not present in wanted list", compEnv.Name, compEnv.Value)
}
}
})
}
}

View File

@@ -5,37 +5,50 @@ import (
"strconv"
"github.com/docker/go-connections/nat"
"k8s.io/klog"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/docker/storage"
"github.com/openshift/odo/pkg/devfile/parser/data"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
)
const (
supervisordVolume = "supervisord"
projectsVolume = "projects"
volume = "vol"
// SupervisordVolume is supervisord volume type
SupervisordVolume = "supervisord"
// ProjectsVolume is project source volume type
ProjectsVolume = "projects"
)
// ComponentExists checks if Docker containers labeled with the specified component name exists
func ComponentExists(client lclient.Client, name string) bool {
// ComponentExists checks if a component exist
// returns true, if the number of containers equals the number of unique devfile components
// returns false, if number of containers is zero
// returns an error, if number of containers is more than zero but does not equal the number of unique devfile components
func ComponentExists(client lclient.Client, data data.DevfileData, name string) (bool, error) {
containers, err := GetComponentContainers(client, name)
if err != nil {
// log the error since this function basically returns a bool
log.Error(err)
return false
return false, errors.Wrapf(err, "unable to get the containers for component %s", name)
}
return len(containers) != 0
supportedComponents := adaptersCommon.GetSupportedComponents(data)
var componentExists bool
if len(containers) == 0 {
componentExists = false
} else if len(containers) == len(supportedComponents) {
componentExists = true
} else if len(containers) > 0 && len(containers) != len(supportedComponents) {
return false, errors.New(fmt.Sprintf("component %s is in an invalid state, please execute odo delete and retry odo push", name))
}
return componentExists, nil
}
// GetComponentContainers returns a list of the running component containers
@@ -147,7 +160,7 @@ func AddVolumeToContainer(volumeName, volumeMount string, hostConfig *container.
func GetProjectVolumeLabels(componentName string) map[string]string {
volumeLabels := map[string]string{
"component": componentName,
"type": projectsVolume,
"type": ProjectsVolume,
}
return volumeLabels
}
@@ -162,14 +175,15 @@ func GetContainerLabels(componentName, alias string) map[string]string {
}
// GetSupervisordVolumeLabels returns the label selectors used to retrieve the supervisord volume
func GetSupervisordVolumeLabels() map[string]string {
func GetSupervisordVolumeLabels(componentName string) map[string]string {
image := adaptersCommon.GetBootstrapperImage()
_, _, _, imageTag := util.ParseComponentImageName(image)
_, imageWithoutTag, _, imageTag := util.ParseComponentImageName(image)
supervisordLabels := map[string]string{
"name": adaptersCommon.SupervisordVolumeName,
"type": supervisordVolume,
"version": imageTag,
"component": componentName,
"type": SupervisordVolume,
"image": imageWithoutTag,
"version": imageTag,
}
return supervisordLabels
}
@@ -202,113 +216,3 @@ func containerHasPort(devfilePort nat.Port, exposedPorts nat.PortSet) bool {
}
return false
}
// UpdateComponentWithSupervisord updates the devfile component's
// 1. command and args with supervisord, if absent
// 2. env with ODO_COMMAND_RUN and ODO_COMMAND_RUN_WORKING_DIR, if absent
func UpdateComponentWithSupervisord(comp *common.DevfileComponent, runCommand common.DevfileCommand, supervisordVolumeName string, hostConfig *container.HostConfig) {
// Mount the supervisord volume for the run command container
if runCommand.Exec.Component == comp.Container.Name {
AddVolumeToContainer(supervisordVolumeName, adaptersCommon.SupervisordMountPath, hostConfig)
if len(comp.Container.Command) == 0 && len(comp.Container.Args) == 0 {
klog.V(4).Infof("Updating container %v entrypoint with supervisord", comp.Container.Name)
comp.Container.Command = append(comp.Container.Command, adaptersCommon.SupervisordBinaryPath)
comp.Container.Args = append(comp.Container.Args, "-c", adaptersCommon.SupervisordConfFile)
}
if !adaptersCommon.IsEnvPresent(comp.Container.Env, adaptersCommon.EnvOdoCommandRun) {
envName := adaptersCommon.EnvOdoCommandRun
envValue := runCommand.Exec.CommandLine
comp.Container.Env = append(comp.Container.Env, common.Env{
Name: envName,
Value: envValue,
})
}
if !adaptersCommon.IsEnvPresent(comp.Container.Env, adaptersCommon.EnvOdoCommandRunWorkingDir) && runCommand.Exec.WorkingDir != "" {
envName := adaptersCommon.EnvOdoCommandRunWorkingDir
envValue := runCommand.Exec.WorkingDir
comp.Container.Env = append(comp.Container.Env, common.Env{
Name: envName,
Value: envValue,
})
}
}
}
// CreateAndInitSupervisordVolume creates the supervisord volume and initializes
// it with supervisord bootstrap image - assembly files and supervisord binary
func CreateAndInitSupervisordVolume(client lclient.Client) (string, error) {
log.Info("\nInitialization")
s := log.Spinner("Initializing the component")
defer s.End(false)
supervisordVolumeName, err := storage.GenerateVolName(adaptersCommon.SupervisordVolumeName, volume)
if err != nil {
return "", errors.Wrapf(err, "unable to generate volume name for supervisord")
}
supervisordLabels := GetSupervisordVolumeLabels()
_, err = client.CreateVolume(supervisordVolumeName, supervisordLabels)
if err != nil {
return "", errors.Wrapf(err, "Unable to create supervisord volume for component")
}
err = StartBootstrapSupervisordInitContainer(client, supervisordVolumeName)
if err != nil {
return "", errors.Wrapf(err, "Unable to start supervisord container for component")
}
s.End(true)
return supervisordVolumeName, nil
}
// StartBootstrapSupervisordInitContainer pulls the supervisord bootstrap image, mounts the supervisord
// volume, starts the bootstrap container and initializes the supervisord volume via its entrypoint
func StartBootstrapSupervisordInitContainer(client lclient.Client, supervisordVolumeName string) error {
supervisordLabels := GetSupervisordVolumeLabels()
image := adaptersCommon.GetBootstrapperImage()
command := []string{"/usr/bin/cp"}
args := []string{
"-r",
adaptersCommon.OdoInitImageContents,
adaptersCommon.SupervisordMountPath,
}
var s *log.Status
if log.IsDebug() {
s = log.Spinnerf("Pulling image %s", image)
defer s.End(false)
}
err := client.PullImage(image)
if err != nil {
return errors.Wrapf(err, "unable to pull %s image", image)
}
if log.IsDebug() {
s.End(true)
}
containerConfig := client.GenerateContainerConfig(image, command, args, nil, supervisordLabels, nil)
hostConfig := container.HostConfig{}
AddVolumeToContainer(supervisordVolumeName, adaptersCommon.SupervisordMountPath, &hostConfig)
// Create the docker container
if log.IsDebug() {
s = log.Spinnerf("Starting container for %s", image)
defer s.End(false)
}
err = client.StartContainer(&containerConfig, &hostConfig, nil)
if err != nil {
return err
}
if log.IsDebug() {
s.End(true)
}
return nil
}

View File

@@ -26,32 +26,74 @@ func TestComponentExists(t *testing.T) {
name string
componentName string
client *lclient.Client
components []common.DevfileComponent
want bool
wantErr bool
}{
{
name: "Case 1: Component exists",
componentName: "golang",
client: fakeClient,
want: true,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
testingutil.GetFakeComponent("alias2"),
},
want: true,
wantErr: false,
},
{
name: "Case 2: Component doesn't exist",
componentName: "fakecomponent",
client: fakeClient,
want: false,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
},
want: false,
wantErr: false,
},
{
name: "Case 3: Error with docker client",
componentName: "golang",
client: fakeErrorClient,
components: []common.DevfileComponent{
testingutil.GetFakeComponent("alias1"),
},
want: false,
wantErr: true,
},
{
name: "Case 4: Container and devfile component mismatch",
componentName: "test",
client: fakeClient,
components: []common.DevfileComponent{},
want: false,
wantErr: true,
},
{
name: "Case 5: Devfile does not have supported components",
componentName: "golang",
client: fakeClient,
components: []common.DevfileComponent{
{
CheEditor: &common.CheEditor{},
},
},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmpExists := ComponentExists(*tt.client, tt.componentName)
if tt.want != cmpExists {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
Components: tt.components,
},
}
cmpExists, err := ComponentExists(*tt.client, devObj.Data, tt.componentName)
if !tt.wantErr && err != nil {
t.Errorf("TestComponentExists error, unexpected error - %v", err)
} else if !tt.wantErr && tt.want != cmpExists {
t.Errorf("expected %v, wanted %v", cmpExists, tt.want)
}
})
@@ -566,7 +608,7 @@ func TestGetProjectVolumeLabels(t *testing.T) {
componentName: "some-component",
want: map[string]string{
"component": "some-component",
"type": "projects",
"type": ProjectsVolume,
},
},
{
@@ -574,7 +616,7 @@ func TestGetProjectVolumeLabels(t *testing.T) {
componentName: "",
want: map[string]string{
"component": "",
"type": "projects",
"type": ProjectsVolume,
},
},
}
@@ -628,25 +670,30 @@ func TestGetContainerLabels(t *testing.T) {
func TestGetSupervisordVolumeLabels(t *testing.T) {
componentNameArr := []string{"myComponent1", "myComponent2"}
tests := []struct {
name string
customImage bool
want map[string]string
name string
componentName string
customImage bool
want map[string]string
}{
{
name: "Case 1: Default supervisord image",
customImage: false,
name: "Case 1: Default supervisord image",
componentName: componentNameArr[0],
customImage: false,
want: map[string]string{
"name": adaptersCommon.SupervisordVolumeName,
"type": supervisordVolume,
"component": componentNameArr[0],
"type": SupervisordVolume,
},
},
{
name: "Case 2: Custom supervisord image",
customImage: true,
name: "Case 2: Custom supervisord image",
componentName: componentNameArr[1],
customImage: true,
want: map[string]string{
"name": adaptersCommon.SupervisordVolumeName,
"type": supervisordVolume,
"component": componentNameArr[1],
"type": SupervisordVolume,
},
},
}
@@ -656,11 +703,12 @@ func TestGetSupervisordVolumeLabels(t *testing.T) {
os.Setenv("ODO_BOOTSTRAPPER_IMAGE", "customimage:customtag")
}
image := adaptersCommon.GetBootstrapperImage()
_, _, _, imageTag := util.ParseComponentImageName(image)
_, imageWithoutTag, _, imageTag := util.ParseComponentImageName(image)
tt.want["version"] = imageTag
tt.want["image"] = imageWithoutTag
labels := GetSupervisordVolumeLabels()
labels := GetSupervisordVolumeLabels(tt.componentName)
if !reflect.DeepEqual(tt.want, labels) {
t.Errorf("expected %v, actual %v", tt.want, labels)
}
@@ -669,419 +717,6 @@ func TestGetSupervisordVolumeLabels(t *testing.T) {
}
func TestUpdateComponentWithSupervisord(t *testing.T) {
command := "ls -la"
component := "alias1"
workDir := "/"
emptyString := ""
garbageString := "garbageString"
supervisordVolumeName := "supervisordVolumeName"
defaultWorkDirEnv := adaptersCommon.EnvOdoCommandRunWorkingDir
defaultCommandEnv := adaptersCommon.EnvOdoCommandRun
tests := []struct {
name string
commandExecs []common.Exec
commandName string
comp common.DevfileComponent
supervisordVolumeName string
hostConfig container.HostConfig
wantHostConfig container.HostConfig
wantCommand []string
wantArgs []string
wantEnv []common.Env
}{
{
name: "Case 1: No component commands, args, env",
commandExecs: []common.Exec{
{
CommandLine: command,
Component: component,
Group: &common.Group{
Kind: common.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: common.DevfileComponent{
Container: &common.Container{
Command: []string{},
Args: []string{},
Env: []common.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{adaptersCommon.SupervisordBinaryPath},
wantArgs: []string{"-c", adaptersCommon.SupervisordConfFile},
wantEnv: []common.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 2: Existing component command and no args, env",
commandExecs: []common.Exec{
{
CommandLine: command,
Component: component,
Group: &common.Group{
Kind: common.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: common.DevfileComponent{
Container: &common.Container{
Command: []string{"some", "command"},
Args: []string{},
Env: []common.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{},
wantEnv: []common.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 3: Existing component command and args and no env",
commandExecs: []common.Exec{
{
CommandLine: command,
Component: component,
Group: &common.Group{
Kind: common.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: common.DevfileComponent{
Container: &common.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []common.Env{},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []common.Env{
{
Name: defaultWorkDirEnv,
Value: workDir,
},
{
Name: defaultCommandEnv,
Value: command,
},
},
},
{
name: "Case 4: Existing component command, args and env",
commandExecs: []common.Exec{
{
CommandLine: command,
Component: component,
Group: &common.Group{
Kind: common.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: common.DevfileComponent{
Container: &common.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []common.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []common.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
},
{
name: "Case 5: Existing host config, should append to it",
commandExecs: []common.Exec{
{
CommandLine: command,
Component: component,
Group: &common.Group{
Kind: common.RunCommandGroupType,
},
WorkingDir: workDir,
},
},
commandName: emptyString,
comp: common.DevfileComponent{
Container: &common.Container{
Command: []string{"some", "command"},
Args: []string{"some", "args"},
Env: []common.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
Name: component,
},
},
supervisordVolumeName: supervisordVolumeName,
hostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: garbageString,
Target: garbageString,
},
},
},
wantHostConfig: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: supervisordVolumeName,
Target: adaptersCommon.SupervisordMountPath,
},
{
Type: mount.TypeVolume,
Source: garbageString,
Target: garbageString,
},
},
},
wantCommand: []string{"some", "command"},
wantArgs: []string{"some", "args"},
wantEnv: []common.Env{
{
Name: defaultWorkDirEnv,
Value: garbageString,
},
{
Name: defaultCommandEnv,
Value: garbageString,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ExecCommands: tt.commandExecs,
Components: []common.DevfileComponent{
{
Container: &common.Container{
Name: tt.comp.Container.Name,
},
},
},
},
}
runCommand, err := adaptersCommon.GetRunCommand(devObj.Data, tt.commandName)
if err != nil {
t.Errorf("TestUpdateComponentWithSupervisord: error getting the run command")
}
UpdateComponentWithSupervisord(&tt.comp, runCommand, tt.supervisordVolumeName, &tt.hostConfig)
// Check the container host config
for _, containerHostConfigMount := range tt.hostConfig.Mounts {
matched := false
for _, wantHostConfigMount := range tt.wantHostConfig.Mounts {
if reflect.DeepEqual(wantHostConfigMount, containerHostConfigMount) {
matched = true
}
}
if !matched {
t.Errorf("TestUpdateComponentWithSupervisord: host configs source: %v target:%v do not match wanted host config", containerHostConfigMount.Source, containerHostConfigMount.Target)
}
}
// Check the component command
if !reflect.DeepEqual(tt.comp.Container.Command, tt.wantCommand) {
t.Errorf("TestUpdateComponentWithSupervisord: component commands dont match actual: %v wanted: %v", tt.comp.Container.Command, tt.wantCommand)
}
// Check the component args
if !reflect.DeepEqual(tt.comp.Container.Args, tt.wantArgs) {
t.Errorf("TestUpdateComponentWithSupervisord: component args dont match actual: %v wanted: %v", tt.comp.Container.Args, tt.wantArgs)
}
// Check the component env
for _, compEnv := range tt.comp.Container.Env {
matched := false
for _, wantEnv := range tt.wantEnv {
if reflect.DeepEqual(wantEnv, compEnv) {
matched = true
}
}
if !matched {
t.Errorf("TestUpdateComponentWithSupervisord: component env dont match env: %v:%v not present in wanted list", compEnv.Name, compEnv.Value)
}
}
})
}
}
func TestStartBootstrapSupervisordInitContainer(t *testing.T) {
supervisordVolumeName := supervisordVolume
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully create a bootstrap container",
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Failed to create a bootstrap container ",
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := StartBootstrapSupervisordInitContainer(*tt.client, supervisordVolumeName)
if !tt.wantErr && err != nil {
t.Errorf("TestStartBootstrapSupervisordInitContainer: unexpected error got: %v wanted: %v", err, tt.wantErr)
}
})
}
}
func TestCreateAndInitSupervisordVolume(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully create a bootstrap vol and container",
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Failed to create a bootstrap vol and container ",
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
volName, err := CreateAndInitSupervisordVolume(*tt.client)
if !tt.wantErr && err != nil {
t.Logf("TestCreateAndInitSupervisordVolume: unexpected error %v, wanted %v", err, tt.wantErr)
} else if !tt.wantErr && volName != adaptersCommon.SupervisordVolumeName {
t.Logf("TestCreateAndInitSupervisordVolume: unexpected supervisord vol name, expected: %v got: %v", adaptersCommon.SupervisordVolumeName, volName)
}
})
}
}
func TestGetContainerIDForAlias(t *testing.T) {
containers := []types.Container{

View File

@@ -3,6 +3,7 @@ package lclient
import (
"io"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@@ -53,19 +54,19 @@ func (dc *Client) GetContainerList() ([]types.Container, error) {
// containerConfig - configurations for the container itself (image name, command, ports, etc) (if needed)
// hostConfig - configurations related to the host (volume mounts, exposed ports, etc) (if needed)
// networkingConfig - endpoints to expose (if needed)
// Returns an error if the container couldn't be started.
func (dc *Client) StartContainer(containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) error {
// Returns containerID of the started container, an error if the container couldn't be started
func (dc *Client) StartContainer(containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) (string, error) {
resp, err := dc.Client.ContainerCreate(dc.Context, containerConfig, hostConfig, networkingConfig, "")
if err != nil {
return err
return "", err
}
// Start the container
if err := dc.Client.ContainerStart(dc.Context, resp.ID, types.ContainerStartOptions{}); err != nil {
return err
return "", err
}
return nil
return resp.ID, nil
}
// RemoveContainer takes in a given container ID and kills it, then removes it.
@@ -150,3 +151,22 @@ func (dc *Client) ExtractProjectToComponent(compInfo common.ComponentInfo, targe
}
return nil
}
// WaitForContainer waits for the container until the condition is reached
func (dc *Client) WaitForContainer(containerID string, condition container.WaitCondition) error {
containerWaitCh, errCh := dc.Client.ContainerWait(dc.Context, containerID, condition)
for {
select {
case containerWait := <-containerWaitCh:
if containerWait.StatusCode != 0 {
return errors.Errorf("error waiting on container %s until condition %s; status code: %v, error message: %v", containerID, string(condition), containerWait.StatusCode, containerWait.Error.Message)
}
return nil
case err := <-errCh:
return errors.Wrapf(err, "unable to wait on container %s until condition %s", containerID, string(condition))
case <-time.After(2 * time.Minute):
return errors.Errorf("timeout while waiting for container %s to reach condition %s", containerID, string(condition))
}
}
}

View File

@@ -200,6 +200,7 @@ func TestGetContainersList(t *testing.T) {
Image: "golang",
Labels: map[string]string{
"component": "golang",
"alias": "alias1",
"8080": "testurl3",
},
HostConfig: container.HostConfig{
@@ -212,6 +213,11 @@ func TestGetContainersList(t *testing.T) {
},
},
},
Mounts: []types.MountPoint{
{
Destination: OdoSourceVolumeMount,
},
},
},
},
wantErr: false,
@@ -244,26 +250,31 @@ func TestStartContainer(t *testing.T) {
fakeContainer := container.Config{}
tests := []struct {
name string
client *Client
wantErr bool
name string
client *Client
wantContainerID string
wantErr bool
}{
{
name: "Case 1: Successfully start container",
client: fakeClient,
wantErr: false,
name: "Case 1: Successfully start container",
client: fakeClient,
wantContainerID: "golang",
wantErr: false,
},
{
name: "Case 2: Fail to start",
client: fakeErrorClient,
wantErr: true,
name: "Case 2: Fail to start",
client: fakeErrorClient,
wantContainerID: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.client.StartContainer(&fakeContainer, nil, nil)
containerID, err := tt.client.StartContainer(&fakeContainer, nil, nil)
if !tt.wantErr == (err != nil) {
t.Errorf("expected %v, wanted %v", err, tt.wantErr)
t.Errorf("TestStartContainer error: expected %v, wanted %v", err, tt.wantErr)
} else if !tt.wantErr && containerID != tt.wantContainerID {
t.Errorf("TestStartContainer error: container id of start container did not match: got %v, wanted %v", containerID, tt.wantContainerID)
}
})
}
@@ -409,3 +420,42 @@ func TestExecCMDInContainer(t *testing.T) {
})
}
}
func TestWaitForContainer(t *testing.T) {
fakeClient := FakeNew()
fakeErrorClient := FakeErrorNew()
tests := []struct {
name string
client *Client
condition container.WaitCondition
wantErr bool
}{
{
name: "Case 1: Successfully wait for a condition",
client: fakeClient,
condition: container.WaitConditionNotRunning,
wantErr: false,
},
{
name: "Case 2: Failed to wait for a condition with error channel",
client: fakeErrorClient,
condition: container.WaitConditionNotRunning,
wantErr: true,
},
{
name: "Case 3: Failed to wait for a condition with bad exit code",
client: fakeErrorClient,
condition: container.WaitConditionNextExit,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.client.WaitForContainer("id", tt.condition)
if !tt.wantErr == (err != nil) {
t.Errorf("got: %v, wanted: %v", err, tt.wantErr)
}
})
}
}

View File

@@ -80,6 +80,12 @@ var mockContainerList = []types.Container{
Labels: map[string]string{
"component": "golang",
"8080": "testurl3",
"alias": "alias1",
},
Mounts: []types.MountPoint{
{
Destination: OdoSourceVolumeMount,
},
},
HostConfig: container.HostConfig{
PortBindings: nat.PortMap{
@@ -157,6 +163,15 @@ func (m *mockDockerClient) ContainerInspect(ctx context.Context, containerID str
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
resultC := make(chan container.ContainerWaitOKBody)
go func() {
res := container.ContainerWaitOKBody{
StatusCode: 0,
Error: &container.ContainerWaitOKBodyError{
Message: "",
},
}
resultC <- res
}()
return resultC, nil
}
@@ -195,6 +210,21 @@ func (m *mockDockerClient) VolumeList(ctx context.Context, filter filters.Args)
"component": "test",
"type": "projects",
},
Name: "odo-project-source-test",
},
{
Labels: map[string]string{
"component": "duplicate",
"type": "projects",
},
Name: "odo-project-source-duplicate1",
},
{
Labels: map[string]string{
"component": "duplicate",
"type": "projects",
},
Name: "odo-project-source-duplicate2",
},
},
}, nil
@@ -246,7 +276,7 @@ var errContainerStart = errors.New("error starting containers")
var errContainerStop = errors.New("error stopping container")
var errContainerRemove = errors.New("error removing container")
var errContainerInspect = errors.New("error inspecting container")
var errContainerWait = errors.New("error timeout waiting for container")
var errContainerWait = errors.New("error waiting for container")
var errDistributionInspect = errors.New("error inspecting distribution")
var errVolumeCreate = errors.New("error creating volume")
var errVolumeList = errors.New("error listing volume")
@@ -289,8 +319,23 @@ func (m *mockDockerErrorClient) ContainerInspect(ctx context.Context, containerI
}
func (m *mockDockerErrorClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
resultC := make(chan container.ContainerWaitOKBody)
err := make(chan error)
err <- errContainerWait
go func() {
if condition == container.WaitConditionNextExit {
res := container.ContainerWaitOKBody{
StatusCode: 1,
Error: &container.ContainerWaitOKBodyError{
Message: "bad status code",
},
}
resultC <- res
}
err <- errContainerWait
}()
if condition == container.WaitConditionNextExit {
return resultC, nil
}
return nil, err
}

View File

@@ -39,6 +39,8 @@ var _ = Describe("odo docker devfile push command tests", func() {
label := "component=" + cmpName
dockerClient.StopContainers(label)
dockerClient.RemoveVolumesByComponent(cmpName)
helper.Chdir(currentWorkingDirectory)
helper.DeleteDir(context)
os.Unsetenv("GLOBALODOCONFIG")
@@ -83,7 +85,7 @@ var _ = Describe("odo docker devfile push command tests", func() {
// Verify the volumes got created successfully (and 3 volumes exist: one source and two defined in devfile)
label := "component=" + cmpName
volumes := dockerClient.GetVolumesByLabel(label)
Expect(len(volumes)).To(Equal(3))
Expect(len(volumes)).To(Equal(4))
})
It("Check that odo push mounts the docker volumes in the container", func() {