Implement a basic, first pass odo push with Docker support (#2806)

* Implement docker adapter

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Docker component support

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Implement odo push for Docker

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Create helper file for integration tests

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix build and test issues

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Don't require kube client for odo create

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix storage tests

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Address gosec messages

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Finish docker integration tests

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix borked git rebase

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Address review comments

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix merge conflict issues

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix more merge conflict issues

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Fix more merge conflicts in tests

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Address rev

Signed-off-by: John Collier <John.J.Collier@ibm.com>

* Don't require kube context

Signed-off-by: John Collier <John.J.Collier@ibm.com>
This commit is contained in:
John Collier
2020-04-15 20:48:26 -04:00
committed by GitHub
parent a8d0a170a3
commit 22a63649b0
25 changed files with 1709 additions and 35 deletions

View File

@@ -139,5 +139,13 @@ jobs:
- travis_wait make test-e2e-source
- travis_wait make test-e2e-images
- odo logout
- <<: *base-test
stage: test
name: "docker devfile push command integration tests"
script:
- ./scripts/oc-cluster.sh
- make bin
- sudo cp odo /usr/bin
- travis_wait make test-cmd-docker-devfile-push

View File

@@ -233,6 +233,11 @@ test-cmd-url:
test-cmd-devfile-url:
ginkgo $(GINKGO_FLAGS) -focus="odo devfile url command tests" tests/integration/devfile/
# Run odo push docker devfile command tests
.PHONY: test-cmd-docker-devfile-push
test-cmd-docker-devfile-push:
ginkgo $(GINKGO_FLAGS) -focus="odo docker devfile push command tests" tests/integration/devfile/docker/
# Run odo watch command tests
.PHONY: test-cmd-watch
test-cmd-watch:

View File

@@ -0,0 +1,44 @@
package docker
import (
"github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/docker/component"
"github.com/openshift/odo/pkg/lclient"
"github.com/pkg/errors"
)
// Adapter maps Devfiles to Docker resources and actions
type Adapter struct {
componentAdapter common.ComponentAdapter
}
// New instantiates a Docker adapter
func New(adapterContext common.AdapterContext, client lclient.Client) Adapter {
compAdapter := component.New(adapterContext, client)
return Adapter{
componentAdapter: compAdapter,
}
}
// Push creates Docker resources that correspond to the devfile if they don't already exist
func (d Adapter) Push(parameters common.PushParameters) error {
err := d.componentAdapter.Push(parameters)
if err != nil {
return errors.Wrap(err, "Failed to create the component")
}
return nil
}
// DoesComponentExist returns true if a component with the specified name exists
func (d Adapter) DoesComponentExist(cmpName string) bool {
return d.componentAdapter.DoesComponentExist(cmpName)
}
// Delete attempts to delete the component with the specified labels, returning an error if it fails
func (d Adapter) Delete(labels map[string]string) error {
return d.componentAdapter.Delete(labels)
}

View File

@@ -0,0 +1,51 @@
package component
import (
"github.com/pkg/errors"
"github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/docker/utils"
"github.com/openshift/odo/pkg/lclient"
)
// New instantiantes a component adapter
func New(adapterContext common.AdapterContext, client lclient.Client) Adapter {
return Adapter{
Client: client,
AdapterContext: adapterContext,
}
}
// Adapter is a component adapter implementation for Kubernetes
type Adapter struct {
Client lclient.Client
common.AdapterContext
}
// 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)
if componentExists {
err = a.updateComponent()
} else {
err = a.createComponent()
}
if err != nil {
return errors.Wrap(err, "unable to create or update component")
}
return nil
}
// 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)
}
// Delete attempts to delete the component with the specified labels, returning an error if it fails
// Stub function until the functionality is added as part of https://github.com/openshift/odo/issues/2581
func (d Adapter) Delete(labels map[string]string) error {
return nil
}

View File

@@ -0,0 +1,131 @@
package component
import (
"testing"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
devfileParser "github.com/openshift/odo/pkg/devfile/parser"
versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
"github.com/openshift/odo/pkg/testingutil"
)
func TestPush(t *testing.T) {
testComponentName := "test"
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentType versionsCommon.DevfileComponentType
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Invalid devfile",
componentType: "",
client: fakeClient,
wantErr: true,
},
{
name: "Case 2: Valid devfile",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeClient,
wantErr: false,
},
{
name: "Case 3: Valid devfile, docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: testComponentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
// ToDo: Add more meaningful unit tests once Push actually does something with its parameters
err := componentAdapter.Push(adaptersCommon.PushParameters{})
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("component adapter create unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestDoesComponentExist(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
client *lclient.Client
componentType versionsCommon.DevfileComponentType
componentName string
getComponentName string
want bool
}{
{
name: "Case 1: Valid component name",
client: fakeClient,
componentType: versionsCommon.DevfileComponentTypeDockerimage,
componentName: "golang",
getComponentName: "golang",
want: true,
},
{
name: "Case 2: Non-existent component name",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeClient,
componentName: "test-name",
getComponentName: "fake-component",
want: false,
},
{
name: "Case 3: Docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeErrorClient,
componentName: "test-name",
getComponentName: "fake-component",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: tt.componentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
// Verify that a comopnent with the specified name exists
componentExists := componentAdapter.DoesComponentExist(tt.getComponentName)
if componentExists != tt.want {
t.Errorf("expected %v, actual %v", tt.want, componentExists)
}
})
}
}

View File

@@ -0,0 +1,167 @@
package component
import (
"fmt"
"github.com/docker/docker/api/types/container"
"github.com/golang/glog"
"github.com/pkg/errors"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/docker/utils"
versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/log"
)
func (a Adapter) createComponent() (err error) {
componentName := a.ComponentName
volumeLabels := utils.GetProjectVolumeLabels(componentName)
supportedComponents := adaptersCommon.GetSupportedComponents(a.Devfile.Data)
if len(supportedComponents) == 0 {
return fmt.Errorf("No valid components found in the devfile")
}
// Create a docker volume to store the project source code
volume, err := a.Client.CreateVolume(volumeLabels)
if err != nil {
return errors.Wrapf(err, "Unable to create project source volume for component %s", componentName)
}
projectVolumeName := volume.Name
// Loop over each component and start a container for it
for _, comp := range supportedComponents {
err = a.pullAndStartContainer(componentName, projectVolumeName, comp)
if err != nil {
return errors.Wrapf(err, "unable to pull and start container %s for component %s", *comp.Alias, componentName)
}
}
glog.V(3).Infof("Successfully created all containers for component %s", componentName)
return nil
}
func (a Adapter) updateComponent() (err error) {
glog.V(3).Info("The component already exists, attempting to update it")
componentName := a.ComponentName
// Get the project source volume
volumeLabels := utils.GetProjectVolumeLabels(componentName)
vols, err := a.Client.GetVolumesByLabel(volumeLabels)
if err != nil {
return errors.Wrapf(err, "Unable to retrieve source volume for component "+componentName)
}
if len(vols) == 0 {
return fmt.Errorf("Unable to find source volume for component %s", componentName)
}
projectVolumeName := vols[0].Name
supportedComponents := adaptersCommon.GetSupportedComponents(a.Devfile.Data)
if len(supportedComponents) == 0 {
return fmt.Errorf("No valid components found in the devfile")
}
for _, comp := range supportedComponents {
// Check to see if this component is already running and if so, update it
// If component isn't running, re-create it, as it either may be new, or crashed.
containers, err := a.Client.GetContainersByComponentAndAlias(componentName, *comp.Alias)
if err != nil {
return errors.Wrapf(err, "unable to list containers for component %s", componentName)
}
if len(containers) == 0 {
// Container doesn't exist, so need to pull its image (to be safe) and start a new container
err = a.pullAndStartContainer(componentName, projectVolumeName, comp)
if err != nil {
return errors.Wrapf(err, "unable to pull and start container %s for component %s", *comp.Alias, componentName)
}
} else if len(containers) == 1 {
// Container already exists
containerID := containers[0].ID
// Get the associated container config from the container ID
containerConfig, err := a.Client.GetContainerConfig(containerID)
if err != nil {
return errors.Wrapf(err, "unable to get the container config for component %s", componentName)
}
// See if the container needs to be updated
if utils.DoesContainerNeedUpdating(comp, containerConfig) {
s := log.Spinner("Updating the component " + *comp.Alias)
defer s.End(false)
// Remove the container
err := a.Client.RemoveContainer(containerID)
if err != nil {
return errors.Wrapf(err, "Unable to remove container %s for component %s", containerID, *comp.Alias)
}
// Start the container
err = a.startContainer(componentName, projectVolumeName, comp)
if err != nil {
return errors.Wrapf(err, "Unable to start container for devfile component %s", *comp.Alias)
}
glog.V(3).Infof("Successfully created container %s for component %s", *comp.Image, componentName)
s.End(true)
}
} else {
// Multiple containers were returned with the specified label (which should be unique)
// Error out, as this isn't expected
return fmt.Errorf("Found multiple running containers for devfile component %s and cannot push changes", *comp.Alias)
}
}
return nil
}
func (a Adapter) pullAndStartContainer(componentName string, projectVolumeName string, comp versionsCommon.DevfileComponent) error {
// Container doesn't exist, so need to pull its image (to be safe) and start a new container
s := log.Spinner("Pulling image " + *comp.Image)
err := a.Client.PullImage(*comp.Image)
if err != nil {
s.End(false)
return errors.Wrapf(err, "Unable to pull %s image", *comp.Image)
}
s.End(true)
// Start the container
err = a.startContainer(componentName, projectVolumeName, comp)
if err != nil {
return errors.Wrapf(err, "Unable to start container for devfile component %s", *comp.Alias)
}
glog.V(3).Infof("Successfully created container %s for component %s", *comp.Image, componentName)
return nil
}
func (a Adapter) startContainer(componentName string, projectVolumeName string, comp versionsCommon.DevfileComponent) error {
containerConfig := a.generateAndGetContainerConfig(componentName, comp)
hostConfig := container.HostConfig{}
// If the component set `mountSources` to true, add the source volume to it
if comp.MountSources {
utils.AddProjectVolumeToComp(projectVolumeName, &hostConfig)
}
// Create the docker container
s := log.Spinner("Starting container for " + *comp.Image)
defer s.End(false)
err := a.Client.StartContainer(&containerConfig, &hostConfig, nil)
if err != nil {
return err
}
s.End(true)
return nil
}
func (a Adapter) generateAndGetContainerConfig(componentName string, comp versionsCommon.DevfileComponent) container.Config {
// Convert the env vars in the Devfile to the format expected by Docker
envVars := utils.ConvertEnvs(comp.Env)
containerLabels := map[string]string{
"component": componentName,
"alias": *comp.Alias,
}
containerConfig := a.Client.GenerateContainerConfig(*comp.Image, comp.Command, comp.Args, envVars, containerLabels)
return containerConfig
}

View File

@@ -0,0 +1,237 @@
package component
import (
"testing"
adaptersCommon "github.com/openshift/odo/pkg/devfile/adapters/common"
devfileParser "github.com/openshift/odo/pkg/devfile/parser"
versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
"github.com/openshift/odo/pkg/testingutil"
)
func TestCreateComponent(t *testing.T) {
testComponentName := "test"
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentType versionsCommon.DevfileComponentType
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Invalid devfile",
componentType: "",
client: fakeClient,
wantErr: true,
},
{
name: "Case 2: Valid devfile",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeClient,
wantErr: false,
},
{
name: "Case 3: Valid devfile, docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: testComponentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.createComponent()
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("component adapter create unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestUpdateComponent(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentType versionsCommon.DevfileComponentType
componentName string
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Invalid devfile",
componentType: "",
componentName: "",
client: fakeClient,
wantErr: true,
},
{
name: "Case 2: Valid devfile",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
componentName: "node",
client: fakeClient,
wantErr: false,
},
{
name: "Case 3: Valid devfile, docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
componentName: "",
client: fakeErrorClient,
wantErr: true,
},
{
name: "Case 3: Valid devfile, missing component",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
componentName: "fakecomponent",
client: fakeClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: tt.componentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.updateComponent()
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("component adapter update unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestPullAndStartContainer(t *testing.T) {
testComponentName := "test"
testVolumeName := "projects"
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentType versionsCommon.DevfileComponentType
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully start container",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: testComponentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.pullAndStartContainer(testComponentName, testVolumeName, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("component adapter create unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestStartContainer(t *testing.T) {
testComponentName := "test"
testVolumeName := "projects"
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentType versionsCommon.DevfileComponentType
client *lclient.Client
wantErr bool
}{
{
name: "Case 1: Successfully start container",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Docker client error",
componentType: versionsCommon.DevfileComponentTypeDockerimage,
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
devObj := devfileParser.DevfileObj{
Data: testingutil.TestDevfileData{
ComponentType: tt.componentType,
},
}
adapterCtx := adaptersCommon.AdapterContext{
ComponentName: testComponentName,
Devfile: devObj,
}
componentAdapter := New(adapterCtx, *tt.client)
err := componentAdapter.startContainer(testComponentName, testVolumeName, adapterCtx.Devfile.Data.GetAliasedComponents()[0])
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("component adapter create unexpected error %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -0,0 +1,68 @@
package utils
import (
"fmt"
"reflect"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
)
// ComponentExists checks if Docker containers labeled with the specified component name exists
func ComponentExists(client lclient.Client, name string) bool {
containerList, err := client.GetContainerList()
if err != nil {
return false
}
containers := client.GetContainersByComponent(name, containerList)
return len(containers) != 0
}
// ConvertEnvs converts environment variables from the devfile structure to an array of strings, as expected by Docker
func ConvertEnvs(vars []common.DockerimageEnv) []string {
dockerVars := []string{}
for _, env := range vars {
envString := fmt.Sprintf("%s=%s", *env.Name, *env.Value)
dockerVars = append(dockerVars, envString)
}
return dockerVars
}
// DoesContainerNeedUpdating returns true if a given container needs to be removed and recreated
// This function compares values in the container vs corresponding values in the devfile component.
// If any of the values between the two differ, a restart is required (and this function returns true)
// Unlike Kube, Docker doesn't provide a mechanism to update a container in place only when necesary
// so this function is necessary to prevent having to restart the container on every odo pushs
func DoesContainerNeedUpdating(component common.DevfileComponent, containerConfig *container.Config) bool {
// If the image was changed in the devfile, the container needs to be updated
if *component.Image != containerConfig.Image {
return true
}
// Update the container if the env vars were updated in the devfile
// Need to convert the devfile envvars to the format expected by Docker
devfileEnvVars := ConvertEnvs(component.Env)
return !reflect.DeepEqual(devfileEnvVars, containerConfig.Env)
}
func AddProjectVolumeToComp(projectVolumeName string, hostConfig *container.HostConfig) *container.HostConfig {
mount := mount.Mount{
Type: mount.TypeVolume,
Source: projectVolumeName,
Target: lclient.OdoSourceVolumeMount,
}
hostConfig.Mounts = append(hostConfig.Mounts, mount)
return hostConfig
}
// GetProjectVolumeLabels returns the label selectors used to retrieve the project/source volume for a given component
func GetProjectVolumeLabels(componentName string) map[string]string {
volumeLabels := map[string]string{
"component": componentName,
"type": "projects",
}
return volumeLabels
}

View File

@@ -0,0 +1,300 @@
package utils
import (
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/openshift/odo/pkg/devfile/parser/data/common"
"github.com/openshift/odo/pkg/lclient"
)
func TestComponentExists(t *testing.T) {
fakeClient := lclient.FakeNew()
fakeErrorClient := lclient.FakeErrorNew()
tests := []struct {
name string
componentName string
client *lclient.Client
want bool
}{
{
name: "Case 1: Component exists",
componentName: "golang",
client: fakeClient,
want: true,
},
{
name: "Case 2: Component doesn't exist",
componentName: "fakecomponent",
client: fakeClient,
want: false,
},
{
name: "Case 3: Error with docker client",
componentName: "golang",
client: fakeErrorClient,
want: false,
},
}
for _, tt := range tests {
cmpExists := ComponentExists(*tt.client, tt.componentName)
if tt.want != cmpExists {
t.Errorf("expected %v, wanted %v", cmpExists, tt.want)
}
}
}
func TestConvertEnvs(t *testing.T) {
envVarsNames := []string{"test", "sample-var", "myvar"}
envVarsValues := []string{"value1", "value2", "value3"}
tests := []struct {
name string
envVars []common.DockerimageEnv
want []string
}{
{
name: "Case 1: One env var",
envVars: []common.DockerimageEnv{
{
Name: &envVarsNames[0],
Value: &envVarsValues[0],
},
},
want: []string{"test=value1"},
},
{
name: "Case 2: Multiple env vars",
envVars: []common.DockerimageEnv{
{
Name: &envVarsNames[0],
Value: &envVarsValues[0],
},
{
Name: &envVarsNames[1],
Value: &envVarsValues[1],
},
{
Name: &envVarsNames[2],
Value: &envVarsValues[2],
},
},
want: []string{"test=value1", "sample-var=value2", "myvar=value3"},
},
{
name: "Case 3: No env vars",
envVars: []common.DockerimageEnv{},
want: []string{},
},
}
for _, tt := range tests {
envVars := ConvertEnvs(tt.envVars)
if !reflect.DeepEqual(tt.want, envVars) {
t.Errorf("expected %v, wanted %v", envVars, tt.want)
}
}
}
func TestDoesContainerNeedUpdating(t *testing.T) {
envVarsNames := []string{"test", "sample-var", "myvar"}
envVarsValues := []string{"value1", "value2", "value3"}
tests := []struct {
name string
envVars []common.DockerimageEnv
image string
containerConfig container.Config
want bool
}{
{
name: "Case 1: No changes",
envVars: []common.DockerimageEnv{
{
Name: &envVarsNames[0],
Value: &envVarsValues[0],
},
{
Name: &envVarsNames[1],
Value: &envVarsValues[1],
},
},
image: "golang",
containerConfig: container.Config{
Image: "golang",
Env: []string{"test=value1", "sample-var=value2"},
},
want: false,
},
{
name: "Case 2: Update required, env var changed",
envVars: []common.DockerimageEnv{
{
Name: &envVarsNames[2],
Value: &envVarsValues[2],
},
},
image: "golang",
containerConfig: container.Config{
Image: "golang",
Env: []string{"test=value1", "sample-var=value2"},
},
want: true,
},
{
name: "Case 2: Update required, image changed",
envVars: []common.DockerimageEnv{
{
Name: &envVarsNames[2],
Value: &envVarsValues[2],
},
},
image: "node",
containerConfig: container.Config{
Image: "golang",
Env: []string{"test=value1", "sample-var=value2"},
},
want: true,
},
}
for _, tt := range tests {
component := common.DevfileComponent{
DevfileComponentDockerimage: common.DevfileComponentDockerimage{
Image: &tt.image,
Env: tt.envVars,
},
}
needsUpdating := DoesContainerNeedUpdating(component, &tt.containerConfig)
if needsUpdating != tt.want {
t.Errorf("expected %v, wanted %v", needsUpdating, tt.want)
}
}
}
func TestAddProjectVolumeToComp(t *testing.T) {
projectVolumeName := "projects"
tests := []struct {
name string
mounts []mount.Mount
want container.HostConfig
}{
{
name: "Case 1: No existing mounts",
mounts: []mount.Mount{},
want: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeVolume,
Source: projectVolumeName,
Target: lclient.OdoSourceVolumeMount,
},
},
},
},
{
name: "Case 2: One existing mount",
mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: "/my/local/folder",
Target: "/test",
},
},
want: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: "/my/local/folder",
Target: "/test",
},
{
Type: mount.TypeVolume,
Source: projectVolumeName,
Target: lclient.OdoSourceVolumeMount,
},
},
},
},
{
name: "Case 3: Multiple mounts",
mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: "/my/local/folder",
Target: "/test",
},
{
Type: mount.TypeBind,
Source: "/my/second/folder",
Target: "/two",
},
},
want: container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: "/my/local/folder",
Target: "/test",
},
{
Type: mount.TypeBind,
Source: "/my/second/folder",
Target: "/two",
},
{
Type: mount.TypeVolume,
Source: projectVolumeName,
Target: lclient.OdoSourceVolumeMount,
},
},
},
},
}
for _, tt := range tests {
hostConfig := container.HostConfig{
Mounts: tt.mounts,
}
AddProjectVolumeToComp(projectVolumeName, &hostConfig)
if !reflect.DeepEqual(tt.want, hostConfig) {
t.Errorf("expected %v, actual %v", tt.want, hostConfig)
}
}
}
func TestGetProjectVolumeLabels(t *testing.T) {
tests := []struct {
name string
componentName string
want map[string]string
}{
{
name: "Case 1: Regular component name",
componentName: "some-component",
want: map[string]string{
"component": "some-component",
"type": "projects",
},
},
{
name: "Case 1: Empty component name",
componentName: "",
want: map[string]string{
"component": "",
"type": "projects",
},
},
}
for _, tt := range tests {
labels := GetProjectVolumeLabels(tt.componentName)
if !reflect.DeepEqual(tt.want, labels) {
t.Errorf("expected %v, actual %v", tt.want, labels)
}
}
}

View File

@@ -4,9 +4,12 @@ import (
"fmt"
"github.com/openshift/odo/pkg/devfile/adapters/common"
"github.com/openshift/odo/pkg/devfile/adapters/docker"
"github.com/openshift/odo/pkg/devfile/adapters/kubernetes"
devfileParser "github.com/openshift/odo/pkg/devfile/parser"
"github.com/openshift/odo/pkg/kclient"
"github.com/openshift/odo/pkg/lclient"
"github.com/openshift/odo/pkg/odo/util/pushtarget"
)
// NewPlatformAdapter returns a Devfile adapter for the targeted platform
@@ -17,13 +20,17 @@ func NewPlatformAdapter(componentName string, devObj devfileParser.DevfileObj, p
Devfile: devObj,
}
// Only the kubernetes adapter is implemented at the moment
// When there are others this function should be updated to retrieve the correct adapter for the desired platform target
// If the pushtarget is set to Docker, initialize the Docker adapter, otherwise initialize the Kubernetes adapter
if pushtarget.IsPushTargetDocker() {
return createDockerAdapter(adapterContext)
}
kc, ok := platformContext.(kubernetes.KubernetesContext)
if !ok {
return nil, fmt.Errorf("Error retrieving context for Kubernetes")
}
return createKubernetesAdapter(adapterContext, kc.Namespace)
}
func createKubernetesAdapter(adapterContext common.AdapterContext, namespace string) (PlatformAdapter, error) {
@@ -45,3 +52,17 @@ func newKubernetesAdapter(adapterContext common.AdapterContext, client kclient.C
return kubernetesAdapter, nil
}
func createDockerAdapter(adapterContext common.AdapterContext) (PlatformAdapter, error) {
client, err := lclient.New()
if err != nil {
return nil, err
}
return newDockerAdapter(adapterContext, *client)
}
func newDockerAdapter(adapterContext common.AdapterContext, client lclient.Client) (PlatformAdapter, error) {
dockerAdapter := docker.New(adapterContext, client)
return dockerAdapter, nil
}

View File

@@ -3,12 +3,13 @@ package lclient
import (
"context"
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
volumeTypes "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/pkg/errors"
)
@@ -19,7 +20,11 @@ Please ensure that Docker is currently running on your machine.
// MinDockerAPIVersion is the minimum Docker API version to use
// 1.30 corresponds to Docker 17.05, which should be sufficiently old enough
const MinDockerAPIVersion = "1.30"
const (
MinDockerAPIVersion = "1.30" // MinDockerAPIVersion is the minimum Docker API version to use 1.30 corresponds to Docker 17.05, which should be sufficiently old enough to support most systems
DockerStorageDriver = ""
OdoSourceVolumeMount = "/projects"
)
// DockerClient requires functions called on the docker client package
// By abstracting these functions into an interface, it makes creating mock clients for unit testing much easier
@@ -31,9 +36,10 @@ type DockerClient interface {
ContainerList(ctx context.Context, containerListOptions types.ContainerListOptions) ([]types.Container, error)
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error)
ContainerStop(ctx context.Context, containerID string, timeout *time.Duration) error
ContainerRemove(ctx context.Context, containerID string, options types.ContainerRemoveOptions) error
DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error)
VolumeCreate(ctx context.Context, options volumeTypes.VolumeCreateBody) (types.Volume, error)
VolumeList(ctx context.Context, filter filters.Args) (volumeTypes.VolumeListOKBody, error)
}
// Client is a collection of fields used for client configuration and interaction

View File

@@ -20,6 +20,21 @@ func (dc *Client) GetContainersByComponent(componentName string, containers []ty
return containerList
}
// GetContainersByComponentAndAlias returns the list of Docker containers that have the same component and alias labeled
func (dc *Client) GetContainersByComponentAndAlias(componentName string, alias string) ([]types.Container, error) {
containerList, err := dc.GetContainerList()
if err != nil {
return nil, err
}
var labeledContainers []types.Container
for _, container := range containerList {
if container.Labels["component"] == componentName && container.Labels["alias"] == alias {
labeledContainers = append(labeledContainers, container)
}
}
return labeledContainers, nil
}
// GetContainerList returns a list of all of the running containers on the user's system
func (dc *Client) GetContainerList() ([]types.Container, error) {
containers, err := dc.Client.ContainerList(dc.Context, types.ContainerListOptions{})
@@ -33,10 +48,9 @@ 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)
// containerName - name to give to the container
// Returns an error if the container couldn't be started.
func (dc *Client) StartContainer(containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) error {
resp, err := dc.Client.ContainerCreate(dc.Context, containerConfig, hostConfig, networkingConfig, containerName)
func (dc *Client) StartContainer(containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) error {
resp, err := dc.Client.ContainerCreate(dc.Context, containerConfig, hostConfig, networkingConfig, "")
if err != nil {
return err
}
@@ -48,3 +62,24 @@ func (dc *Client) StartContainer(containerConfig *container.Config, hostConfig *
return nil
}
// RemoveContainer takes in a given container ID and kills it, then removes it.
func (dc *Client) RemoveContainer(containerID string) error {
err := dc.Client.ContainerRemove(dc.Context, containerID, types.ContainerRemoveOptions{
Force: true,
})
if err != nil {
return errors.Wrapf(err, "unable to remove container %s", containerID)
}
return nil
}
// GetContainerConfig takes in a given container ID and retrieves its corresponding container config
func (dc *Client) GetContainerConfig(containerID string) (*container.Config, error) {
containerJSON, err := dc.Client.ContainerInspect(dc.Context, containerID)
if err != nil {
return nil, errors.Wrapf(err, "unable to inspect container %s", containerID)
}
return containerJSON.Config, nil
}

View File

@@ -200,7 +200,7 @@ func TestGetContainersList(t *testing.T) {
}
}
func TestStartStartContainer(t *testing.T) {
func TestStartContainer(t *testing.T) {
fakeClient := FakeNew()
fakeErrorClient := FakeErrorNew()
@@ -222,7 +222,36 @@ func TestStartStartContainer(t *testing.T) {
},
}
for _, tt := range tests {
err := tt.client.StartContainer(&fakeContainer, nil, nil, "golang")
err := tt.client.StartContainer(&fakeContainer, nil, nil)
if !tt.wantErr == (err != nil) {
t.Errorf("expected %v, wanted %v", err, tt.wantErr)
}
}
}
func TestRemoveContainer(t *testing.T) {
fakeClient := FakeNew()
fakeErrorClient := FakeErrorNew()
fakeContainerID := "golang"
tests := []struct {
name string
client *Client
wantErr bool
}{
{
name: "Case 1: Successfully remove container",
client: fakeClient,
wantErr: false,
},
{
name: "Case 2: Fail to remove container",
client: fakeErrorClient,
wantErr: true,
},
}
for _, tt := range tests {
err := tt.client.RemoveContainer(fakeContainerID)
if !tt.wantErr == (err != nil) {
t.Errorf("expected %v, wanted %v", err, tt.wantErr)
}

View File

@@ -10,8 +10,10 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/registry"
volumeTypes "github.com/docker/docker/api/types/volume"
)
// This mock client will return container and images lists
@@ -116,6 +118,41 @@ func (m *mockDockerClient) DistributionInspect(ctx context.Context, image, encod
return registry.DistributionInspect{}, nil
}
func (m *mockDockerClient) VolumeCreate(ctx context.Context, options volumeTypes.VolumeCreateBody) (types.Volume, error) {
return types.Volume{
Driver: "local",
Labels: options.Labels,
}, nil
}
func (m *mockDockerClient) VolumeList(ctx context.Context, filter filters.Args) (volumeTypes.VolumeListOKBody, error) {
return volumeTypes.VolumeListOKBody{
Volumes: []*types.Volume{
{
Labels: map[string]string{
"component": "golang",
},
},
{
Labels: map[string]string{
"component": "golang",
},
},
{
Labels: map[string]string{
"component": "java",
},
},
{
Labels: map[string]string{
"component": "node",
"type": "projects",
},
},
},
}, nil
}
// This mock client will return errors for each call to a docker function
type mockDockerErrorClient struct {
}
@@ -139,6 +176,8 @@ var errContainerRemove = errors.New("error removing container")
var errContainerInspect = errors.New("error inspecting container")
var errContainerWait = errors.New("error timeout waiting for container")
var errDistributionInspect = errors.New("error inspecting distribution")
var errVolumeCreate = errors.New("error creating volume")
var errVolumeList = errors.New("error listing volume")
func (m *mockDockerErrorClient) ImageList(ctx context.Context, imageListOptions types.ImageListOptions) ([]types.ImageSummary, error) {
return nil, errImageList
@@ -182,3 +221,11 @@ func (m *mockDockerErrorClient) ContainerWait(ctx context.Context, containerID s
func (m *mockDockerErrorClient) DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error) {
return registry.DistributionInspect{}, errDistributionInspect
}
func (m *mockDockerErrorClient) VolumeCreate(ctx context.Context, options volumeTypes.VolumeCreateBody) (types.Volume, error) {
return types.Volume{}, errVolumeCreate
}
func (m *mockDockerErrorClient) VolumeList(ctx context.Context, filter filters.Args) (volumeTypes.VolumeListOKBody, error) {
return volumeTypes.VolumeListOKBody{}, errVolumeList
}

25
pkg/lclient/generators.go Normal file
View File

@@ -0,0 +1,25 @@
package lclient
import (
"github.com/docker/docker/api/types/container"
)
// GenerateContainerConfig creates a containerConfig resource that can be used to create a local Docker container
func (dc *Client) GenerateContainerConfig(image string, entrypoint []string, cmd []string, envVars []string, labels map[string]string) container.Config {
containerConfig := container.Config{
Image: image,
Entrypoint: entrypoint,
Cmd: cmd,
Env: envVars,
Labels: labels,
}
return containerConfig
}
func (dc *Client) GenerateHostConfig(isPrivileged bool, publishPorts bool) container.HostConfig {
hostConfig := container.HostConfig{
Privileged: isPrivileged,
PublishAllPorts: publishPorts,
}
return hostConfig
}

View File

@@ -0,0 +1,118 @@
package lclient
import (
"reflect"
"testing"
"github.com/docker/docker/api/types/container"
)
// GenerateContainerConfig creates a containerConfig resource that can be used to create a local Docker container
func TestGenerateContainerConfig(t *testing.T) {
fakeClient := FakeNew()
tests := []struct {
name string
image string
entrypoint []string
cmd []string
envVars []string
labels map[string]string
want container.Config
}{
{
name: "Case 1: Simple config, no env vars or labels",
image: "docker.io/fake-image:latest",
entrypoint: []string{"bash"},
cmd: []string{"tail", "-f", "/dev/null"},
envVars: []string{},
labels: nil,
want: container.Config{
Image: "docker.io/fake-image:latest",
Entrypoint: []string{"bash"},
Cmd: []string{"tail", "-f", "/dev/null"},
Env: []string{},
Labels: nil,
},
},
{
name: "Case 2: Simple config, env vars and labels set",
image: "docker.io/fake-image:latest",
entrypoint: []string{"bash"},
cmd: []string{"tail", "-f", "/dev/null"},
envVars: []string{"test=hello", "sample=value"},
labels: map[string]string{
"component": "some-component",
"alias": "maven",
},
want: container.Config{
Image: "docker.io/fake-image:latest",
Entrypoint: []string{"bash"},
Cmd: []string{"tail", "-f", "/dev/null"},
Env: []string{"test=hello", "sample=value"},
Labels: map[string]string{
"component": "some-component",
"alias": "maven",
},
},
},
}
for _, tt := range tests {
config := fakeClient.GenerateContainerConfig(tt.image, tt.entrypoint, tt.cmd, tt.envVars, tt.labels)
if !reflect.DeepEqual(tt.want, config) {
t.Errorf("expected %v, actual %v", tt.want, config)
}
}
}
func TestGenerateHostConfig(t *testing.T) {
fakeClient := FakeNew()
tests := []struct {
name string
privileged bool
publishPorts bool
want container.HostConfig
}{
{
name: "Case 1: Unprivileged and not publishing ports",
privileged: false,
publishPorts: false,
want: container.HostConfig{
Privileged: false,
PublishAllPorts: false,
},
},
{
name: "Case 2: Privileged and not publishing ports",
privileged: true,
publishPorts: false,
want: container.HostConfig{
Privileged: true,
PublishAllPorts: false,
},
},
{
name: "Case 3: Unprivileged and publishing ports",
privileged: false,
publishPorts: true,
want: container.HostConfig{
Privileged: false,
PublishAllPorts: true,
},
},
{
name: "Case 4: Privileged and publishing ports",
privileged: true,
publishPorts: true,
want: container.HostConfig{
Privileged: true,
PublishAllPorts: true,
},
},
}
for _, tt := range tests {
config := fakeClient.GenerateHostConfig(tt.privileged, tt.publishPorts)
if !reflect.DeepEqual(tt.want, config) {
t.Errorf("expected %v, actual %v", tt.want, config)
}
}
}

View File

@@ -1,12 +1,13 @@
package lclient
import (
"bytes"
"io"
"os"
"github.com/docker/docker/api/types"
"github.com/golang/glog"
"github.com/pkg/errors"
"k8s.io/klog/glog"
)
// PullImage uses Docker to pull the specified image. If there are any issues pulling the image,
@@ -18,13 +19,20 @@ func (dc *Client) PullImage(image string) error {
if err != nil {
return errors.Wrapf(err, "Unable to pull image")
}
defer out.Close()
if glog.V(4) {
_, err := io.Copy(os.Stdout, out)
if err != nil {
return err
}
} else {
// Need to read from the buffer or else Docker won't finish pulling the image
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(out)
if err != nil {
return err
}
}
return nil

41
pkg/lclient/storage.go Normal file
View File

@@ -0,0 +1,41 @@
package lclient
import (
"reflect"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
volumeTypes "github.com/docker/docker/api/types/volume"
"github.com/pkg/errors"
)
// CreateVolume creates a Docker volume with the given labels and the default Docker storage driver
func (dc *Client) CreateVolume(labels map[string]string) (types.Volume, error) {
volume, err := dc.Client.VolumeCreate(dc.Context, volumeTypes.VolumeCreateBody{
Driver: DockerStorageDriver,
Labels: labels,
})
if err != nil {
return volume, errors.Wrapf(err, "error creating docker volume")
}
return volume, nil
}
// GetVolumesByLabel returns the list of all volumes matching the given label.
func (dc *Client) GetVolumesByLabel(labels map[string]string) ([]types.Volume, error) {
var volumes []types.Volume
volumeList, err := dc.Client.VolumeList(dc.Context, filters.Args{})
if err != nil {
return nil, errors.Wrapf(err, "unable to get list of docker volumes")
}
for _, vol := range volumeList.Volumes {
if reflect.DeepEqual(vol.Labels, labels) {
volumes = append(volumes, *vol)
}
}
return volumes, nil
}

143
pkg/lclient/storage_test.go Normal file
View File

@@ -0,0 +1,143 @@
package lclient
import (
"reflect"
"testing"
"github.com/docker/docker/api/types"
)
func TestCreateVolume(t *testing.T) {
fakeClient := FakeNew()
fakeErrorClient := FakeErrorNew()
tests := []struct {
name string
client *Client
labels map[string]string
wantErr bool
wantVolume types.Volume
}{
{
name: "Case 1: Create volume, no labels",
client: fakeClient,
labels: map[string]string{},
wantErr: false,
wantVolume: types.Volume{
Driver: "local",
Labels: map[string]string{},
},
},
{
name: "Case 2: Create volume, multiple labels",
client: fakeClient,
labels: map[string]string{
"component": "golang",
"type": "project",
},
wantErr: false,
wantVolume: types.Volume{
Driver: "local",
Labels: map[string]string{
"component": "golang",
"type": "project",
},
},
},
{
name: "Case 3: Unable to create volume",
client: fakeErrorClient,
labels: map[string]string{
"component": "golang",
"type": "project",
},
wantErr: true,
wantVolume: types.Volume{},
},
}
for _, tt := range tests {
volume, err := tt.client.CreateVolume(tt.labels)
if !tt.wantErr == (err != nil) {
t.Errorf("expected %v, wanted %v", err, tt.wantErr)
}
if !reflect.DeepEqual(volume, tt.wantVolume) {
t.Errorf("expected %v, wanted %v", volume, tt.wantVolume)
}
}
}
func TestGetVolumesByLabel(t *testing.T) {
fakeClient := FakeNew()
fakeErrorClient := FakeErrorNew()
tests := []struct {
name string
client *Client
labels map[string]string
wantErr bool
wantVolumes []types.Volume
}{
{
name: "Case 1: Only one volume with label",
client: fakeClient,
labels: map[string]string{
"component": "java",
},
wantErr: false,
wantVolumes: []types.Volume{
{
Labels: map[string]string{
"component": "java",
},
},
},
},
{
name: "Case 2: Multiple volumes with label",
client: fakeClient,
labels: map[string]string{
"component": "golang",
},
wantErr: false,
wantVolumes: []types.Volume{
{
Labels: map[string]string{
"component": "golang",
},
},
{
Labels: map[string]string{
"component": "golang",
},
},
},
},
{
name: "Case 3: No volumes with label",
client: fakeClient,
labels: map[string]string{
"fakecomponent": "test",
},
wantErr: false,
wantVolumes: nil,
},
{
name: "Case 4: Docker client error",
client: fakeErrorClient,
labels: map[string]string{
"fakecomponent": "test",
},
wantErr: true,
wantVolumes: nil,
},
}
for _, tt := range tests {
volumes, err := tt.client.GetVolumesByLabel(tt.labels)
if !tt.wantErr == (err != nil) {
t.Errorf("expected %v, wanted %v", err, tt.wantErr)
}
if !reflect.DeepEqual(volumes, tt.wantVolumes) {
t.Errorf("expected %v, wanted %v", volumes, tt.wantVolumes)
}
}
}

View File

@@ -26,6 +26,7 @@ import (
odoutil "github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/odo/util/completion"
"github.com/openshift/odo/pkg/odo/util/experimental"
"github.com/openshift/odo/pkg/odo/util/pushtarget"
"github.com/openshift/odo/pkg/util"
corev1 "k8s.io/api/core/v1"
@@ -295,13 +296,16 @@ func (co *CreateOptions) Complete(name string, cmd *cobra.Command, args []string
if len(args) == 0 {
co.interactive = true
}
// Get current active namespace
client, err := kclient.New()
if err != nil {
return err
var defaultComponentNamespace string
// If the push target is set to Docker, we can't assume we have an active Kube context
if !pushtarget.IsPushTargetDocker() {
// Get current active namespace
client, err := kclient.New()
if err != nil {
return err
}
defaultComponentNamespace = client.Namespace
}
defaultComponentNamespace := client.Namespace
catalogDevfileList, err := catalog.ListDevfileComponents()
if err != nil {
@@ -612,9 +616,12 @@ func (co *CreateOptions) Validate() (err error) {
return err
}
err := util.ValidateK8sResourceName("component namespace", co.devfileMetadata.componentNamespace)
if err != nil {
return err
// Only validate namespace if pushtarget isn't docker
if !pushtarget.IsPushTargetDocker() {
err := util.ValidateK8sResourceName("component namespace", co.devfileMetadata.componentNamespace)
if err != nil {
return err
}
}
spinner.End(true)

View File

@@ -8,6 +8,7 @@ import (
"github.com/openshift/odo/pkg/envinfo"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util/pushtarget"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
@@ -59,16 +60,27 @@ func (po *PushOptions) DevfilePush() (err error) {
spinner := log.SpinnerNoSpin(fmt.Sprintf("Push devfile component %s", componentName))
defer spinner.End(false)
if len(po.namespace) <= 0 {
po.namespace, err = getNamespace()
if err != nil {
return err
var platformContext interface{}
if pushtarget.IsPushTargetDocker() {
platformContext = nil
} else {
if len(po.namespace) <= 0 {
po.namespace, err = getNamespace()
if err != nil {
return err
}
}
po.Context.KClient.Namespace = po.namespace
kc := kubernetes.KubernetesContext{
Namespace: po.namespace,
}
platformContext = kc
}
kc := kubernetes.KubernetesContext{
Namespace: po.namespace,
}
devfileHandler, err := adapters.NewPlatformAdapter(componentName, devObj, kc)
devfileHandler, err := adapters.NewPlatformAdapter(componentName, devObj, platformContext)
if err != nil {
return err

View File

@@ -14,6 +14,7 @@ import (
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/odo/util/pushtarget"
"github.com/openshift/odo/pkg/project"
pkgUtil "github.com/openshift/odo/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -465,15 +466,19 @@ func newDevfileContext(command *cobra.Command) *Context {
command: command,
}
// create a new kclient
kClient := kClient(command)
internalCxt.KClient = kClient
envInfo, err := getValidEnvinfo(command)
if err != nil {
util.LogErrorAndExit(err, "")
}
internalCxt.EnvSpecificInfo = envInfo
resolveNamespace(command, kClient, envInfo)
if !pushtarget.IsPushTargetDocker() {
// create a new kclient
kClient := kClient(command)
internalCxt.KClient = kClient
internalCxt.EnvSpecificInfo = envInfo
resolveNamespace(command, kClient, envInfo)
}
// create a context from the internal representation
context := &Context{
internalCxt: internalCxt,

View File

@@ -0,0 +1,72 @@
package helper
import (
"fmt"
"strings"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
)
type DockerRunner struct {
// path to docker binary
path string
}
// NewDockerRunner initializes new DockerRunner
func NewDockerRunner(dockerPath string) DockerRunner {
return DockerRunner{
path: dockerPath,
}
}
// Run dpcler with given arguments
func (d *DockerRunner) Run(cmd string) *gexec.Session {
session := CmdRunner(cmd)
Eventually(session).Should(gexec.Exit(0))
return session
}
// ListRunningContainers runs 'docker ps' to list all running images
func (d *DockerRunner) ListRunningContainers() string {
fmt.Fprintf(GinkgoWriter, "Listing locally running Docker images")
output := CmdShouldPass(d.path, "ps")
return output
}
// GetRunningContainersByLabel lists all running images with the label (of the form "key=value")
func (d *DockerRunner) GetRunningContainersByLabel(label string) []string {
fmt.Fprintf(GinkgoWriter, "Listing locally running Docker images with label %s", label)
filterLabel := "label=" + label
output := strings.TrimSpace(CmdShouldPass(d.path, "ps", "-q", "--filter", filterLabel))
// Split the strings and remove any whitespace
containers := strings.Fields(output)
return containers
}
// ListVolumes lists all volumes on the cluster
func (d *DockerRunner) ListVolumes() string {
session := CmdRunner(d.path, "volume", "ls", "-q")
session.Wait()
if session.ExitCode() == 0 {
return strings.TrimSpace(string(session.Out.Contents()))
}
return ""
}
// StopContainers kills and stops all running containers with the specified label (such as component=nodejs)
func (d *DockerRunner) StopContainers(label string) {
fmt.Fprintf(GinkgoWriter, "Removing locally running Docker images with label %s", label)
// Get the container IDs matching the specified label
containerIDs := d.GetRunningContainersByLabel(label)
// Loop over the containers to remove and run `docker stop` for each of them
// We have to loop because `docker stop` does not allow us to remove all of the containers at once (e.g. docker stop con1 con2 con3 ... is not allowed)
for _, container := range containerIDs {
CmdShouldPass(d.path, "stop", container)
}
}

View File

@@ -0,0 +1,79 @@
package docker
import (
"os"
"path/filepath"
"time"
"github.com/openshift/odo/tests/helper"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("odo docker devfile push command tests", func() {
var context string
var currentWorkingDirectory string
var cmpName string
dockerClient := helper.NewDockerRunner("docker")
// This is run after every Spec (It)
var _ = BeforeEach(func() {
SetDefaultEventuallyTimeout(10 * time.Minute)
context = helper.CreateNewContext()
currentWorkingDirectory = helper.Getwd()
cmpName = helper.RandString(6)
helper.Chdir(context)
os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml"))
})
// Clean up after the test
// This is run after every Spec (It)
var _ = AfterEach(func() {
// Stop all containers labeled with the component name
label := "component=" + cmpName
dockerClient.StopContainers(label)
helper.Chdir(currentWorkingDirectory)
helper.DeleteDir(context)
os.Unsetenv("GLOBALODOCONFIG")
})
Context("Verify devfile push works", func() {
It("Check that odo push works with a devfile", func() {
// Local devfile push requires experimental mode to be set and the pushtarget set to docker
helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true")
helper.CmdShouldPass("odo", "preference", "set", "pushtarget", "docker")
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), context)
helper.CmdShouldPass("odo", "create", "nodejs", "--context", context, cmpName)
output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml")
Expect(output).To(ContainSubstring("Changes successfully pushed to component"))
// update devfile and push again
helper.ReplaceString("devfile.yaml", "name: FOO", "name: BAR")
helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml")
})
It("Check that odo push works with a devfile that has multiple containers", func() {
// Local devfile push requires experimental mode to be set and the pushtarget set to docker
helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true")
helper.CmdShouldPass("odo", "preference", "set", "pushtarget", "docker")
// Springboot devfile references multiple containers
helper.CmdShouldPass("odo", "create", "java-spring-boot", "--context", context, cmpName)
output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml")
Expect(output).To(ContainSubstring("Changes successfully pushed to component"))
// update devfile and push again
helper.ReplaceString("devfile.yaml", "name: FOO", "name: BAR")
helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml")
})
})
})

View File

@@ -0,0 +1,15 @@
package docker
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestDocker(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Docker Devfile Suite")
// Keep CustomReporters commented till https://github.com/onsi/ginkgo/issues/628 is fixed
// RunSpecsWithDefaultAndCustomReporters(t, "Project Suite", []Reporter{reporter.JunitReport(t, "../../../reports")})
}