mirror of
				https://github.com/redhat-developer/odo.git
				synced 2025-10-19 03:06:19 +03:00 
			
		
		
		
	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:
		
							
								
								
									
										10
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							| @@ -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: | ||||
|   | ||||
							
								
								
									
										44
									
								
								pkg/devfile/adapters/docker/adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								pkg/devfile/adapters/docker/adapter.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										51
									
								
								pkg/devfile/adapters/docker/component/adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								pkg/devfile/adapters/docker/component/adapter.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										131
									
								
								pkg/devfile/adapters/docker/component/adapter_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								pkg/devfile/adapters/docker/component/adapter_test.go
									
									
									
									
									
										Normal 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) | ||||
| 			} | ||||
|  | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										167
									
								
								pkg/devfile/adapters/docker/component/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								pkg/devfile/adapters/docker/component/utils.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										237
									
								
								pkg/devfile/adapters/docker/component/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								pkg/devfile/adapters/docker/component/utils_test.go
									
									
									
									
									
										Normal 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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										68
									
								
								pkg/devfile/adapters/docker/utils/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								pkg/devfile/adapters/docker/utils/utils.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										300
									
								
								pkg/devfile/adapters/docker/utils/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								pkg/devfile/adapters/docker/utils/utils_test.go
									
									
									
									
									
										Normal 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) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										25
									
								
								pkg/lclient/generators.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										118
									
								
								pkg/lclient/generators_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								pkg/lclient/generators_test.go
									
									
									
									
									
										Normal 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										41
									
								
								pkg/lclient/storage.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										143
									
								
								pkg/lclient/storage_test.go
									
									
									
									
									
										Normal 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										72
									
								
								tests/helper/helper_docker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								tests/helper/helper_docker.go
									
									
									
									
									
										Normal 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) | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -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") | ||||
| 		}) | ||||
|  | ||||
| 	}) | ||||
|  | ||||
| }) | ||||
							
								
								
									
										15
									
								
								tests/integration/devfile/docker/docker_suite_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/integration/devfile/docker/docker_suite_test.go
									
									
									
									
									
										Normal 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")}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 John Collier
					John Collier