diff --git a/.travis.yml b/.travis.yml index e7a8f0516be..4c984293d43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ env: - MINIKUBE_WANTREPORTERRORPROMPT=false - MINIKUBE_HOME=$HOME - CHANGE_MINIKUBE_NONE_USER=true - - KUBECONFIG=$HOME/.kube/config + - KUBECONFIG=$HOME/.kube/config jobs: include: # YAML alias, for settings shared across the tests @@ -70,7 +70,7 @@ jobs: - <<: *base-test stage: test - name: "generic, login, component command integration tests" + name: "generic, login and component command integration tests" script: - ./scripts/oc-cluster.sh - make bin @@ -98,7 +98,7 @@ jobs: # Run service-catalog e2e tests - <<: *base-test stage: test - name: "service, link, component sub-commands command integration tests" + name: "service, link and component sub-commands command integration tests" script: - ./scripts/oc-cluster.sh service-catalog - make bin @@ -111,7 +111,7 @@ jobs: - <<: *base-test stage: test - name: "watch, storage, app, project, push, devfile catalog, devfile create, devfile push, devfile delete, devfile registry command integration tests" + name: "watch, storage, app, project and push command integration tests" script: - ./scripts/oc-cluster.sh - make bin @@ -122,6 +122,22 @@ jobs: - travis_wait make test-cmd-app - travis_wait make test-cmd-push - travis_wait make test-cmd-project + - odo logout + + - <<: *base-test + stage: test + # Docker push target test command does not need a running cluster at all, however few test + # scenario of docker devfile url testing needs only Kube config file. So the test has been + # added here just to make sure docker devfile url command test gets a proper kube config file. + # without creating a separate OpenShift cluster. + name: "devfile catalog, create, push, delete, registry and docker devfile url command integration tests" + script: + - ./scripts/oc-cluster.sh + - make bin + - sudo cp odo /usr/bin + - travis_wait make test-cmd-docker-devfile-url + # These tests need cluster login as they will be interacting with a Kube environment + - odo login -u developer - travis_wait make test-cmd-devfile-catalog - travis_wait make test-cmd-devfile-create - travis_wait make test-cmd-devfile-push @@ -146,15 +162,14 @@ jobs: - <<: *base-test stage: test - name: "docker devfile push command integration tests" + name: "docker devfile push and delete command integration tests" script: - - ./scripts/oc-cluster.sh - make bin - sudo cp odo /usr/bin - travis_wait make test-cmd-docker-devfile-push - - travis_wait make test-cmd-docker-devfile-url + - travis_wait make test-cmd-docker-devfile-catalog + - travis_wait make test-cmd-docker-devfile-delete - # Run devfile integration test on Kubernetes cluster - <<: *base-test stage: test diff --git a/Makefile b/Makefile index 43b33d12739..3914e3da1cf 100644 --- a/Makefile +++ b/Makefile @@ -247,11 +247,16 @@ test-cmd-docker-devfile-push: .PHONY: test-cmd-docker-devfile-url test-cmd-docker-devfile-url: ginkgo $(GINKGO_FLAGS) -focus="odo docker devfile url command tests" tests/integration/devfile/docker/ + # Run odo docker devfile delete command tests .PHONY: test-cmd-docker-devfile-delete test-cmd-docker-devfile-delete: ginkgo $(GINKGO_FLAGS) -focus="odo docker devfile delete command tests" tests/integration/devfile/docker/ +# Run odo catalog devfile command tests +.PHONY: test-cmd-docker-devfile-catalog +test-cmd-docker-devfile-catalog: + ginkgo $(GINKGO_FLAGS) -focus="odo docker devfile catalog command tests" tests/integration/devfile/docker/ # Run odo watch command tests .PHONY: test-cmd-watch diff --git a/README.adoc b/README.adoc index e3cd392d802..1097edc0695 100644 --- a/README.adoc +++ b/README.adoc @@ -17,7 +17,7 @@ image:https://img.shields.io/github/license/openshift/odo?style=for-the-badge[Li `odo` is a fast, iterative, and straightforward CLI tool for developers who write, build, and deploy applications on OpenShift. -Existing tools such as `oc` are more operations-focused and require a deep-understanding of Kubernetes and OpenShift concepts. `odo` abstracts away complex Kubernetes and OpenShift concepts for the developer, thus allowing developers to focus on what is most important to them: code. +Existing tools such as `oc` are more operations-focused and require a deep-understanding of Kubernetes and OpenShift concepts. `odo` abstracts away complex Kubernetes and OpenShift concepts for the developer. [[key-features]] == Key features diff --git a/cmd/cli-doc/cli-doc.go b/cmd/cli-doc/cli-doc.go index 8f3106ccf10..cda8122ce43 100644 --- a/cmd/cli-doc/cli-doc.go +++ b/cmd/cli-doc/cli-doc.go @@ -81,7 +81,7 @@ func referencePrinter(command *cobra.Command, level int) string { } // The main markdown "template" for everything - return fmt.Sprintf(`= Overview of the OpenShift Do (odo) CLI Structure + return fmt.Sprintf(`= Overview of the odo CLI Structure ___________________ Example application diff --git a/docs/dev/development.adoc b/docs/dev/development.adoc index d9a030fe287..e5585504cbd 100644 --- a/docs/dev/development.adoc +++ b/docs/dev/development.adoc @@ -7,7 +7,12 @@ toc::[] == Setting up -Requires *Go 1.13* +Requires *Go 1.12* + +**WARNING**: If you are adding any features that require a higher version of golang, such as golang 1.13 +for example, please contact maintainers to check of the releasing systems can handle the newer versions. + +If that is ok, please ensure you update the required golang version, both here and in the file link:/scripts/rpm-prepare.sh[`scripts/rpm-prepare.sh`] . link:https://help.github.com/en/articles/fork-a-repo[Fork] the link:https://github.com/openshift/odo[`odo`] repository. diff --git a/pkg/component/component.go b/pkg/component/component.go index e09c4e67dde..5825ad49192 100644 --- a/pkg/component/component.go +++ b/pkg/component/component.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "github.com/openshift/odo/pkg/devfile/adapters/common" @@ -542,110 +543,49 @@ func ValidateComponentCreateRequest(client *occlient.Client, componentSettings c // Returns: // err: Errors if any else nil func ApplyConfig(client *occlient.Client, kClient *kclient.Client, componentConfig config.LocalConfigInfo, envSpecificInfo envinfo.EnvSpecificInfo, stdout io.Writer, cmpExist bool) (err error) { - if !experimental.IsExperimentalModeEnabled() { - // if component exist then only call the update function - if cmpExist { - if err = Update(client, componentConfig, componentConfig.GetSourceLocation(), stdout); err != nil { - return err - } - } - } - - showChanges, pushedURLMap, err := checkIfURLChangesWillBeMade(client, kClient, componentConfig, envSpecificInfo) - if err != nil { - return err - } + isExperimentalModeEnabled := experimental.IsExperimentalModeEnabled() - if showChanges { - log.Info("\nApplying URL changes") - // Create any URLs that have been added to the component - err = ApplyConfigCreateURL(client, kClient, componentConfig, envSpecificInfo, pushedURLMap) - if err != nil { - return err - } - - // Delete any URLs - err = applyConfigDeleteURL(client, kClient, componentConfig, envSpecificInfo, pushedURLMap) + if client == nil { + var err error + client, err = occlient.New() if err != nil { return err } + client.Namespace = envSpecificInfo.GetNamespace() } - return -} - -// ApplyConfigDeleteURL applies url config deletion onto component -func applyConfigDeleteURL(client *occlient.Client, kClient *kclient.Client, componentConfig config.LocalConfigInfo, envSpecificInfo envinfo.EnvSpecificInfo, pushedURLMap map[string]bool) (err error) { - if experimental.IsExperimentalModeEnabled() { - localURLList := envSpecificInfo.GetURL() - tempMap := make(map[string]envinfo.EnvInfoURL) - for _, urlElement := range localURLList { - tempMap[urlElement.Name] = urlElement - } - // urlName is the key of each element - for urlName := range pushedURLMap { - if _, exist := tempMap[urlName]; !exist { - err = urlpkg.Delete(client, kClient, urlName, componentConfig.GetApplication()) - if err != nil { - return err - } - log.Successf("URL %s successfully deleted", urlName) - } - } - } else { - localURLList := componentConfig.GetURL() - tempMap := make(map[string]config.ConfigURL) - for _, urlElement := range localURLList { - tempMap[urlElement.Name] = urlElement - } - // urlName is the key of each element - for urlName := range pushedURLMap { - if _, exist := tempMap[urlName]; !exist { - err = urlpkg.Delete(client, kClient, urlName, componentConfig.GetApplication()) - if err != nil { - return err - } - log.Successf("URL %s successfully deleted", urlName) + if !isExperimentalModeEnabled { + // if component exist then only call the update function + if cmpExist { + if err = Update(client, componentConfig, componentConfig.GetSourceLocation(), stdout); err != nil { + return err } } } - return nil -} -// ApplyConfigCreateURL applies url config onto component -func ApplyConfigCreateURL(client *occlient.Client, kClient *kclient.Client, componentConfig config.LocalConfigInfo, envSpecificInfo envinfo.EnvSpecificInfo, pushedURLMap map[string]bool) error { - if experimental.IsExperimentalModeEnabled() { - urls := envSpecificInfo.GetURL() - componentName := envSpecificInfo.GetName() - for _, urlo := range urls { - _, exist := pushedURLMap[urlo.Name] - if exist { - log.Successf("URL %s already exists", urlo.Name) - } else { - host, err := urlpkg.Create(client, kClient, urlo.Name, urlo.Port, urlo.Secure, componentName, "", urlo.Host, urlo.TLSSecret) - if err != nil { - return errors.Wrapf(err, "unable to create url") - } - log.Successf("URL %s: %s created", urlo.Name, host) - } - } + var componentName string + var applicationName string + if !isExperimentalModeEnabled || kClient == nil { + componentName = componentConfig.GetName() + applicationName = componentConfig.GetApplication() } else { - urls := componentConfig.GetURL() - for _, urlo := range urls { - _, exist := pushedURLMap[urlo.Name] - if exist { - log.Successf("URL %s already exists", urlo.Name) - } else { - host, err := urlpkg.Create(client, kClient, urlo.Name, urlo.Port, urlo.Secure, componentConfig.GetName(), componentConfig.GetApplication(), "", "") - if err != nil { - return errors.Wrapf(err, "unable to create url") - } - log.Successf("URL %s: %s created", urlo.Name, host) - } - } + componentName = envSpecificInfo.GetName() } - return nil + isRouteSupported := false + isRouteSupported, err = client.IsRouteSupported() + if err != nil { + isRouteSupported = false + } + + return urlpkg.Push(client, kClient, urlpkg.PushParameters{ + ComponentName: componentName, + ApplicationName: applicationName, + ConfigURLs: componentConfig.GetURL(), + EnvURLS: envSpecificInfo.GetURL(), + IsRouteSupported: isRouteSupported, + IsExperimentalModeEnabled: isExperimentalModeEnabled, + }) } // PushLocal push local code to the cluster and trigger build there. @@ -954,8 +894,21 @@ func GetComponentFromConfig(localConfig *config.LocalConfigInfo) (Component, err component.Spec.Source = util.GenFileURL(localConfig.GetSourceLocation()) } - for _, localURL := range localConfig.GetURL() { - component.Spec.URL = append(component.Spec.URL, localURL.Name) + urls := localConfig.GetURL() + if len(urls) > 0 { + // We will clean up the existing value of ports and re-populate it so that we don't panic in `odo describe` and don't show inconsistent info + // This will also help in the case where there are more URLs created than the number of ports exposed by a component #2776 + oldPortsProtocol, err := getPortsProtocolMapping(component.Spec.Ports) + if err != nil { + return Component{}, err + } + component.Spec.Ports = []string{} + + for _, url := range urls { + port := strconv.Itoa(url.Port) + component.Spec.Ports = append(component.Spec.Ports, fmt.Sprintf("%s/%s", port, oldPortsProtocol[port])) + component.Spec.URL = append(component.Spec.URL, url.Name) + } } for _, localEnv := range localConfig.GetEnvVars() { @@ -970,6 +923,22 @@ func GetComponentFromConfig(localConfig *config.LocalConfigInfo) (Component, err return Component{}, nil } +// This function returns a mapping of port and protocol. +// So for a value of ports {"8080/TCP", "45/UDP"} it will return a map {"8080": +// "TCP", "45": "UDP"} +func getPortsProtocolMapping(ports []string) (map[string]string, error) { + oldPortsProtocol := make(map[string]string, len(ports)) + for _, port := range ports { + portProtocol := strings.Split(port, "/") + if len(portProtocol) != 2 { + // this will be the case if value of a port is something like 8080/TCP/something-else or simply 8080 + return nil, errors.New("invalid mapping. Please update the component configuration") + } + oldPortsProtocol[portProtocol[0]] = portProtocol[1] + } + return oldPortsProtocol, nil +} + // ListIfPathGiven lists all available component in given path directory func ListIfPathGiven(client *occlient.Client, paths []string) (ComponentList, error) { var components []Component @@ -1496,45 +1465,6 @@ func getStorageFromConfig(localConfig *config.LocalConfigInfo) storage.StorageLi return storageList } -// checkIfURLChangesWillBeMade checks to see if there are going to be any changes -// to the URLs when deploying and returns a true / false -func checkIfURLChangesWillBeMade(client *occlient.Client, kClient *kclient.Client, componentConfig config.LocalConfigInfo, envSpecificInfo envinfo.EnvSpecificInfo) (bool, map[string]bool, error) { - if experimental.IsExperimentalModeEnabled() { - componentName := envSpecificInfo.GetName() - urlList, err := urlpkg.ListPushedIngress(kClient, componentName) - if err != nil { - return false, nil, err - } - - // If envinfo has URL(s) (since we check) or if the cluster has URL's but - // envinfo does not (deleting) - if len(envSpecificInfo.GetURL()) > 0 || len(envSpecificInfo.GetURL()) == 0 && (len(urlList.Items) > 0) { - pushedURLMap := make(map[string]bool) - for _, element := range urlList.Items { - pushedURLMap[element.Name] = true - } - return true, pushedURLMap, nil - } - } else { - urlList, err := urlpkg.ListPushed(client, componentConfig.GetName(), componentConfig.GetApplication()) - if err != nil { - return false, nil, err - } - - // If envinfo has URL(s) (since we check) or if the cluster has URL's but - // envinfo does not (deleting) - if len(componentConfig.GetURL()) > 0 || len(componentConfig.GetURL()) == 0 && (len(urlList.Items) > 0) { - pushedURLMap := make(map[string]bool) - for _, element := range urlList.Items { - pushedURLMap[element.Name] = true - } - return true, pushedURLMap, nil - } - } - - return false, nil, nil -} - func addDebugPortToEnv(envVarList *config.EnvVarList, componentConfig config.LocalConfigInfo) { // adding the debug port as an env variable *envVarList = append(*envVarList, config.EnvVar{ diff --git a/pkg/config/fakeConfig.go b/pkg/config/fakeConfig.go index 4d2555af257..87c45413b38 100644 --- a/pkg/config/fakeConfig.go +++ b/pkg/config/fakeConfig.go @@ -13,14 +13,16 @@ func GetOneExistingConfigInfo(componentName, applicationName, projectName string }, } - portsValue := []string{"8080/TCP,45/UDP"} + portsValue := []string{"8080/TCP", "45/UDP"} urlValue := []ConfigURL{ { Name: "example-url-0", + Port: 8080, }, { Name: "example-url-1", + Port: 45, }, } diff --git a/pkg/debug/info.go b/pkg/debug/info.go index b1a1a864236..a114815bb44 100644 --- a/pkg/debug/info.go +++ b/pkg/debug/info.go @@ -18,12 +18,15 @@ import ( type OdoDebugFile struct { metav1.TypeMeta - DebugProcessId int - ProjectName string - AppName string - ComponentName string - RemotePort int - LocalPort int + metav1.ObjectMeta `json:"metadata"` + Spec OdoDebugFileSpec `json:"spec"` +} + +type OdoDebugFileSpec struct { + App string `json:"app"` + DebugProcessID int `json:"debugProcessID"` + RemotePort int `json:"remotePort"` + LocalPort int `json:"localPort"` } // GetDebugInfoFilePath gets the file path of the debug info file @@ -60,12 +63,16 @@ func createDebugInfoFile(f *DefaultPortForwarder, portPair string, fs filesystem Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: f.client.Namespace, - AppName: f.appName, - ComponentName: f.componentName, - RemotePort: remotePort, - LocalPort: localPort, + ObjectMeta: metav1.ObjectMeta{ + Name: f.componentName, + Namespace: f.client.Namespace, + }, + Spec: OdoDebugFileSpec{ + App: f.appName, + DebugProcessID: os.Getpid(), + RemotePort: remotePort, + LocalPort: localPort, + }, } odoDebugPathData, err := json.Marshal(odoDebugFile) if err != nil { @@ -84,6 +91,7 @@ func createDebugInfoFile(f *DefaultPortForwarder, portPair string, fs filesystem return nil } +// GetDebugInfo gathers the information with regards to debugging information func GetDebugInfo(f *DefaultPortForwarder) (OdoDebugFile, bool) { return getDebugInfo(f, filesystem.DefaultFs{}) } @@ -112,9 +120,9 @@ func getDebugInfo(f *DefaultPortForwarder, fs filesystem.Filesystem) (OdoDebugFi // On Unix systems, FindProcess always succeeds and returns a Process for the given pid, regardless of whether the process exists. // thus this step will pass on Unix systems and so for those systems and some others supporting signals // we check if the process is alive or not by sending a signal 0 to the process - processInfo, err := os.FindProcess(odoDebugFileData.DebugProcessId) + processInfo, err := os.FindProcess(odoDebugFileData.Spec.DebugProcessID) if err != nil || processInfo == nil { - glog.V(4).Infof("error getting the process info for pid %v", odoDebugFileData.DebugProcessId) + glog.V(4).Infof("error getting the process info for pid %v", odoDebugFileData.Spec.DebugProcessID) return OdoDebugFile{}, false } @@ -122,17 +130,17 @@ func getDebugInfo(f *DefaultPortForwarder, fs filesystem.Filesystem) (OdoDebugFi if runtime.GOOS != "windows" { err = processInfo.Signal(syscall.Signal(0)) if err != nil { - glog.V(4).Infof("error sending signal 0 to pid %v, cause: %v", odoDebugFileData.DebugProcessId, err) + glog.V(4).Infof("error sending signal 0 to pid %v, cause: %v", odoDebugFileData.Spec.DebugProcessID, err) return OdoDebugFile{}, false } } // gets the debug local port and tries to listen on it // if error doesn't occur the debug port was free and thus no debug process was using the port - addressLook := "localhost:" + strconv.Itoa(odoDebugFileData.LocalPort) + addressLook := "localhost:" + strconv.Itoa(odoDebugFileData.Spec.LocalPort) listener, err := net.Listen("tcp", addressLook) if err == nil { - glog.V(4).Infof("the debug port %v is free, thus debug is not running", odoDebugFileData.LocalPort) + glog.V(4).Infof("the debug port %v is free, thus debug is not running", odoDebugFileData.Spec.LocalPort) err = listener.Close() if err != nil { glog.V(4).Infof("error occurred while closing the listener, cause :%v", err) diff --git a/pkg/debug/info_test.go b/pkg/debug/info_test.go index 3bff0b76b94..7f2dce1b0e0 100644 --- a/pkg/debug/info_test.go +++ b/pkg/debug/info_test.go @@ -14,15 +14,19 @@ import ( ) // fakeOdoDebugFileString creates a json string of a fake OdoDebugFile -func fakeOdoDebugFileString(typeMeta v1.TypeMeta, processId int, projectName, appName, componentName string, remotePort, localPort int) (string, error) { +func fakeOdoDebugFileString(typeMeta v1.TypeMeta, processID int, projectName, appName, componentName string, remotePort, localPort int) (string, error) { odoDebugFile := OdoDebugFile{ - TypeMeta: typeMeta, - DebugProcessId: processId, - ProjectName: projectName, - AppName: appName, - ComponentName: componentName, - RemotePort: remotePort, - LocalPort: localPort, + TypeMeta: typeMeta, + ObjectMeta: v1.ObjectMeta{ + Namespace: projectName, + Name: componentName, + }, + Spec: OdoDebugFileSpec{ + App: appName, + DebugProcessID: processID, + RemotePort: remotePort, + LocalPort: localPort, + }, } data, err := json.Marshal(odoDebugFile) @@ -64,12 +68,16 @@ func Test_createDebugInfoFile(t *testing.T) { Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 9001, - LocalPort: 5858, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid(), + App: "app", + RemotePort: 9001, + LocalPort: 5858, + }, }, alreadyExistFile: false, wantErr: false, @@ -89,12 +97,16 @@ func Test_createDebugInfoFile(t *testing.T) { Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 9004, - LocalPort: 5758, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid(), + App: "app", + RemotePort: 9004, + LocalPort: 5758, + }, }, alreadyExistFile: true, wantErr: false, @@ -173,22 +185,32 @@ func Test_getDebugInfo(t *testing.T) { Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 5858, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid(), + App: "app", + RemotePort: 5858, + LocalPort: 9001, + }, }, readDebugFile: OdoDebugFile{ TypeMeta: v1.TypeMeta{ Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 5858, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid(), + App: "app", + RemotePort: 5858, + LocalPort: 9001, + }, }, debugPortListening: true, fileExists: true, @@ -225,11 +247,16 @@ func Test_getDebugInfo(t *testing.T) { Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid(), - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 5858, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid(), + App: "app", + RemotePort: 5858, + LocalPort: 9001, + }, }, fileExists: true, debugRunning: false, @@ -250,11 +277,16 @@ func Test_getDebugInfo(t *testing.T) { Kind: "OdoDebugInfo", APIVersion: "v1", }, - DebugProcessId: os.Getpid() + 818177979, - ProjectName: "testing-1", - AppName: "app", - ComponentName: "nodejs-ex", - RemotePort: 5858, + ObjectMeta: v1.ObjectMeta{ + Name: "nodejs-ex", + Namespace: "testing-1", + }, + Spec: OdoDebugFileSpec{ + DebugProcessID: os.Getpid() + 818177979, + App: "app", + RemotePort: 5858, + LocalPort: 9001, + }, }, fileExists: true, debugRunning: false, @@ -273,23 +305,23 @@ func Test_getDebugInfo(t *testing.T) { t.Errorf("error occured while getting a free port, cause: %v", err) } - if (OdoDebugFile{}) != tt.readDebugFile { - tt.readDebugFile.LocalPort = freePort + if tt.readDebugFile.Spec.LocalPort != 0 { + tt.readDebugFile.Spec.LocalPort = freePort } - if (OdoDebugFile{}) != tt.wantDebugFile { - tt.wantDebugFile.LocalPort = freePort + if tt.wantDebugFile.Spec.LocalPort != 0 { + tt.wantDebugFile.Spec.LocalPort = freePort } odoDebugFilePath := GetDebugInfoFilePath(tt.args.defaultPortForwarder.client, tt.args.defaultPortForwarder.componentName, tt.args.defaultPortForwarder.appName) if tt.fileExists { fakeString, err := fakeOdoDebugFileString(tt.readDebugFile.TypeMeta, - tt.readDebugFile.DebugProcessId, - tt.readDebugFile.ProjectName, - tt.readDebugFile.AppName, - tt.readDebugFile.ComponentName, - tt.readDebugFile.RemotePort, - tt.readDebugFile.LocalPort) + tt.readDebugFile.Spec.DebugProcessID, + tt.readDebugFile.ObjectMeta.Namespace, + tt.readDebugFile.Spec.App, + tt.readDebugFile.ObjectMeta.Name, + tt.readDebugFile.Spec.RemotePort, + tt.readDebugFile.Spec.LocalPort) if err != nil { t.Errorf("error occured while getting odo debug file string, cause: %v", err) @@ -306,7 +338,7 @@ func Test_getDebugInfo(t *testing.T) { if tt.debugPortListening { startListenerChan := make(chan bool) go func() { - err := testingutil.FakePortListener(startListenerChan, stopListenerChan, tt.readDebugFile.LocalPort) + err := testingutil.FakePortListener(startListenerChan, stopListenerChan, tt.readDebugFile.Spec.LocalPort) if err != nil { // the fake listener failed, show error and close the channel t.Errorf("error while starting fake port listerner, cause: %v", err) diff --git a/pkg/devfile/adapters/common/command.go b/pkg/devfile/adapters/common/command.go index 6f89574d0a3..e7a5b8491f6 100644 --- a/pkg/devfile/adapters/common/command.go +++ b/pkg/devfile/adapters/common/command.go @@ -29,6 +29,7 @@ func getCommand(data data.DevfileData, commandName string, required bool) (suppo // The command is supported, use it supportedCommand.Name = command.Name supportedCommand.Actions = supportedCommandActions + supportedCommand.Attributes = command.Attributes return supportedCommand, nil } } @@ -107,44 +108,63 @@ func validateAction(data data.DevfileData, action common.DevfileCommandAction) ( return } +// GetInitCommand iterates through the components in the devfile and returns the init command +func GetInitCommand(data data.DevfileData, devfileInitCmd string) (initCommand common.DevfileCommand, err error) { + if devfileInitCmd != "" { + // a init command was specified so if it is not found then it is an error + return getCommand(data, devfileInitCmd, true) + } + // a init command was not specified so if it is not found then it is not an error + return getCommand(data, string(DefaultDevfileInitCommand), false) +} + // GetBuildCommand iterates through the components in the devfile and returns the build command func GetBuildCommand(data data.DevfileData, devfileBuildCmd string) (buildCommand common.DevfileCommand, err error) { if devfileBuildCmd != "" { // a build command was specified so if it is not found then it is an error - buildCommand, err = getCommand(data, devfileBuildCmd, true) - } else { - // a build command was not specified so if it is not found then it is not an error - buildCommand, err = getCommand(data, string(DefaultDevfileBuildCommand), false) + return getCommand(data, devfileBuildCmd, true) } - - return + // a build command was not specified so if it is not found then it is not an error + return getCommand(data, string(DefaultDevfileBuildCommand), false) } // GetRunCommand iterates through the components in the devfile and returns the run command func GetRunCommand(data data.DevfileData, devfileRunCmd string) (runCommand common.DevfileCommand, err error) { if devfileRunCmd != "" { - runCommand, err = getCommand(data, devfileRunCmd, true) - } else { - runCommand, err = getCommand(data, string(DefaultDevfileRunCommand), true) + return getCommand(data, devfileRunCmd, true) } - - return + return getCommand(data, string(DefaultDevfileRunCommand), true) } // ValidateAndGetPushDevfileCommands validates the build and the run command, // if provided through odo push or else checks the devfile for devBuild and devRun. // It returns the build and run commands if its validated successfully, error otherwise. -func ValidateAndGetPushDevfileCommands(data data.DevfileData, devfileBuildCmd, devfileRunCmd string) (pushDevfileCommands []common.DevfileCommand, err error) { +func ValidateAndGetPushDevfileCommands(data data.DevfileData, devfileInitCmd, devfileBuildCmd, devfileRunCmd string) (pushDevfileCommands []common.DevfileCommand, err error) { var emptyCommand common.DevfileCommand - isBuildCommandValid, isRunCommandValid := false, false + isInitCommandValid, isBuildCommandValid, isRunCommandValid := false, false, false + + initCommand, initCmdErr := GetInitCommand(data, devfileInitCmd) + + isInitCmdEmpty := reflect.DeepEqual(emptyCommand, initCommand) + if isInitCmdEmpty && initCmdErr == nil { + // If there was no init command specified through odo push and no default init command in the devfile, default validate to true since the init command is optional + isInitCommandValid = true + glog.V(3).Infof("No init command was provided") + } else if !isInitCmdEmpty && initCmdErr == nil { + isInitCommandValid = true + pushDevfileCommands = append(pushDevfileCommands, initCommand) + glog.V(3).Infof("Init command: %v", initCommand.Name) + } buildCommand, buildCmdErr := GetBuildCommand(data, devfileBuildCmd) - if reflect.DeepEqual(emptyCommand, buildCommand) && buildCmdErr == nil { + isBuildCmdEmpty := reflect.DeepEqual(emptyCommand, buildCommand) + if isBuildCmdEmpty && buildCmdErr == nil { // If there was no build command specified through odo push and no default build command in the devfile, default validate to true since the build command is optional isBuildCommandValid = true glog.V(3).Infof("No build command was provided") - } else if !reflect.DeepEqual(emptyCommand, buildCommand) && buildCmdErr == nil { + + } else if !isBuildCmdEmpty && buildCmdErr == nil { isBuildCommandValid = true pushDevfileCommands = append(pushDevfileCommands, buildCommand) glog.V(3).Infof("Build command: %v", buildCommand.Name) @@ -158,13 +178,16 @@ func ValidateAndGetPushDevfileCommands(data data.DevfileData, devfileBuildCmd, d } // If either command had a problem, return an empty list of commands and an error - if !isBuildCommandValid || !isRunCommandValid { + if !isInitCommandValid || !isBuildCommandValid || !isRunCommandValid { commandErrors := "" + if initCmdErr != nil { + commandErrors += fmt.Sprintf(initCmdErr.Error(), "\n") + } if buildCmdErr != nil { - commandErrors += buildCmdErr.Error() + commandErrors += fmt.Sprintf(buildCmdErr.Error(), "\n") } if runCmdErr != nil { - commandErrors += runCmdErr.Error() + commandErrors += fmt.Sprintf(runCmdErr.Error(), "\n") } return []common.DevfileCommand{}, fmt.Errorf(commandErrors) } diff --git a/pkg/devfile/adapters/common/command_test.go b/pkg/devfile/adapters/common/command_test.go index 00e4d38657d..c6efc7052f3 100644 --- a/pkg/devfile/adapters/common/command_test.go +++ b/pkg/devfile/adapters/common/command_test.go @@ -6,6 +6,7 @@ import ( devfileParser "github.com/openshift/odo/pkg/devfile/parser" "github.com/openshift/odo/pkg/devfile/parser/data/common" + versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common" "github.com/openshift/odo/pkg/testingutil" ) @@ -27,7 +28,7 @@ func TestGetCommand(t *testing.T) { wantErr bool }{ { - name: "Case: Valid devfile", + name: "Case 1: Valid devfile", requestedCommands: []string{"devbuild", "devrun"}, commandActions: []common.DevfileCommandAction{ { @@ -37,11 +38,39 @@ func TestGetCommand(t *testing.T) { Type: &validCommandType, }, }, - isCommandRequired: []bool{false, true}, + isCommandRequired: []bool{false, false, true}, wantErr: false, }, { - name: "Case: Wrong command requested", + name: "Case 2: Valid devfile with devinit and devbuild", + requestedCommands: []string{"devinit", "devbuild", "devrun"}, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &commands[0], + Component: &components[0], + Workdir: &workDir[0], + Type: &validCommandType, + }, + }, + isCommandRequired: []bool{false, false, true}, + wantErr: false, + }, + { + name: "Case 3: Valid devfile with devinit and devrun", + requestedCommands: []string{"devinit", "devrun"}, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &commands[0], + Component: &components[0], + Workdir: &workDir[0], + Type: &validCommandType, + }, + }, + isCommandRequired: []bool{false, false, true}, + wantErr: false, + }, + { + name: "Case 4: Wrong command requested", requestedCommands: []string{"garbage1"}, commandActions: []common.DevfileCommandAction{ { @@ -55,7 +84,49 @@ func TestGetCommand(t *testing.T) { wantErr: true, }, { - name: "Case: Invalid devfile with wrong command type", + name: "Case 5: Invalid devfile with wrong devinit command type", + requestedCommands: []string{"devinit"}, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &commands[0], + Component: &components[0], + Workdir: &workDir[0], + Type: &invalidCommandType, + }, + }, + isCommandRequired: []bool{true}, + wantErr: true, + }, + { + name: "Case 6: Invalid devfile with empty devinit component", + requestedCommands: []string{"devinit"}, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &commands[0], + Component: &emptyString, + Workdir: &workDir[0], + Type: &validCommandType, + }, + }, + isCommandRequired: []bool{false}, + wantErr: true, + }, + { + name: "Case 7: Invalid devfile with empty devinit command", + requestedCommands: []string{"devinit"}, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &emptyString, + Component: &components[0], + Workdir: &workDir[0], + Type: &validCommandType, + }, + }, + isCommandRequired: []bool{false}, + wantErr: true, + }, + { + name: "Case 8: Invalid devfile with wrong devbuild command type", requestedCommands: []string{"devbuild"}, commandActions: []common.DevfileCommandAction{ { @@ -69,7 +140,7 @@ func TestGetCommand(t *testing.T) { wantErr: true, }, { - name: "Case: Invalid devfile with empty component", + name: "Case 9: Invalid devfile with empty devbuild component", requestedCommands: []string{"devbuild"}, commandActions: []common.DevfileCommandAction{ { @@ -83,7 +154,7 @@ func TestGetCommand(t *testing.T) { wantErr: true, }, { - name: "Case: Invalid devfile with empty command", + name: "Case 10: Invalid devfile with empty devbuild command", requestedCommands: []string{"devbuild"}, commandActions: []common.DevfileCommandAction{ { @@ -97,7 +168,7 @@ func TestGetCommand(t *testing.T) { wantErr: true, }, { - name: "Case: Valid devfile with empty workdir", + name: "Case 11: Valid devfile with empty workdir", requestedCommands: []string{"devrun"}, commandActions: []common.DevfileCommandAction{ { @@ -110,7 +181,7 @@ func TestGetCommand(t *testing.T) { wantErr: false, }, { - name: "Case: Invalid command referencing an absent component", + name: "Case 12: Invalid command referencing an absent component", requestedCommands: []string{"devrun"}, commandActions: []common.DevfileCommandAction{ { @@ -331,6 +402,85 @@ func TestValidateAction(t *testing.T) { } +func TestGetInitCommand(t *testing.T) { + + command := "ls -la" + component := "alias1" + workDir := "/" + validCommandType := common.DevfileCommandTypeExec + emptyString := "" + + var emptyCommand common.DevfileCommand + + tests := []struct { + name string + commandName string + commandActions []common.DevfileCommandAction + wantErr bool + }{ + { + name: "Case: Default Init Command", + commandName: emptyString, + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: false, + }, + { + name: "Case: Custom Init Command", + commandName: "customcommand", + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: false, + }, + { + name: "Case: Missing Init Command", + commandName: "customcommand123", + commandActions: []versionsCommon.DevfileCommandAction{ + { + Command: &command, + Component: &component, + Workdir: &workDir, + Type: &validCommandType, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + devObj := devfileParser.DevfileObj{ + Data: testingutil.TestDevfileData{ + CommandActions: tt.commandActions, + ComponentType: versionsCommon.DevfileComponentTypeDockerimage, + }, + } + + command, err := GetInitCommand(devObj.Data, tt.commandName) + + if !tt.wantErr == (err != nil) { + t.Errorf("TestGetInitCommand: unexpected error for command \"%v\" expected: %v actual: %v", tt.commandName, tt.wantErr, err) + } else if !tt.wantErr && reflect.DeepEqual(emptyCommand, command) { + t.Errorf("TestGetInitCommand: unexpected empty command returned for command: %v", tt.commandName) + } + + }) + } + +} + func TestGetBuildCommand(t *testing.T) { command := "ls -la" @@ -507,39 +657,36 @@ func TestValidateAndGetPushDevfileCommands(t *testing.T) { tests := []struct { name string + initCommand string buildCommand string runCommand string numberOfCommands int componentType common.DevfileComponentType + missingInitCommand bool missingBuildCommand bool wantErr bool }{ { name: "Case: Default Devfile Commands", + initCommand: emptyString, buildCommand: emptyString, runCommand: emptyString, - numberOfCommands: 2, + numberOfCommands: 3, componentType: common.DevfileComponentTypeDockerimage, wantErr: false, }, { - name: "Case: Default Build Command and Provided Run Command", + name: "Case: Default Init and Build Command, and Provided Run Command", + initCommand: emptyString, buildCommand: emptyString, runCommand: "customcommand", - numberOfCommands: 2, - componentType: common.DevfileComponentTypeDockerimage, - wantErr: false, - }, - { - name: "Case: Provided Build Command and Provided Run Command", - buildCommand: "customcommand", - runCommand: "customcommand", - numberOfCommands: 2, + numberOfCommands: 3, componentType: common.DevfileComponentTypeDockerimage, wantErr: false, }, { name: "Case: No Dockerimage Component", + initCommand: emptyString, buildCommand: "customcommand", runCommand: "customcommand", numberOfCommands: 0, @@ -548,6 +695,7 @@ func TestValidateAndGetPushDevfileCommands(t *testing.T) { }, { name: "Case: Provided Wrong Build Command and Provided Run Command", + initCommand: emptyString, buildCommand: "customcommand123", runCommand: "customcommand", numberOfCommands: 1, @@ -555,14 +703,54 @@ func TestValidateAndGetPushDevfileCommands(t *testing.T) { wantErr: true, }, { - name: "Case: Missing Build Command and Provided Run Command", + name: "Case: Provided Wrong Init Command and Provided Build and Run Command", + initCommand: "customcommand123", + buildCommand: emptyString, + runCommand: "customcommand", + numberOfCommands: 1, + componentType: versionsCommon.DevfileComponentTypeDockerimage, + wantErr: true, + }, + { + name: "Case: Missing Init and Build Command, and Provided Run Command", + initCommand: emptyString, buildCommand: emptyString, runCommand: "customcommand", numberOfCommands: 1, componentType: common.DevfileComponentTypeDockerimage, + missingInitCommand: true, + missingBuildCommand: true, + wantErr: false, + }, + { + name: "Case: Missing Init Command with provided Build and Run Command", + initCommand: emptyString, + buildCommand: "customcommand", + runCommand: "customcommand", + numberOfCommands: 2, + componentType: versionsCommon.DevfileComponentTypeDockerimage, + missingInitCommand: true, + wantErr: false, + }, + { + name: "Case: Missing Build Command with provided Init and Run Command", + initCommand: "customcommand", + buildCommand: emptyString, + runCommand: "customcommand", + numberOfCommands: 2, + componentType: versionsCommon.DevfileComponentTypeDockerimage, missingBuildCommand: true, wantErr: false, }, + { + name: "Case: Optional Init Command with provided Build and Run Command", + initCommand: "customcommand", + buildCommand: "customcommand", + runCommand: "customcommand", + numberOfCommands: 3, + componentType: versionsCommon.DevfileComponentTypeDockerimage, + wantErr: false, + }, } for _, tt := range tests { @@ -571,11 +759,12 @@ func TestValidateAndGetPushDevfileCommands(t *testing.T) { Data: testingutil.TestDevfileData{ CommandActions: actions, ComponentType: tt.componentType, + MissingInitCommand: tt.missingInitCommand, MissingBuildCommand: tt.missingBuildCommand, }, } - pushCommands, err := ValidateAndGetPushDevfileCommands(devObj.Data, tt.buildCommand, tt.runCommand) + pushCommands, err := ValidateAndGetPushDevfileCommands(devObj.Data, tt.initCommand, tt.buildCommand, tt.runCommand) if !tt.wantErr == (err != nil) { t.Errorf("TestValidateAndGetPushDevfileCommands unexpected error when validating commands wantErr: %v err: %v", tt.wantErr, err) } else if tt.wantErr && err != nil { diff --git a/pkg/devfile/adapters/common/types.go b/pkg/devfile/adapters/common/types.go index 3c947bd4580..8b95ee53dea 100644 --- a/pkg/devfile/adapters/common/types.go +++ b/pkg/devfile/adapters/common/types.go @@ -32,6 +32,7 @@ type PushParameters struct { IgnoredFiles []string // IgnoredFiles is the list of files to not push up to a component ForceBuild bool // ForceBuild determines whether or not to push all of the files up to a component or just files that have changed, added or removed. Show bool // Show tells whether the devfile command output should be shown on stdout + DevfileInitCmd string // DevfileInitCmd takes the init command through the command line and overwrites devfile init command DevfileBuildCmd string // DevfileBuildCmd takes the build command through the command line and overwrites devfile build command DevfileRunCmd string // DevfileRunCmd takes the run command through the command line and overwrites devfile run command EnvSpecificInfo envinfo.EnvSpecificInfo // EnvSpecificInfo contains infomation of env.yaml file diff --git a/pkg/devfile/adapters/common/utils.go b/pkg/devfile/adapters/common/utils.go index b9791a384e5..96204b1aba2 100644 --- a/pkg/devfile/adapters/common/utils.go +++ b/pkg/devfile/adapters/common/utils.go @@ -1,8 +1,8 @@ package common import ( - "fmt" "os" + "strconv" "github.com/golang/glog" @@ -15,6 +15,9 @@ import ( type PredefinedDevfileCommands string const ( + // DefaultDevfileInitCommand is a predefined devfile command for init + DefaultDevfileInitCommand PredefinedDevfileCommands = "devinit" + // DefaultDevfileBuildCommand is a predefined devfile command for build DefaultDevfileBuildCommand PredefinedDevfileCommands = "devbuild" @@ -28,6 +31,9 @@ const ( // use GetBootstrapperImage() function instead of this variable defaultBootstrapperImage = "registry.access.redhat.com/openshiftdo/odo-init-image-rhel7:1.1.2" + // SupervisordControlCommand sub command which stands for control + SupervisordControlCommand = "ctl" + // SupervisordVolumeName Create a custom name and (hope) that users don't use the *exact* same name in their deployment (occlient.go) SupervisordVolumeName = "odo-supervisord-shared-data" @@ -46,6 +52,9 @@ const ( // ENV variable to overwrite image used to bootstrap SupervisorD in S2I and Devfile builder Image bootstrapperImageEnvName = "ODO_BOOTSTRAPPER_IMAGE" + // BinBash The path to sh executable + BinBash = "/bin/sh" + // Default volume size for volumes defined in a devfile volumeSize = "5Gi" @@ -65,6 +74,12 @@ const ( SupervisordCtlSubCommand = "ctl" ) +// CommandNames is a struct to store the default and adapter names for devfile commands +type CommandNames struct { + DefaultName string + AdapterName string +} + func isComponentSupported(component common.DevfileComponent) bool { // Currently odo only uses devfile components of type dockerimage, since most of the Che registry devfiles use it return component.Type == common.DevfileComponentTypeDockerimage @@ -133,20 +148,18 @@ func IsPortPresent(endpoints []common.DockerimageEndpoint, port int) bool { return false } -// IsComponentBuildRequired checks if a component build is required based on the push commands, it throws an error -// if the push commands does not meet the expected criteria -func IsComponentBuildRequired(pushDevfileCommands []common.DevfileCommand) (bool, error) { - var buildRequired bool - - switch len(pushDevfileCommands) { - case 1: // if there is one command, it is the mandatory run command. No need to build. - buildRequired = false - case 2: - // if there are two commands, it is the optional build command and the mandatory run command, set buildRequired to true - buildRequired = true - default: - return false, fmt.Errorf("error executing devfile commands - there should be at least 1 command or at most 2 commands, currently there are %d commands", len(pushDevfileCommands)) +// IsRestartRequired returns if restart is required for devrun command +func IsRestartRequired(command common.DevfileCommand) bool { + var restart = true + var err error + rs, ok := command.Attributes["restart"] + if ok { + restart, err = strconv.ParseBool(rs) + // Ignoring error here as restart is true for all error and default cases. + if err != nil { + glog.V(4).Info("Error converting restart attribute to bool") + } } - return buildRequired, nil + return restart } diff --git a/pkg/devfile/adapters/docker/component/adapter.go b/pkg/devfile/adapters/docker/component/adapter.go index 2d81527005c..4eb7bb71182 100644 --- a/pkg/devfile/adapters/docker/component/adapter.go +++ b/pkg/devfile/adapters/docker/component/adapter.go @@ -32,6 +32,7 @@ type Adapter struct { componentAliasToVolumes map[string][]common.DevfileVolume uniqueStorage []common.Storage volumeNameToDockerVolName map[string]string + devfileInitCmd string devfileBuildCmd string devfileRunCmd string supervisordVolumeName string @@ -53,7 +54,7 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { // Validate the devfile build and run commands log.Info("\nValidation") s := log.Spinner("Validating the devfile") - pushDevfileCommands, err := common.ValidateAndGetPushDevfileCommands(a.Devfile.Data, a.devfileBuildCmd, a.devfileRunCmd) + pushDevfileCommands, err := common.ValidateAndGetPushDevfileCommands(a.Devfile.Data, a.devfileInitCmd, a.devfileBuildCmd, a.devfileRunCmd) if err != nil { s.End(false) return errors.Wrap(err, "failed to validate devfile build and run commands") diff --git a/pkg/devfile/adapters/docker/component/utils.go b/pkg/devfile/adapters/docker/component/utils.go index d66781507dd..c9893df1d9b 100644 --- a/pkg/devfile/adapters/docker/component/utils.go +++ b/pkg/devfile/adapters/docker/component/utils.go @@ -324,62 +324,83 @@ func getPortMap(endpoints []versionsCommon.DockerimageEndpoint, show bool) (nat. return portmap, nil } -// Push syncs source code from the user's disk to the component +// Executes all the commands from the devfile in order: init and build - which are both optional, and a compulsary run. +// Init only runs once when the component is created. func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand, componentExists, show bool, containers []types.Container) (err error) { - buildRequired, err := common.IsComponentBuildRequired(pushDevfileCommands) - if err != nil { - return err + // If nothing has been passed, then the devfile is missing the required run command + if len(pushDevfileCommands) == 0 { + return errors.New(fmt.Sprint("error executing devfile commands - there should be at least 1 command")) } - for i := 0; i < len(pushDevfileCommands); i++ { - command := pushDevfileCommands[i] + commandOrder := []common.CommandNames{} - // Exec the devBuild command if buildRequired is true - if (command.Name == string(common.DefaultDevfileBuildCommand) || command.Name == a.devfileBuildCmd) && buildRequired { - glog.V(3).Infof("Executing devfile command %v", command.Name) + // Only add runinit to the expected commands if the component doesn't already exist + // This would be the case when first running the container + if !componentExists { + commandOrder = append(commandOrder, common.CommandNames{DefaultName: string(common.DefaultDevfileInitCommand), AdapterName: a.devfileInitCmd}) + } + commandOrder = append( + commandOrder, + common.CommandNames{DefaultName: string(common.DefaultDevfileBuildCommand), AdapterName: a.devfileBuildCmd}, + common.CommandNames{DefaultName: string(common.DefaultDevfileRunCommand), AdapterName: a.devfileRunCmd}, + ) + + // Loop through each of the expected commands in the devfile + for i, currentCommand := range commandOrder { + // Loop through each of the command given from the devfile + for _, command := range pushDevfileCommands { + // If the current command from the devfile is the currently expected command from the devfile + if command.Name == currentCommand.DefaultName || command.Name == currentCommand.AdapterName { + // If the current command is not the last command in the slice + // it is not expected to be the run command + if i < len(commandOrder)-1 { + // Any exec command such as "Init" and "Build" + + for _, action := range command.Actions { + containerID := utils.GetContainerIDForAlias(containers, *action.Component) + compInfo := common.ComponentInfo{ + ContainerName: containerID, + } + + err = exec.ExecuteDevfileBuildAction(&a.Client, action, command.Name, compInfo, show) + if err != nil { + return err + } + } - for _, action := range command.Actions { - // Get the containerID - containerID := utils.GetContainerIDForAlias(containers, *action.Component) - compInfo := common.ComponentInfo{ - ContainerName: containerID, - } + // If the current command is the last command in the slice + // it is expected to be the run command + } else { + // Last command is "Run" + glog.V(4).Infof("Executing devfile command %v", command.Name) - err = exec.ExecuteDevfileBuildAction(&a.Client, action, command.Name, compInfo, show) - if err != nil { - return err - } - } + for _, action := range command.Actions { - // Reset the for loop counter and iterate through all the devfile commands again for others - i = -1 - // Set the buildRequired to false since we already executed the build command - buildRequired = false - } else if (command.Name == string(common.DefaultDevfileRunCommand) || command.Name == a.devfileRunCmd) && !buildRequired { - // Always check for buildRequired is false, since the command may be iterated out of order and we always want to execute devBuild first if buildRequired is true. If buildRequired is false, then we don't need to build and we can execute the devRun command - glog.V(3).Infof("Executing devfile command %v", command.Name) + // Check if the devfile run component containers have supervisord as the entrypoint. + // Start the supervisord if the odo component does not exist + if !componentExists { + err = a.InitRunContainerSupervisord(*action.Component, containers) + if err != nil { + return + } + } - for _, action := range command.Actions { + containerID := utils.GetContainerIDForAlias(containers, *action.Component) + compInfo := common.ComponentInfo{ + ContainerName: containerID, + } - // Get the containerID - containerID := utils.GetContainerIDForAlias(containers, *action.Component) - compInfo := common.ComponentInfo{ - ContainerName: containerID, - } + if componentExists && !common.IsRestartRequired(command) { + glog.V(4).Info("restart:false, Not restarting DevRun Command") + err = exec.ExecuteDevfileRunActionWithoutRestart(&a.Client, action, command.Name, compInfo, show) + return + } + + err = exec.ExecuteDevfileRunAction(&a.Client, action, command.Name, compInfo, show) - // Check if the devfile run component containers have supervisord as the entrypoint. - // Start the supervisord if the odo component does not exist - if !componentExists { - err = a.InitRunContainerSupervisord(*action.Component, containers) - if err != nil { - return } } - err = exec.ExecuteDevfileRunAction(&a.Client, action, command.Name, compInfo, show) - if err != nil { - return err - } } } } diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index cd72ac162db..99d73387f08 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -37,6 +37,7 @@ func New(adapterContext common.AdapterContext, client kclient.Client) Adapter { type Adapter struct { Client kclient.Client common.AdapterContext + devfileInitCmd string devfileBuildCmd string devfileRunCmd string } @@ -46,6 +47,7 @@ type Adapter struct { func (a Adapter) Push(parameters common.PushParameters) (err error) { componentExists := utils.ComponentExists(a.Client, a.ComponentName) + a.devfileInitCmd = parameters.DevfileInitCmd a.devfileBuildCmd = parameters.DevfileBuildCmd a.devfileRunCmd = parameters.DevfileRunCmd @@ -64,7 +66,7 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { // Validate the devfile build and run commands log.Info("\nValidation") s := log.Spinner("Validating the devfile") - pushDevfileCommands, err := common.ValidateAndGetPushDevfileCommands(a.Devfile.Data, a.devfileBuildCmd, a.devfileRunCmd) + pushDevfileCommands, err := common.ValidateAndGetPushDevfileCommands(a.Devfile.Data, a.devfileInitCmd, a.devfileBuildCmd, a.devfileRunCmd) if err != nil { s.End(false) return errors.Wrap(err, "failed to validate devfile build and run commands") @@ -301,59 +303,80 @@ func (a Adapter) waitAndGetComponentPod(hideSpinner bool) (*corev1.Pod, error) { return pod, nil } -// Push syncs source code from the user's disk to the component +// Executes all the commands from the devfile in order: init and build - which are both optional, and a compulsary run. +// Init only runs once when the component is created. func (a Adapter) execDevfile(pushDevfileCommands []versionsCommon.DevfileCommand, componentExists, show bool, podName string, containers []corev1.Container) (err error) { - buildRequired, err := common.IsComponentBuildRequired(pushDevfileCommands) - if err != nil { - return err + // If nothing has been passed, then the devfile is missing the required run command + if len(pushDevfileCommands) == 0 { + return errors.New(fmt.Sprint("error executing devfile commands - there should be at least 1 command")) } - for i := 0; i < len(pushDevfileCommands); i++ { - command := pushDevfileCommands[i] - - // Exec the devBuild command if buildRequired is true - if (command.Name == string(common.DefaultDevfileBuildCommand) || command.Name == a.devfileBuildCmd) && buildRequired { - glog.V(3).Infof("Executing devfile command %v", command.Name) - - for _, action := range command.Actions { - compInfo := common.ComponentInfo{ - ContainerName: *action.Component, - PodName: podName, - } + commandOrder := []common.CommandNames{} - err = exec.ExecuteDevfileBuildAction(&a.Client, action, command.Name, compInfo, show) - if err != nil { - return err - } - } - - // Reset the for loop counter and iterate through all the devfile commands again for others - i = -1 - // Set the buildRequired to false since we already executed the build command - buildRequired = false - } else if (command.Name == string(common.DefaultDevfileRunCommand) || command.Name == a.devfileRunCmd) && !buildRequired { - // Always check for buildRequired is false, since the command may be iterated out of order and we always want to execute devBuild first if buildRequired is true. If buildRequired is false, then we don't need to build and we can execute the devRun command - glog.V(3).Infof("Executing devfile command %v", command.Name) - - for _, action := range command.Actions { - - // Check if the devfile run component containers have supervisord as the entrypoint. - // Start the supervisord if the odo component does not exist - if !componentExists { - err = a.InitRunContainerSupervisord(*action.Component, podName, containers) - if err != nil { - return + // Only add runinit to the expected commands if the component doesn't already exist + // This would be the case when first running the container + if !componentExists { + commandOrder = append(commandOrder, common.CommandNames{DefaultName: string(common.DefaultDevfileInitCommand), AdapterName: a.devfileInitCmd}) + } + commandOrder = append( + commandOrder, + common.CommandNames{DefaultName: string(common.DefaultDevfileBuildCommand), AdapterName: a.devfileBuildCmd}, + common.CommandNames{DefaultName: string(common.DefaultDevfileRunCommand), AdapterName: a.devfileRunCmd}, + ) + + // Loop through each of the expected commands in the devfile + for i, currentCommand := range commandOrder { + // Loop through each of the command given from the devfile + for _, command := range pushDevfileCommands { + // If the current command from the devfile is the currently expected command from the devfile + if command.Name == currentCommand.DefaultName || command.Name == currentCommand.AdapterName { + // If the current command is not the last command in the slice + // it is not expected to be the run command + if i < len(commandOrder)-1 { + // Any exec command such as "Init" and "Build" + + for _, action := range command.Actions { + compInfo := common.ComponentInfo{ + ContainerName: *action.Component, + PodName: podName, + } + + err = exec.ExecuteDevfileBuildAction(&a.Client, action, command.Name, compInfo, show) + if err != nil { + return err + } } - } - - compInfo := common.ComponentInfo{ - ContainerName: *action.Component, - PodName: podName, - } - err = exec.ExecuteDevfileRunAction(&a.Client, action, command.Name, compInfo, show) - if err != nil { - return err + // If the current command is the last command in the slice + // it is expected to be the run command + } else { + // Last command is "Run" + glog.V(4).Infof("Executing devfile command %v", command.Name) + + for _, action := range command.Actions { + + // Check if the devfile run component containers have supervisord as the entrypoint. + // Start the supervisord if the odo component does not exist + if !componentExists { + err = a.InitRunContainerSupervisord(*action.Component, podName, containers) + if err != nil { + return + } + } + + compInfo := common.ComponentInfo{ + ContainerName: *action.Component, + PodName: podName, + } + + if componentExists && !common.IsRestartRequired(command) { + glog.V(4).Infof("restart:false, Not restarting DevRun Command") + err = exec.ExecuteDevfileRunActionWithoutRestart(&a.Client, action, command.Name, compInfo, show) + return + } + + err = exec.ExecuteDevfileRunAction(&a.Client, action, command.Name, compInfo, show) + } } } } diff --git a/pkg/devfile/parser/data/common/types.go b/pkg/devfile/parser/data/common/types.go index c455dff5a5f..6c21062cb94 100644 --- a/pkg/devfile/parser/data/common/types.go +++ b/pkg/devfile/parser/data/common/types.go @@ -26,6 +26,7 @@ const ( type DevfileCommandType string const ( + DevfileCommandTypeInit DevfileCommandType = "init" DevfileCommandTypeBuild DevfileCommandType = "build" DevfileCommandTypeRun DevfileCommandType = "run" DevfileCommandTypeDebug DevfileCommandType = "debug" diff --git a/pkg/envinfo/envinfo.go b/pkg/envinfo/envinfo.go index 824c04bbe53..9663a738b30 100644 --- a/pkg/envinfo/envinfo.go +++ b/pkg/envinfo/envinfo.go @@ -26,6 +26,14 @@ type ComponentSettings struct { URL *[]EnvInfoURL `yaml:"Url,omitempty"` } +// URLKind is an enum to indicate the type of the URL i.e ingress/route +type URLKind string + +const ( + INGRESS URLKind = "ingress" + ROUTE URLKind = "route" +) + // EnvInfoURL holds URL related information type EnvInfoURL struct { // Name of the URL @@ -34,12 +42,14 @@ type EnvInfoURL struct { Port int `yaml:"Port,omitempty"` // Indicates if the URL should be a secure https one Secure bool `yaml:"Secure,omitempty"` - // Clutser host + // Cluster host Host string `yaml:"host,omitempty"` // TLS secret name to create ingress to provide a secure URL TLSSecret string `yaml:"TLSSecret,omitempty"` // Exposed port number for docker container, required for local scenarios ExposedPort int `yaml:"ExposedPort,omitempty"` + // Kind is the kind of the URL + Kind URLKind `yaml:"Kind,omitempty"` } // EnvInfo holds all the env specific infomation relavent to a specific Component. diff --git a/pkg/exec/devfile.go b/pkg/exec/devfile.go index bb202a38aa1..9116e8b76a4 100644 --- a/pkg/exec/devfile.go +++ b/pkg/exec/devfile.go @@ -70,3 +70,29 @@ func ExecuteDevfileRunAction(client ExecClient, action common.DevfileCommandActi return nil } + +// ExecuteDevfileRunActionWithoutRestart executes devfile run command without restarting. +func ExecuteDevfileRunActionWithoutRestart(client ExecClient, action common.DevfileCommandAction, commandName string, compInfo adaptersCommon.ComponentInfo, show bool) error { + var s *log.Status + + type devRunExecutable struct { + command []string + } + // with restart false, executing only supervisord start command, if the command is already running, supvervisord will not restart it. + // if the command is failed or not running suprvisord would start it. + devRunExec := devRunExecutable{ + command: []string{adaptersCommon.SupervisordBinaryPath, adaptersCommon.SupervisordCtlSubCommand, "start", string(adaptersCommon.DefaultDevfileRunCommand)}, + } + + s = log.Spinnerf("Executing %s command %q, if not running", commandName, *action.Command) + defer s.End(false) + + err := ExecuteCommand(client, compInfo, devRunExec.command, show) + if err != nil { + return errors.Wrapf(err, "unable to execute the run command") + } + + s.End(true) + + return nil +} diff --git a/pkg/kclient/fake/ingress.go b/pkg/kclient/fake/ingress.go new file mode 100644 index 00000000000..e525119954c --- /dev/null +++ b/pkg/kclient/fake/ingress.go @@ -0,0 +1,62 @@ +package fake + +import ( + applabels "github.com/openshift/odo/pkg/application/labels" + componentlabels "github.com/openshift/odo/pkg/component/labels" + "github.com/openshift/odo/pkg/kclient" + "github.com/openshift/odo/pkg/url/labels" + "github.com/openshift/odo/pkg/version" + extensionsv1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func GetIngressListWithMultiple(componentName string) *extensionsv1.IngressList { + return &extensionsv1.IngressList{ + Items: []extensionsv1.Ingress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-0", + Labels: map[string]string{ + applabels.ApplicationLabel: "", + componentlabels.ComponentLabel: componentName, + applabels.OdoManagedBy: "odo", + applabels.OdoVersion: version.VERSION, + labels.URLLabel: "example-0", + }, + }, + Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{ServiceName: "example-0", PortNumber: intstr.FromInt(8080)}), + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-1", + Labels: map[string]string{ + applabels.ApplicationLabel: "", + componentlabels.ComponentLabel: componentName, + applabels.OdoManagedBy: "odo", + applabels.OdoVersion: version.VERSION, + labels.URLLabel: "example-1", + }, + }, + Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{ServiceName: "example-1", PortNumber: intstr.FromInt(8080)}), + }, + }, + } +} + +func GetSingleIngress(urlName, componentName string) *extensionsv1.Ingress { + return &extensionsv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: urlName, + Labels: map[string]string{ + applabels.ApplicationLabel: "", + componentlabels.ComponentLabel: componentName, + applabels.OdoManagedBy: "odo", + applabels.OdoVersion: version.VERSION, + labels.URLLabel: urlName, + applabels.App: "", + }, + }, + Spec: *kclient.GenerateIngressSpec(kclient.IngressParameter{ServiceName: urlName, PortNumber: intstr.FromInt(8080)}), + } +} diff --git a/pkg/kclient/fake/secrets.go b/pkg/kclient/fake/secrets.go new file mode 100644 index 00000000000..58aa5d85a8e --- /dev/null +++ b/pkg/kclient/fake/secrets.go @@ -0,0 +1,15 @@ +package fake + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetSecret(secretName string) *v1.Secret { + return &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + } +} diff --git a/pkg/machineoutput/services.go b/pkg/machineoutput/services.go new file mode 100644 index 00000000000..9dd8d32d266 --- /dev/null +++ b/pkg/machineoutput/services.go @@ -0,0 +1,25 @@ +package machineoutput + +import ( + "github.com/openshift/odo/pkg/catalog" + olm "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type CatalogListOutput struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Services *catalog.ServiceTypeList `json:"services,omitempty"` + // list of clusterserviceversions (installed by Operators) + Operators *olm.ClusterServiceVersionList `json:"operators,omitempty"` +} + +func NewCatalogListOutput(services *catalog.ServiceTypeList, operators *olm.ClusterServiceVersionList) CatalogListOutput { + return CatalogListOutput{ + TypeMeta: metav1.TypeMeta{ + Kind: "CatalogListOutput", + }, + Services: services, + Operators: operators, + } +} diff --git a/pkg/occlient/occlient.go b/pkg/occlient/occlient.go index f9970aae733..6e59dd6466f 100644 --- a/pkg/occlient/occlient.go +++ b/pkg/occlient/occlient.go @@ -57,6 +57,7 @@ import ( "k8s.io/apimachinery/pkg/version" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -209,6 +210,7 @@ type Client struct { routeClient routeclientset.RouteV1Interface userClient userclientset.UserV1Interface KubeConfig clientcmd.ClientConfig + discoveryClient discovery.DiscoveryClient Namespace string } @@ -275,6 +277,12 @@ func New() (*Client, error) { client.userClient = userClient + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + client.discoveryClient = *discoveryClient + namespace, _, err := client.KubeConfig.Namespace() if err != nil { return nil, err @@ -909,7 +917,7 @@ func (c *Client) NewAppS2I(params CreateArgs, commonObjectMeta metav1.ObjectMeta return errors.Wrapf(err, "unable to create DeploymentConfig for %s", commonObjectMeta.Name) } - ownerReference := generateOwnerReference(createdDC) + ownerReference := GenerateOwnerReference(createdDC) // update the owner references for the new storage for _, storage := range params.StorageToBeMounted { @@ -1213,7 +1221,7 @@ func (c *Client) BootstrapSupervisoredS2I(params CreateArgs, commonObjectMeta me jsonDC, _ = json.Marshal(createdDC) glog.V(5).Infof("Created new DeploymentConfig:\n%s\n", string(jsonDC)) - ownerReference := generateOwnerReference(createdDC) + ownerReference := GenerateOwnerReference(createdDC) // update the owner references for the new storage for _, storage := range params.StorageToBeMounted { @@ -1431,7 +1439,7 @@ func (c *Client) PatchCurrentDC(dc appsv1.DeploymentConfig, prePatchDCHandler dc // update the owner references for the new storage for _, storage := range ucp.StorageToBeMounted { - err := updateStorageOwnerReference(c, storage, generateOwnerReference(updatedDc)) + err := updateStorageOwnerReference(c, storage, GenerateOwnerReference(updatedDc)) if err != nil { return errors.Wrapf(err, "unable to update owner reference of storage") } @@ -1608,7 +1616,7 @@ func (c *Client) UpdateDCToSupervisor(ucp UpdateComponentParams, isToLocal bool, ) addInitVolumesToDC(&dc, ucp.CommonObjectMeta.Name, s2iPaths.DeploymentDir) - ownerReference := generateOwnerReference(ucp.ExistingDC) + ownerReference := GenerateOwnerReference(ucp.ExistingDC) // Setup PVC _, err = c.CreatePVC(getAppRootVolumeName(ucp.CommonObjectMeta.Name), "1Gi", ucp.CommonObjectMeta.Labels, ownerReference) @@ -2587,7 +2595,7 @@ func (c *Client) GetAllClusterServicePlans() ([]scv1beta1.ClusterServicePlan, er // serviceName is the name of the service for the target reference // portNumber is the target port of the route // secureURL indicates if the route is a secure one or not -func (c *Client) CreateRoute(name string, serviceName string, portNumber intstr.IntOrString, labels map[string]string, secureURL bool) (*routev1.Route, error) { +func (c *Client) CreateRoute(name string, serviceName string, portNumber intstr.IntOrString, labels map[string]string, secureURL bool, ownerReference metav1.OwnerReference) (*routev1.Route, error) { route := &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -2611,16 +2619,6 @@ func (c *Client) CreateRoute(name string, serviceName string, portNumber intstr. } } - // since the serviceName is same as the DC name, we use that to get the DC - // to which this route belongs. A better way could be to get service from - // the name and set it as owner of the route - dc, err := c.GetDeploymentConfigFromName(serviceName) - if err != nil { - return nil, errors.Wrapf(err, "unable to get DeploymentConfig %s", name) - } - - ownerReference := generateOwnerReference(dc) - route.SetOwnerReferences(append(route.GetOwnerReferences(), ownerReference)) r, err := c.routeClient.Routes(c.Namespace).Create(route) @@ -3255,11 +3253,32 @@ func isSubDir(baseDir, otherDir string) bool { return matches } -// generateOwnerReference genertes an ownerReference which can then be set as +// IsRouteSupported checks if route resource type is present on the cluster +func (c *Client) IsRouteSupported() (bool, error) { + const ClusterVersionGroup = "route.openshift.io" + const ClusterVersionVersion = "v1" + groupVersion := metav1.GroupVersion{Group: ClusterVersionGroup, Version: ClusterVersionVersion}.String() + + list, err := c.discoveryClient.ServerResourcesForGroupVersion(groupVersion) + if kerrors.IsNotFound(err) { + return false, nil + } else if err != nil { + return false, err + } + + for _, resources := range list.APIResources { + if resources.Name == "routes" { + return true, nil + } + } + return false, nil +} + +// GenerateOwnerReference genertes an ownerReference which can then be set as // owner for various OpenShift objects and ensure that when the owner object is // deleted from the cluster, all other objects are automatically removed by // OpenShift garbage collector -func generateOwnerReference(dc *appsv1.DeploymentConfig) metav1.OwnerReference { +func GenerateOwnerReference(dc *appsv1.DeploymentConfig) metav1.OwnerReference { ownerReference := metav1.OwnerReference{ APIVersion: "apps.openshift.io/v1", diff --git a/pkg/occlient/occlient_test.go b/pkg/occlient/occlient_test.go index ef0ea6fcc99..f10d388d63d 100644 --- a/pkg/occlient/occlient_test.go +++ b/pkg/occlient/occlient_test.go @@ -608,13 +608,13 @@ func TestCreateRoute(t *testing.T) { // initialising the fakeclient fkclient, fkclientset := FakeNew() + ownerReferences := GenerateOwnerReference(&tt.existingDC) + fkclientset.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) { - dc := &appsv1.DeploymentConfig{} - dc.Name = tt.service - return true, dc, nil + return true, &tt.existingDC, nil }) - createdRoute, err := fkclient.CreateRoute(tt.urlName, tt.service, tt.portNumber, tt.labels, tt.secureURL) + createdRoute, err := fkclient.CreateRoute(tt.urlName, tt.service, tt.portNumber, tt.labels, tt.secureURL, ownerReferences) if tt.secureURL { wantedTLSConfig := &routev1.TLSConfig{ diff --git a/pkg/occlient/volumes_test.go b/pkg/occlient/volumes_test.go index ddccef1a9bf..16eee986f87 100644 --- a/pkg/occlient/volumes_test.go +++ b/pkg/occlient/volumes_test.go @@ -270,7 +270,7 @@ func Test_updateStorageOwnerReference(t *testing.T) { args: args{ pvc: testingutil.FakePVC("pvc-1", "1Gi", map[string]string{}), ownerReference: []v1.OwnerReference{ - generateOwnerReference(fakeDC), + GenerateOwnerReference(fakeDC), }, }, wantErr: false, diff --git a/pkg/odo/cli/catalog/list/components.go b/pkg/odo/cli/catalog/list/components.go index 8cb6144ffdf..77fb360958d 100644 --- a/pkg/odo/cli/catalog/list/components.go +++ b/pkg/odo/cli/catalog/list/components.go @@ -14,6 +14,7 @@ import ( "github.com/openshift/odo/pkg/odo/cli/catalog/util" "github.com/openshift/odo/pkg/odo/genericclioptions" "github.com/openshift/odo/pkg/odo/util/experimental" + "github.com/openshift/odo/pkg/odo/util/pushtarget" "github.com/spf13/cobra" ) @@ -41,14 +42,18 @@ func NewListComponentsOptions() *ListComponentsOptions { // Complete completes ListComponentsOptions after they've been created func (o *ListComponentsOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { - o.Context = genericclioptions.NewContext(cmd) - o.catalogList, err = catalog.ListComponents(o.Client) - if err != nil { - if experimental.IsExperimentalModeEnabled() { - glog.V(4).Info("Please log in to an OpenShift cluster to list OpenShift/s2i components") - } else { - return err + if !pushtarget.IsPushTargetDocker() { + o.Context = genericclioptions.NewContext(cmd) + o.catalogList, err = catalog.ListComponents(o.Client) + if err != nil { + if experimental.IsExperimentalModeEnabled() { + glog.V(4).Info("Please log in to an OpenShift cluster to list OpenShift/s2i components") + } else { + return err + } } + + o.catalogList.Items = util.FilterHiddenComponents(o.catalogList.Items) } if experimental.IsExperimentalModeEnabled() { @@ -62,8 +67,6 @@ func (o *ListComponentsOptions) Complete(name string, cmd *cobra.Command, args [ } } - o.catalogList.Items = util.FilterHiddenComponents(o.catalogList.Items) - return } diff --git a/pkg/odo/cli/catalog/list/services.go b/pkg/odo/cli/catalog/list/services.go index ac8c8930a2d..0a3b4c2b513 100644 --- a/pkg/odo/cli/catalog/list/services.go +++ b/pkg/odo/cli/catalog/list/services.go @@ -93,7 +93,7 @@ func (o *ListServicesOptions) Validate() (err error) { // Run contains the logic for the command associated with ListServicesOptions func (o *ListServicesOptions) Run() (err error) { if log.IsJSON() { - machineoutput.OutputSuccess(o.services) + machineoutput.OutputSuccess(machineoutput.NewCatalogListOutput(&o.services, o.csvs)) } else { if experimental.IsExperimentalModeEnabled() { if len(o.csvs.Items) > 0 { diff --git a/pkg/odo/cli/cli.go b/pkg/odo/cli/cli.go index acd92379ba9..9e257aec1ba 100644 --- a/pkg/odo/cli/cli.go +++ b/pkg/odo/cli/cli.go @@ -36,7 +36,7 @@ const OdoRecommendedName = "odo" var ( // We do not use ktemplates.Normalize here as it messed up the newlines.. - odoLong = `(OpenShift Do) odo is a CLI tool for running OpenShift applications in a fast and automated manner. + odoLong = `odo is a CLI tool for running OpenShift applications in a fast and automated manner. Reducing the complexity of deployment, odo adds iterative development without the worry of deploying your source code. Find more information at https://github.com/openshift/odo` @@ -133,7 +133,7 @@ func odoRootCmd(name, fullName string) *cobra.Command { // rootCmd represents the base command when called without any subcommands rootCmd := &cobra.Command{ Use: name, - Short: "odo (OpenShift Do)", + Short: "odo", Long: odoLong, RunE: ShowHelp, Example: fmt.Sprintf(odoExample, fullName), diff --git a/pkg/odo/cli/component/delete.go b/pkg/odo/cli/component/delete.go index 71f8179db68..0cfec64863a 100644 --- a/pkg/odo/cli/component/delete.go +++ b/pkg/odo/cli/component/delete.go @@ -2,6 +2,7 @@ package component import ( "fmt" + "os" "path/filepath" "github.com/openshift/odo/pkg/envinfo" @@ -99,6 +100,10 @@ func (do *DeleteOptions) Validate() (err error) { } if !do.isCmpExists { log.Errorf("Component %s does not exist on the cluster", do.ComponentOptions.componentName) + // If request is to delete non existing component without all flag, exit with exit code 1 + if !do.componentDeleteAllFlag { + os.Exit(1) + } } return } diff --git a/pkg/odo/cli/component/describe.go b/pkg/odo/cli/component/describe.go index 24ea2a0fe77..94a68e7e44d 100644 --- a/pkg/odo/cli/component/describe.go +++ b/pkg/odo/cli/component/describe.go @@ -61,11 +61,18 @@ func (do *DescribeOptions) Run() (err error) { state := component.GetComponentState(do.Context.Client, do.componentName, do.Context.Application) if state == component.StateTypeNotPushed || state == component.StateTypeUnknown { + if !do.LocalConfigInfo.ConfigFileExists() { + return fmt.Errorf("Component %v does not exist", do.componentName) + } componentDesc, err = component.GetComponentFromConfig(do.LocalConfigInfo) componentDesc.Status.State = state if err != nil { return err } + if componentDesc.Name != do.componentName { + return fmt.Errorf("Component %v does not exist", do.componentName) + } + } else { componentDesc, err = component.GetComponent(do.Context.Client, do.componentName, do.Context.Application, do.Context.Project) if err != nil { diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go index 9658f883207..06bd0d5b846 100644 --- a/pkg/odo/cli/component/devfile.go +++ b/pkg/odo/cli/component/devfile.go @@ -79,6 +79,7 @@ func (po *PushOptions) DevfilePush() (err error) { ForceBuild: po.forceBuild, Show: po.show, EnvSpecificInfo: *po.EnvSpecificInfo, + DevfileInitCmd: strings.ToLower(po.devfileInitCommand), DevfileBuildCmd: strings.ToLower(po.devfileBuildCommand), DevfileRunCmd: strings.ToLower(po.devfileRunCommand), } diff --git a/pkg/odo/cli/component/push.go b/pkg/odo/cli/component/push.go index e9f2211472a..e882557b061 100644 --- a/pkg/odo/cli/component/push.go +++ b/pkg/odo/cli/component/push.go @@ -42,10 +42,10 @@ type PushOptions struct { DevfilePath string // devfile commands + devfileInitCommand string devfileBuildCommand string devfileRunCommand string - - namespace string + namespace string } // NewPushOptions returns new instance of PushOptions @@ -170,6 +170,7 @@ func NewCmdPush(name, fullName string) *cobra.Command { if experimental.IsExperimentalModeEnabled() { pushCmd.Flags().StringVar(&po.DevfilePath, "devfile", "./devfile.yaml", "Path to a devfile.yaml") pushCmd.Flags().StringVar(&po.namespace, "namespace", "", "Namespace to push the component to") + pushCmd.Flags().StringVar(&po.devfileInitCommand, "init-command", "", "Devfile Init Command to execute") pushCmd.Flags().StringVar(&po.devfileBuildCommand, "build-command", "", "Devfile Build Command to execute") pushCmd.Flags().StringVar(&po.devfileRunCommand, "run-command", "", "Devfile Run Command to execute") } diff --git a/pkg/odo/cli/config/set.go b/pkg/odo/cli/config/set.go index 5c1a508bad6..8559e6ec8e2 100644 --- a/pkg/odo/cli/config/set.go +++ b/pkg/odo/cli/config/set.go @@ -33,6 +33,7 @@ var ( %[1]s %[9]s 0.5 %[1]s %[10]s 2 %[1]s %[11]s 1 + %[1]s %[12]s 8080/TCP,8443/TCP # Set a env variable in the local config %[1]s --env KAFKA_HOST=kafka --env KAFKA_PORT=6639 @@ -159,7 +160,7 @@ func NewCmdSet(name, fullName string) *cobra.Command { Short: "Set a value in odo config file", Long: fmt.Sprintf(setLongDesc, config.FormatLocallySupportedParameters()), Example: fmt.Sprintf(fmt.Sprint("\n", setExample), fullName, config.Type, - config.Name, config.MinMemory, config.MaxMemory, config.Memory, config.DebugPort, config.Ignore, config.MinCPU, config.MaxCPU, config.CPU), + config.Name, config.MinMemory, config.MaxMemory, config.Memory, config.DebugPort, config.Ignore, config.MinCPU, config.MaxCPU, config.CPU, config.Ports), Args: func(cmd *cobra.Command, args []string) error { if o.envArray != nil { // no args are needed diff --git a/pkg/odo/cli/debug/info.go b/pkg/odo/cli/debug/info.go index 2a1aa002e61..a05dfbda107 100644 --- a/pkg/odo/cli/debug/info.go +++ b/pkg/odo/cli/debug/info.go @@ -1,9 +1,12 @@ package debug import ( + "fmt" + "github.com/openshift/odo/pkg/config" "github.com/openshift/odo/pkg/debug" "github.com/openshift/odo/pkg/log" + "github.com/openshift/odo/pkg/machineoutput" "github.com/openshift/odo/pkg/odo/genericclioptions" "github.com/spf13/cobra" k8sgenclioptions "k8s.io/cli-runtime/pkg/genericclioptions" @@ -58,9 +61,13 @@ func (o InfoOptions) Validate() error { // Run implements all the necessary functionality for port-forward cmd. func (o InfoOptions) Run() error { if debugFileInfo, debugging := debug.GetDebugInfo(o.PortForwarder); debugging { - log.Infof("Debug is running for the component on the local port : %v\n", debugFileInfo.LocalPort) + if log.IsJSON() { + machineoutput.OutputSuccess(debugFileInfo) + } else { + log.Infof("Debug is running for the component on the local port : %v", debugFileInfo.Spec.LocalPort) + } } else { - log.Infof("Debug is not running for the component %v\n", o.LocalConfigInfo.GetName()) + return fmt.Errorf("debug is not running for the component %v", o.LocalConfigInfo.GetName()) } return nil } @@ -70,10 +77,11 @@ func NewCmdInfo(name, fullName string) *cobra.Command { opts := NewInfoOptions() cmd := &cobra.Command{ - Use: name, - Short: "Displays debug info of a component", - Long: infoLong, - Example: infoExample, + Use: name, + Short: "Displays debug info of a component", + Long: infoLong, + Example: infoExample, + Annotations: map[string]string{"machineoutput": "json"}, Run: func(cmd *cobra.Command, args []string) { genericclioptions.GenericRun(opts, cmd, args) }, diff --git a/pkg/odo/cli/url/create.go b/pkg/odo/cli/url/create.go index 8f69a47dbea..714d56fb717 100644 --- a/pkg/odo/cli/url/create.go +++ b/pkg/odo/cli/url/create.go @@ -44,13 +44,16 @@ var ( urlCreateExampleExperimental = ktemplates.Examples(` # Create a URL with a specific host by automatically detecting the port used by the component (using CRC as an exampple) %[1]s example --host apps-crc.testing - # Create a URL with a specific name and host (using CRC as an exampple) + # Create a URL with a specific name and host (using CRC as an example) %[1]s example --host apps-crc.testing - # Create a URL for the current component with a specific port and host (using CRC as an exampple) + # Create a URL for the current component with a specific port and host (using CRC as an example) %[1]s --port 8080 --host apps-crc.testing - # Create a secured URL for the current component with a specific host (using CRC as an exampple) + # Create a URL of ingress kind for the current component with a host (using CRC as an example) + %[1]s --host apps-crc.testing --ingress + + # Create a secured URL for the current component with a specific host (using CRC as an example) %[1]s --host apps-crc.testing --secured `) @@ -68,15 +71,18 @@ var ( // URLCreateOptions encapsulates the options for the odo url create command type URLCreateOptions struct { *clicomponent.PushOptions - urlName string - urlPort int - secureURL bool - componentPort int - now bool - host string - tlsSecret string - exposedPort int - forceFlag bool + urlName string + urlPort int + secureURL bool + componentPort int + now bool + host string + tlsSecret string + exposedPort int + forceFlag bool + isRouteSupported bool + wantIngress bool + urlType envinfo.URLKind } // NewURLCreateOptions creates a new URLCreateOptions instance @@ -93,7 +99,28 @@ func (o *URLCreateOptions) Complete(name string, cmd *cobra.Command, args []stri } else { o.Context = genericclioptions.NewContext(cmd) } + + o.Client = genericclioptions.Client(cmd) + + routeSupported, err := o.Client.IsRouteSupported() + if err != nil { + return err + } + if routeSupported { + o.isRouteSupported = true + } + if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(o.DevfilePath) { + if o.wantIngress || (!o.isRouteSupported) { + o.urlType = envinfo.INGRESS + } else { + o.urlType = envinfo.ROUTE + } + + if o.tlsSecret != "" && (!o.wantIngress || !o.secureURL) { + return fmt.Errorf("tls secret is only available for secure URLs of ingress kind") + } + err = o.InitEnvInfoFromContext() if err != nil { return err @@ -180,7 +207,7 @@ func (o *URLCreateOptions) Validate() (err error) { if experimental.IsExperimentalModeEnabled() && util.CheckPathExists(o.DevfilePath) { // if experimental mode is enabled, and devfile is provided. // check if valid host is provided - if !pushtarget.IsPushTargetDocker() && len(o.host) <= 0 { + if !pushtarget.IsPushTargetDocker() && len(o.host) <= 0 && (!o.isRouteSupported || o.wantIngress) { return fmt.Errorf("host must be provided in order to create ingress") } for _, localURL := range o.EnvSpecificInfo.GetURL() { @@ -245,7 +272,7 @@ func (o *URLCreateOptions) Run() (err error) { } } } - err = o.EnvSpecificInfo.SetConfiguration("url", envinfo.EnvInfoURL{Name: o.urlName, Port: o.componentPort, Host: o.host, Secure: o.secureURL, TLSSecret: o.tlsSecret, ExposedPort: o.exposedPort}) + err = o.EnvSpecificInfo.SetConfiguration("url", envinfo.EnvInfoURL{Name: o.urlName, Port: o.componentPort, Host: o.host, Secure: o.secureURL, TLSSecret: o.tlsSecret, ExposedPort: o.exposedPort, Kind: o.urlType}) } else { err = o.LocalConfigInfo.SetConfiguration("url", config.ConfigURL{Name: o.urlName, Port: o.componentPort, Secure: o.secureURL}) } @@ -257,8 +284,7 @@ func (o *URLCreateOptions) Run() (err error) { if pushtarget.IsPushTargetDocker() { log.Successf("URL %s created for component: %v with exposed port: %v", o.urlName, componentName, o.exposedPort) } else { - curIngressDomain := fmt.Sprintf("%v.%v", o.urlName, o.host) - log.Successf("URL %s created for component: %v", curIngressDomain, componentName) + log.Successf("URL %s created for component: %v", o.urlName, componentName) } } else { log.Successf("URL %s created for component: %v", o.urlName, o.Component()) @@ -303,11 +329,13 @@ func NewCmdURLCreate(name, fullName string) *cobra.Command { urlCreateCmd.Flags().StringVar(&o.tlsSecret, "tls-secret", "", "tls secret name for the url of the component if the user bring his own tls secret") urlCreateCmd.Flags().StringVarP(&o.host, "host", "", "", "Cluster ip for this URL") urlCreateCmd.Flags().BoolVarP(&o.secureURL, "secure", "", false, "creates a secure https url") + urlCreateCmd.Flags().BoolVar(&o.wantIngress, "ingress", false, "Creates an ingress instead of Route on OpenShift clusters") urlCreateCmd.Example = fmt.Sprintf(urlCreateExampleExperimental, fullName) } urlCreateCmd.Flags().StringVar(&o.DevfilePath, "devfile", "./devfile.yaml", "Path to a devfile.yaml") } else { urlCreateCmd.Flags().BoolVarP(&o.secureURL, "secure", "", false, "creates a secure https url") + urlCreateCmd.Example = fmt.Sprintf(urlCreateExample, fullName) } genericclioptions.AddNowFlag(urlCreateCmd, &o.now) o.AddContextFlag(urlCreateCmd) diff --git a/pkg/odo/cli/url/describe.go b/pkg/odo/cli/url/describe.go index 84f5a43b69a..a47d4c24d78 100644 --- a/pkg/odo/cli/url/describe.go +++ b/pkg/odo/cli/url/describe.go @@ -73,7 +73,7 @@ func (o *URLDescribeOptions) Run() (err error) { tabWriterURL := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent) fmt.Fprintln(tabWriterURL, "NAME", "\t", "URL", "\t", "PORT") - fmt.Fprintln(tabWriterURL, u.Name, "\t", url.GetURLString(url.GetProtocol(routev1.Route{}, u), "", u.Spec.Rules[0].Host), "\t", u.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal) + fmt.Fprintln(tabWriterURL, u.Name, "\t", url.GetURLString(url.GetProtocol(routev1.Route{}, u, experimental.IsExperimentalModeEnabled()), "", u.Spec.Rules[0].Host, experimental.IsExperimentalModeEnabled()), "\t", u.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal) tabWriterURL.Flush() } } else { @@ -91,7 +91,7 @@ func (o *URLDescribeOptions) Run() (err error) { // are there changes between local and cluster states? outOfSync := false - fmt.Fprintln(tabWriterURL, u.Name, "\t", u.Status.State, "\t", url.GetURLString(u.Spec.Protocol, u.Spec.Host, ""), "\t", u.Spec.Port) + fmt.Fprintln(tabWriterURL, u.Name, "\t", u.Status.State, "\t", url.GetURLString(u.Spec.Protocol, u.Spec.Host, "", experimental.IsExperimentalModeEnabled()), "\t", u.Spec.Port) if u.Status.State != url.StateTypePushed { outOfSync = true } diff --git a/pkg/odo/cli/url/list.go b/pkg/odo/cli/url/list.go index 15f7d0e014f..1ea402c75b3 100644 --- a/pkg/odo/cli/url/list.go +++ b/pkg/odo/cli/url/list.go @@ -88,7 +88,7 @@ func (o *URLListOptions) Run() (err error) { var present bool for _, u := range urls.Items { if i.Name == u.Name { - fmt.Fprintln(tabWriterURL, u.Name, "\t", url.GetURLString(url.GetProtocol(routev1.Route{}, u), "", u.Spec.Rules[0].Host), "\t", u.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal, "\t", u.Spec.TLS != nil) + fmt.Fprintln(tabWriterURL, u.Name, "\t", url.GetURLString(url.GetProtocol(routev1.Route{}, u, experimental.IsExperimentalModeEnabled()), "", u.Spec.Rules[0].Host, experimental.IsExperimentalModeEnabled()), "\t", u.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal, "\t", u.Spec.TLS != nil) present = true } } @@ -121,7 +121,7 @@ func (o *URLListOptions) Run() (err error) { // are there changes between local and cluster states? outOfSync := false for _, u := range urls.Items { - fmt.Fprintln(tabWriterURL, u.Name, "\t", u.Status.State, "\t", url.GetURLString(u.Spec.Protocol, u.Spec.Host, ""), "\t", u.Spec.Port, "\t", u.Spec.Secure) + fmt.Fprintln(tabWriterURL, u.Name, "\t", u.Status.State, "\t", url.GetURLString(u.Spec.Protocol, u.Spec.Host, "", experimental.IsExperimentalModeEnabled()), "\t", u.Spec.Port, "\t", u.Spec.Secure) if u.Status.State != url.StateTypePushed { outOfSync = true } diff --git a/pkg/odo/genericclioptions/context.go b/pkg/odo/genericclioptions/context.go index 682d97601c6..44462c8666b 100644 --- a/pkg/odo/genericclioptions/context.go +++ b/pkg/odo/genericclioptions/context.go @@ -531,11 +531,7 @@ func (o *Context) ComponentAllowingEmpty(allowEmpty bool, optionalComponent ...s } case 1: cmp := optionalComponent[0] - // only check the component if we passed a non-empty string, otherwise return the current component set in NewContext - if len(cmp) > 0 { - o.checkComponentExistsOrFail(cmp) - o.cmp = cmp // update context - } + o.cmp = cmp default: // safeguard: fail if more than one optional string is passed because it would be a programming error log.Errorf("ComponentAllowingEmpty function only accepts one optional argument, was given: %v", optionalComponent) diff --git a/pkg/odo/util/cmdutils.go b/pkg/odo/util/cmdutils.go index 01ef8ccb9b5..75ca1ef0566 100644 --- a/pkg/odo/util/cmdutils.go +++ b/pkg/odo/util/cmdutils.go @@ -152,7 +152,7 @@ func PrintComponentInfo(client *occlient.Client, currentComponentName string, co // Gather the output for _, componentURL := range componentDesc.Spec.URL { url := urls.Get(componentURL) - output += fmt.Sprintf(" · %v exposed via %v\n", urlPkg.GetURLString(url.Spec.Protocol, url.Spec.Host, ""), url.Spec.Port) + output += fmt.Sprintf(" · %v exposed via %v\n", urlPkg.GetURLString(url.Spec.Protocol, url.Spec.Host, "", experimental.IsExperimentalModeEnabled()), url.Spec.Port) } } diff --git a/pkg/testingutil/devfile.go b/pkg/testingutil/devfile.go index 2da5e2ccfc0..303d8b4c3a3 100644 --- a/pkg/testingutil/devfile.go +++ b/pkg/testingutil/devfile.go @@ -8,6 +8,7 @@ import ( type TestDevfileData struct { ComponentType versionsCommon.DevfileComponentType CommandActions []versionsCommon.DevfileCommandAction + MissingInitCommand bool MissingBuildCommand bool } @@ -93,25 +94,30 @@ func (d TestDevfileData) GetProjects() []versionsCommon.DevfileProject { // GetCommands is a mock function to get the commands from a devfile func (d TestDevfileData) GetCommands() []versionsCommon.DevfileCommand { - commandName := [...]string{"devbuild", "devrun", "customcommand"} + commandName := [...]string{"devinit", "devbuild", "devrun", "customcommand"} commands := []versionsCommon.DevfileCommand{ { - Name: commandName[1], + Name: commandName[2], Actions: d.CommandActions, }, { - Name: commandName[2], + Name: commandName[3], Actions: d.CommandActions, }, } - - if !d.MissingBuildCommand { + if !d.MissingInitCommand { commands = append(commands, versionsCommon.DevfileCommand{ Name: commandName[0], Actions: d.CommandActions, }) } + if !d.MissingBuildCommand { + commands = append(commands, versionsCommon.DevfileCommand{ + Name: commandName[1], + Actions: d.CommandActions, + }) + } return commands } diff --git a/pkg/testingutil/routes.go b/pkg/testingutil/routes.go new file mode 100644 index 00000000000..6347b86df8d --- /dev/null +++ b/pkg/testingutil/routes.go @@ -0,0 +1,45 @@ +package testingutil + +import ( + "fmt" + routev1 "github.com/openshift/api/route/v1" + applabels "github.com/openshift/odo/pkg/application/labels" + componentlabels "github.com/openshift/odo/pkg/component/labels" + "github.com/openshift/odo/pkg/url/labels" + "github.com/openshift/odo/pkg/version" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func GetRouteListWithMultiple(componentName, applicationName string) *routev1.RouteList { + return &routev1.RouteList{ + Items: []routev1.Route{ + GetSingleRoute("example", 8080, componentName, applicationName), + GetSingleRoute("example-1", 9100, componentName, applicationName), + }, + } +} + +func GetSingleRoute(urlName string, port int, componentName, applicationName string) routev1.Route { + return routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: urlName, + Labels: map[string]string{ + applabels.ApplicationLabel: applicationName, + componentlabels.ComponentLabel: componentName, + applabels.OdoManagedBy: "odo", + applabels.OdoVersion: version.VERSION, + labels.URLLabel: urlName, + }, + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: fmt.Sprintf("%s-%s", componentName, applicationName), + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromInt(port), + }, + }, + } +} diff --git a/pkg/url/types.go b/pkg/url/types.go index f023378450c..f52f66b75cb 100644 --- a/pkg/url/types.go +++ b/pkg/url/types.go @@ -1,6 +1,7 @@ package url import ( + "github.com/openshift/odo/pkg/envinfo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -14,10 +15,12 @@ type URL struct { // URLSpec is type URLSpec struct { - Host string `json:"host,omitempty"` - Protocol string `json:"protocol,omitempty"` - Port int `json:"port,omitempty"` - Secure bool `json:"secure"` + Host string `json:"host,omitempty"` + Protocol string `json:"protocol,omitempty"` + Port int `json:"port,omitempty"` + Secure bool `json:"secure"` + urlKind envinfo.URLKind + tLSSecret string } // AppList is a list of applications diff --git a/pkg/url/url.go b/pkg/url/url.go index 3e5dbde6727..233d9dc958d 100644 --- a/pkg/url/url.go +++ b/pkg/url/url.go @@ -3,10 +3,12 @@ package url import ( "fmt" "net" + "reflect" "strconv" "strings" "github.com/openshift/odo/pkg/envinfo" + "github.com/openshift/odo/pkg/log" routev1 "github.com/openshift/api/route/v1" applabels "github.com/openshift/odo/pkg/application/labels" @@ -18,6 +20,7 @@ import ( urlLabels "github.com/openshift/odo/pkg/url/labels" "github.com/openshift/odo/pkg/util" "github.com/pkg/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" "github.com/golang/glog" iextensionsv1 "k8s.io/api/extensions/v1beta1" @@ -98,51 +101,80 @@ func GetIngress(kClient *kclient.Client, envSpecificInfo *envinfo.EnvSpecificInf } // Delete deletes a URL -func Delete(client *occlient.Client, kClient *kclient.Client, urlName string, applicationName string) error { - if experimental.IsExperimentalModeEnabled() { +func Delete(client *occlient.Client, kClient *kclient.Client, urlName string, applicationName string, urlType envinfo.URLKind) error { + if urlType == envinfo.INGRESS { return kClient.DeleteIngress(urlName) - } else { - // Namespace the URL name - namespacedOpenShiftObject, err := util.NamespaceOpenShiftObject(urlName, applicationName) - if err != nil { - return errors.Wrapf(err, "unable to create namespaced name") + } else if urlType == envinfo.ROUTE { + if applicationName != "" { + // Namespace the URL name + var err error + urlName, err = util.NamespaceOpenShiftObject(urlName, applicationName) + if err != nil { + return errors.Wrapf(err, "unable to create namespaced name") + } } - return client.DeleteRoute(namespacedOpenShiftObject) + + return client.DeleteRoute(urlName) } + return errors.New("url type is not supported") +} + +type CreateParameters struct { + urlName string + portNumber int + secureURL bool + componentName string + applicationName string + host string + secretName string + urlKind envinfo.URLKind } // Create creates a URL and returns url string and error if any // portNumber is the target port number for the route and is -1 in case no port number is specified in which case it is automatically detected for components which expose only one service port) -func Create(client *occlient.Client, kClient *kclient.Client, urlName string, portNumber int, secureURL bool, componentName, applicationName string, host string, secretName string) (string, error) { - labels := urlLabels.GetLabels(urlName, componentName, applicationName, true) - - var serviceName string - if experimental.IsExperimentalModeEnabled() { - serviceName := componentName - ingressDomain := fmt.Sprintf("%v.%v", urlName, host) - deployment, err := kClient.GetDeploymentByName(componentName) +func Create(client *occlient.Client, kClient *kclient.Client, parameters CreateParameters, isRouteSupported bool, isExperimental bool) (string, error) { + + if parameters.urlKind != envinfo.INGRESS && parameters.urlKind != envinfo.ROUTE { + return "", fmt.Errorf("urlKind %s is not supported for URL creation", parameters.urlKind) + } + + if !parameters.secureURL && parameters.secretName != "" { + return "", fmt.Errorf("secret name can only be used for secure URLs") + } + + labels := urlLabels.GetLabels(parameters.urlName, parameters.componentName, parameters.applicationName, true) + + serviceName := "" + + if isExperimental && parameters.urlKind == envinfo.INGRESS && kClient != nil { + if parameters.host == "" { + return "", errors.Errorf("the host cannot be empty") + } + serviceName := parameters.componentName + ingressDomain := fmt.Sprintf("%v.%v", parameters.urlName, parameters.host) + deployment, err := kClient.GetDeploymentByName(parameters.componentName) if err != nil { return "", err } ownerReference := kclient.GenerateOwnerReference(deployment) - if secureURL { - if len(secretName) != 0 { - _, err := kClient.KubeClient.CoreV1().Secrets(kClient.Namespace).Get(secretName, metav1.GetOptions{}) + if parameters.secureURL { + if len(parameters.secretName) != 0 { + _, err := kClient.KubeClient.CoreV1().Secrets(kClient.Namespace).Get(parameters.secretName, metav1.GetOptions{}) if err != nil { - return "", errors.Wrap(err, "unable to get the provided secret: "+secretName) + return "", errors.Wrap(err, "unable to get the provided secret: "+parameters.secretName) } } - if len(secretName) == 0 { - defaultTLSSecretName := componentName + "-tlssecret" + if len(parameters.secretName) == 0 { + defaultTLSSecretName := parameters.componentName + "-tlssecret" _, err := kClient.KubeClient.CoreV1().Secrets(kClient.Namespace).Get(defaultTLSSecretName, metav1.GetOptions{}) // create tls secret if it does not exist - if err != nil { - selfsignedcert, err := kclient.GenerateSelfSignedCertificate(host) + if kerrors.IsNotFound(err) { + selfsignedcert, err := kclient.GenerateSelfSignedCertificate(parameters.host) if err != nil { - return "", errors.Wrap(err, "unable to generate self-signed certificate for clutser: "+host) + return "", errors.Wrap(err, "unable to generate self-signed certificate for clutser: "+parameters.host) } // create tls secret - secretlabels := componentlabels.GetLabels(componentName, applicationName, true) + secretlabels := componentlabels.GetLabels(parameters.componentName, parameters.applicationName, true) objectMeta := metav1.ObjectMeta{ Name: defaultTLSSecretName, Labels: secretlabels, @@ -152,44 +184,73 @@ func Create(client *occlient.Client, kClient *kclient.Client, urlName string, po } secret, err := kClient.CreateTLSSecret(selfsignedcert.CertPem, selfsignedcert.KeyPem, objectMeta) if err != nil { - return "", errors.Wrap(err, "unable to create tls secret: "+secret.Name) + return "", errors.Wrap(err, "unable to create tls secret") } - secretName = secret.Name + parameters.secretName = secret.Name + } else if err != nil { + return "", err } else { // tls secret found for this component - secretName = defaultTLSSecretName + parameters.secretName = defaultTLSSecretName } } } - ingressParam := kclient.IngressParameter{ServiceName: serviceName, IngressDomain: ingressDomain, PortNumber: intstr.FromInt(portNumber), TLSSecretName: secretName} + ingressParam := kclient.IngressParameter{ServiceName: serviceName, IngressDomain: ingressDomain, PortNumber: intstr.FromInt(parameters.portNumber), TLSSecretName: parameters.secretName} ingressSpec := kclient.GenerateIngressSpec(ingressParam) - objectMeta := kclient.CreateObjectMeta(componentName, kClient.Namespace, labels, nil) - objectMeta.Name = urlName + objectMeta := kclient.CreateObjectMeta(parameters.componentName, kClient.Namespace, labels, nil) + objectMeta.Name = parameters.urlName objectMeta.OwnerReferences = append(objectMeta.OwnerReferences, ownerReference) // Pass in the namespace name, link to the service (componentName) and labels to create a ingress ingress, err := kClient.CreateIngress(objectMeta, *ingressSpec) if err != nil { return "", errors.Wrap(err, "unable to create ingress") } - return GetURLString(GetProtocol(routev1.Route{}, *ingress), "", ingressDomain), nil + return GetURLString(GetProtocol(routev1.Route{}, *ingress, isExperimental), "", ingressDomain, isExperimental), nil } else { - urlName, err := util.NamespaceOpenShiftObject(urlName, applicationName) - if err != nil { - return "", errors.Wrapf(err, "unable to create namespaced name") + if !isRouteSupported { + return "", errors.Errorf("routes are not available on non OpenShift clusters") } - serviceName, err = util.NamespaceOpenShiftObject(componentName, applicationName) - if err != nil { - return "", errors.Wrapf(err, "unable to create namespaced name") + + var ownerReference metav1.OwnerReference + if !isExperimental || kClient == nil { + var err error + parameters.urlName, err = util.NamespaceOpenShiftObject(parameters.urlName, parameters.applicationName) + if err != nil { + return "", errors.Wrapf(err, "unable to create namespaced name") + } + serviceName, err = util.NamespaceOpenShiftObject(parameters.componentName, parameters.applicationName) + if err != nil { + return "", errors.Wrapf(err, "unable to create namespaced name") + } + + // since the serviceName is same as the DC name, we use that to get the DC + // to which this route belongs. A better way could be to get service from + // the name and set it as owner of the route + dc, err := client.GetDeploymentConfigFromName(serviceName) + if err != nil { + return "", errors.Wrapf(err, "unable to get DeploymentConfig %s", serviceName) + } + + ownerReference = occlient.GenerateOwnerReference(dc) + } else { + serviceName = parameters.componentName + + deployment, err := kClient.GetDeploymentByName(parameters.componentName) + if err != nil { + return "", err + } + ownerReference = kclient.GenerateOwnerReference(deployment) } + // Pass in the namespace name, link to the service (componentName) and labels to create a route - route, err := client.CreateRoute(urlName, serviceName, intstr.FromInt(portNumber), labels, secureURL) + route, err := client.CreateRoute(parameters.urlName, serviceName, intstr.FromInt(parameters.portNumber), labels, parameters.secureURL, ownerReference) if err != nil { return "", errors.Wrap(err, "unable to create route") } - return GetURLString(GetProtocol(*route, iextensionsv1.Ingress{}), route.Spec.Host, ""), nil + return GetURLString(GetProtocol(*route, iextensionsv1.Ingress{}, isExperimental), route.Spec.Host, "", isExperimental), nil } } @@ -213,6 +274,9 @@ func ListPushed(client *occlient.Client, componentName string, applicationName s var urls []URL for _, r := range routes { + if r.OwnerReferences != nil && r.OwnerReferences[0].Kind == "Ingress" { + continue + } a := getMachineReadableFormat(r) urls = append(urls, a) } @@ -301,8 +365,8 @@ func List(client *occlient.Client, localConfig *config.LocalConfigInfo, componen } // GetProtocol returns the protocol string -func GetProtocol(route routev1.Route, ingress iextensionsv1.Ingress) string { - if experimental.IsExperimentalModeEnabled() { +func GetProtocol(route routev1.Route, ingress iextensionsv1.Ingress, isExperimental bool) string { + if isExperimental { if ingress.Spec.TLS != nil { return "https" } @@ -331,8 +395,8 @@ func ConvertConfigURL(configURL config.ConfigURL) URL { } // GetURLString returns a string representation of given url -func GetURLString(protocol, URL string, ingressDomain string) string { - if experimental.IsExperimentalModeEnabled() { +func GetURLString(protocol, URL string, ingressDomain string, isExperimentalMode bool) string { + if isExperimentalMode && URL == "" { return protocol + "://" + ingressDomain } return protocol + "://" + URL @@ -423,7 +487,7 @@ func getMachineReadableFormat(r routev1.Route) URL { return URL{ TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.openshift.io/v1alpha1"}, ObjectMeta: metav1.ObjectMeta{Name: r.Labels[urlLabels.URLLabel]}, - Spec: URLSpec{Host: r.Spec.Host, Port: r.Spec.Port.TargetPort.IntValue(), Protocol: GetProtocol(r, iextensionsv1.Ingress{}), Secure: r.Spec.TLS != nil}, + Spec: URLSpec{Host: r.Spec.Host, Port: r.Spec.Port.TargetPort.IntValue(), Protocol: GetProtocol(r, iextensionsv1.Ingress{}, experimental.IsExperimentalModeEnabled()), Secure: r.Spec.TLS != nil}, } } @@ -458,3 +522,134 @@ func getMachineReadableFormatForIngressList(ingresses []iextensionsv1.Ingress) i Items: ingresses, } } + +type PushParameters struct { + ComponentName string + ApplicationName string + ConfigURLs []config.ConfigURL + EnvURLS []envinfo.EnvInfoURL + IsRouteSupported bool + IsExperimentalModeEnabled bool +} + +// Push creates and deletes the required URLs +func Push(client *occlient.Client, kClient *kclient.Client, parameters PushParameters) error { + urlLOCAL := make(map[string]URL) + + // in case the component is a s2i one + // kClient will be nil + if parameters.IsExperimentalModeEnabled && kClient != nil { + urls := parameters.EnvURLS + for _, url := range urls { + urlLOCAL[url.Name] = URL{ + Spec: URLSpec{ + Host: url.Host, + Port: url.Port, + Secure: url.Secure, + tLSSecret: url.TLSSecret, + urlKind: url.Kind, + }, + } + } + } else { + urls := parameters.ConfigURLs + for _, url := range urls { + urlLOCAL[url.Name] = URL{ + Spec: URLSpec{ + Port: url.Port, + Secure: url.Secure, + urlKind: envinfo.ROUTE, + }, + } + } + } + + urlCLUSTER := make(map[string]URL) + if parameters.IsExperimentalModeEnabled && kClient != nil { + urlList, err := ListPushedIngress(kClient, parameters.ComponentName) + if err != nil { + return err + } + for _, url := range urlList.Items { + urlCLUSTER[url.Name] = URL{ + Spec: URLSpec{ + Port: int(url.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal), + urlKind: envinfo.INGRESS, + }, + } + } + } + + if parameters.IsRouteSupported { + urlPushedRoutes, err := ListPushed(client, parameters.ComponentName, parameters.ApplicationName) + if err != nil { + return err + } + for _, urlRoute := range urlPushedRoutes.Items { + urlCLUSTER[urlRoute.Name] = URL{ + Spec: URLSpec{ + Port: urlRoute.Spec.Port, + urlKind: envinfo.ROUTE, + }, + } + } + } + + log.Info("\nApplying URL changes") + urlChange := false + + // find URLs to delete + for urlName, urlSpec := range urlCLUSTER { + val, ok := urlLOCAL[urlName] + if !ok { + if urlSpec.Spec.urlKind == envinfo.INGRESS && kClient == nil { + continue + } + // delete the url + err := Delete(client, kClient, urlName, parameters.ApplicationName, urlSpec.Spec.urlKind) + if err != nil { + return err + } + log.Successf("URL %s successfully deleted", urlName) + urlChange = true + continue + } else { + if !reflect.DeepEqual(val.Spec, urlSpec.Spec) { + return errors.Errorf("config mismatch for URL with the same name %s", val.Name) + } + } + } + + // find URLs to create + for urlName, urlInfo := range urlLOCAL { + _, ok := urlCLUSTER[urlName] + if !ok { + if urlInfo.Spec.urlKind == envinfo.INGRESS && kClient == nil { + continue + } + + createParameters := CreateParameters{ + urlName: urlName, + portNumber: urlInfo.Spec.Port, + secureURL: urlInfo.Spec.Secure, + componentName: parameters.ComponentName, + applicationName: parameters.ApplicationName, + host: urlInfo.Spec.Host, + secretName: urlInfo.Spec.tLSSecret, + urlKind: urlInfo.Spec.urlKind, + } + host, err := Create(client, kClient, createParameters, parameters.IsRouteSupported, parameters.IsExperimentalModeEnabled) + if err != nil { + return err + } + log.Successf("URL %s: %s created", urlName, host) + urlChange = true + } + } + + if !urlChange { + log.Success("URLs are synced with the cluster, no changes are required.") + } + + return nil +} diff --git a/pkg/url/url_test.go b/pkg/url/url_test.go index 1dd8efe77b6..187eb7d51b2 100644 --- a/pkg/url/url_test.go +++ b/pkg/url/url_test.go @@ -2,18 +2,25 @@ package url import ( "fmt" - "reflect" - "testing" - - // "github.com/openshift/odo/pkg/kclient" - - // "github.com/kylelemons/godebug/pretty" - //appsv1 "github.com/openshift/api/apps/v1" + "github.com/kylelemons/godebug/pretty" + appsv1 "github.com/openshift/api/apps/v1" routev1 "github.com/openshift/api/route/v1" applabels "github.com/openshift/odo/pkg/application/labels" componentlabels "github.com/openshift/odo/pkg/component/labels" + "github.com/openshift/odo/pkg/config" + "github.com/openshift/odo/pkg/envinfo" + "github.com/openshift/odo/pkg/kclient" + "github.com/openshift/odo/pkg/kclient/fake" "github.com/openshift/odo/pkg/occlient" + "github.com/openshift/odo/pkg/testingutil" "github.com/openshift/odo/pkg/url/labels" + "github.com/openshift/odo/pkg/util" + "k8s.io/api/core/v1" + extensionsv1 "k8s.io/api/extensions/v1beta1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "reflect" + "testing" //"github.com/openshift/odo/pkg/util" "github.com/openshift/odo/pkg/version" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,30 +29,38 @@ import ( ktesting "k8s.io/client-go/testing" ) -/* -TODO: FIX THESE TESTS. func TestCreate(t *testing.T) { type args struct { - componentName string - applicationName string - urlName string - portNumber int - secure bool + componentName string + applicationName string + urlName string + portNumber int + secure bool + host string + urlKind envinfo.URLKind + isRouteSupported bool + isExperimentalModeEnabled bool + tlsSecret string } tests := []struct { - name string - args args - returnedRoute *routev1.Route - want string - wantErr bool + name string + args args + returnedRoute *routev1.Route + returnedIngress *extensionsv1.Ingress + defaultTLSExists bool + userGivenTLSExists bool + want string + wantErr bool }{ { name: "Case 1: Component name same as urlName", args: args{ - componentName: "nodejs", - applicationName: "app", - urlName: "nodejs", - portNumber: 8080, + componentName: "nodejs", + applicationName: "app", + urlName: "nodejs", + portNumber: 8080, + isRouteSupported: true, + urlKind: envinfo.ROUTE, }, returnedRoute: &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ @@ -75,10 +90,12 @@ func TestCreate(t *testing.T) { { name: "Case 2: Component name different than urlName", args: args{ - componentName: "nodejs", - applicationName: "app", - urlName: "example-url", - portNumber: 9100, + componentName: "nodejs", + applicationName: "app", + urlName: "example-url", + portNumber: 9100, + isRouteSupported: true, + urlKind: envinfo.ROUTE, }, returnedRoute: &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ @@ -108,11 +125,13 @@ func TestCreate(t *testing.T) { { name: "Case 3: a secure URL", args: args{ - componentName: "nodejs", - applicationName: "app", - urlName: "example-url", - portNumber: 9100, - secure: true, + componentName: "nodejs", + applicationName: "app", + urlName: "example-url", + portNumber: 9100, + secure: true, + isRouteSupported: true, + urlKind: envinfo.ROUTE, }, returnedRoute: &routev1.Route{ ObjectMeta: metav1.ObjectMeta{ @@ -143,10 +162,189 @@ func TestCreate(t *testing.T) { want: "https://host", wantErr: false, }, + + { + name: "Case 4: Create a ingress, with same name as component,instead of route on openshift cluster", + args: args{ + componentName: "nodejs", + urlName: "nodejs", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + urlKind: envinfo.INGRESS, + }, + returnedIngress: fake.GetSingleIngress("nodejs", "nodejs"), + want: "http://nodejs.com", + wantErr: false, + }, + { + name: "Case 5: Create a ingress, with different name as component,instead of route on openshift cluster", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + urlKind: envinfo.INGRESS, + }, + returnedRoute: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nodejs-app", + Labels: map[string]string{ + "app.kubernetes.io/part-of": "app", + "app.kubernetes.io/instance": "nodejs", + applabels.App: "app", + applabels.OdoManagedBy: "odo", + applabels.OdoVersion: version.VERSION, + "odo.openshift.io/url-name": "nodejs", + }, + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: "nodejs-app", + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromInt(8080), + }, + }, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + want: "http://example.com", + wantErr: false, + }, + { + name: "Case 6: Create a secure ingress, instead of route on openshift cluster, default tls exists", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + secure: true, + urlKind: envinfo.INGRESS, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: true, + want: "https://example.com", + wantErr: false, + }, + { + name: "Case 7: Create a secure ingress, instead of route on openshift cluster and default tls doesn't exist", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + secure: true, + urlKind: envinfo.INGRESS, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + want: "https://example.com", + wantErr: false, + }, + { + name: "Case 8: Fail when while creating ingress when user given tls secret doesn't exists", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + secure: true, + tlsSecret: "user-secret", + urlKind: envinfo.INGRESS, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + userGivenTLSExists: false, + want: "http://example.com", + wantErr: true, + }, + { + name: "Case 9: Create a secure ingress, instead of route on openshift cluster, user tls secret does exists", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + secure: true, + tlsSecret: "user-secret", + urlKind: envinfo.INGRESS, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + userGivenTLSExists: true, + want: "https://example.com", + wantErr: false, + }, + + { + name: "Case 10: invalid url kind", + args: args{ + componentName: "nodejs", + urlName: "example", + portNumber: 8080, + host: "com", + isRouteSupported: true, + isExperimentalModeEnabled: true, + secure: true, + tlsSecret: "user-secret", + urlKind: "blah", + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + userGivenTLSExists: true, + want: "", + wantErr: true, + }, + { + name: "Case 11: route is not supported on the cluster", + args: args{ + componentName: "nodejs", + applicationName: "app", + urlName: "example", + isRouteSupported: false, + isExperimentalModeEnabled: true, + urlKind: envinfo.ROUTE, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + userGivenTLSExists: true, + want: "", + wantErr: true, + }, + { + name: "Case 11: secretName used without secure flag", + args: args{ + componentName: "nodejs", + applicationName: "app", + urlName: "example", + isRouteSupported: false, + isExperimentalModeEnabled: true, + tlsSecret: "secret", + urlKind: envinfo.ROUTE, + }, + returnedIngress: fake.GetSingleIngress("example", "nodejs"), + defaultTLSExists: false, + userGivenTLSExists: true, + want: "", + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client, fakeClientSet := occlient.FakeNew() + fakeKClient, fakeKClientSet := kclient.FakeNew() fakeClientSet.RouteClientset.PrependReactor("create", "routes", func(action ktesting.Action) (bool, runtime.Object, error) { route := action.(ktesting.CreateAction).GetObject().(*routev1.Route) @@ -154,9 +352,39 @@ func TestCreate(t *testing.T) { return true, route, nil }) - serviceName, err := util.NamespaceOpenShiftObject(tt.args.componentName, tt.args.applicationName) - if err != nil { - t.Error(err) + fakeKClientSet.Kubernetes.PrependReactor("get", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) { + var secretName string + if tt.args.tlsSecret == "" { + secretName = tt.args.componentName + "-tlssecret" + if action.(ktesting.GetAction).GetName() != secretName { + return true, nil, fmt.Errorf("get for secrets called with invalid name, want: %s,got: %s", secretName, action.(ktesting.GetAction).GetName()) + } + } else { + secretName = tt.args.tlsSecret + if action.(ktesting.GetAction).GetName() != tt.args.tlsSecret { + return true, nil, fmt.Errorf("get for secrets called with invalid name, want: %s,got: %s", tt.args.tlsSecret, action.(ktesting.GetAction).GetName()) + } + } + if tt.args.tlsSecret != "" { + if !tt.userGivenTLSExists { + return true, nil, kerrors.NewNotFound(schema.GroupResource{}, "") + } + } else if !tt.defaultTLSExists { + return true, nil, kerrors.NewNotFound(schema.GroupResource{}, "") + } + return true, fake.GetSecret(secretName), nil + }) + + var serviceName string + if tt.args.urlKind == envinfo.INGRESS { + serviceName = tt.args.componentName + + } else if tt.args.urlKind == envinfo.ROUTE { + var err error + serviceName, err = util.NamespaceOpenShiftObject(tt.args.componentName, tt.args.applicationName) + if err != nil { + t.Error(err) + } } fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (bool, runtime.Object, error) { @@ -165,25 +393,113 @@ func TestCreate(t *testing.T) { return true, dc, nil }) - got, err := Create(client, &kclient.Client{}, tt.args.urlName, tt.args.portNumber, tt.args.secure, tt.args.componentName, tt.args.applicationName, "", "") + fakeKClientSet.Kubernetes.PrependReactor("get", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, testingutil.CreateFakeDeployment("nodejs"), nil + }) + + urlCreateParameters := CreateParameters{ + urlName: tt.args.urlName, + portNumber: tt.args.portNumber, + secureURL: tt.args.secure, + componentName: tt.args.componentName, + applicationName: tt.args.applicationName, + host: tt.args.host, + secretName: tt.args.tlsSecret, + urlKind: tt.args.urlKind, + } + + got, err := Create(client, fakeKClient, urlCreateParameters, tt.args.isRouteSupported, tt.args.isExperimentalModeEnabled) if err == nil && !tt.wantErr { - if len(fakeClientSet.RouteClientset.Actions()) != 1 { - t.Errorf("expected 1 RouteClientset.Actions() in CreateService, got: %v", fakeClientSet.RouteClientset.Actions()) - } + if tt.args.urlKind == envinfo.INGRESS { + wantKubernetesActionLength := 0 + if !tt.args.secure { + wantKubernetesActionLength = 2 + } else { + if tt.args.tlsSecret != "" && tt.userGivenTLSExists { + wantKubernetesActionLength = 3 + } else if !tt.defaultTLSExists { + wantKubernetesActionLength = 4 + } else { + wantKubernetesActionLength = 3 + } + } + if len(fakeKClientSet.Kubernetes.Actions()) != wantKubernetesActionLength { + t.Errorf("expected %v Kubernetes.Actions() in Create, got: %v", wantKubernetesActionLength, len(fakeKClientSet.Kubernetes.Actions())) + } + + if len(fakeClientSet.RouteClientset.Actions()) != 0 { + t.Errorf("expected 0 RouteClientset.Actions() in CreateService, got: %v", fakeClientSet.RouteClientset.Actions()) + } + + var createdIngress *extensionsv1.Ingress + createIngressActionNo := 0 + if !tt.args.secure { + createIngressActionNo = 1 + } else { + if tt.args.tlsSecret != "" { + createIngressActionNo = 2 + } else if !tt.defaultTLSExists { + createdDefaultTLS := fakeKClientSet.Kubernetes.Actions()[2].(ktesting.CreateAction).GetObject().(*v1.Secret) + if createdDefaultTLS.Name != tt.args.componentName+"-tlssecret" { + t.Errorf("default tls created with different name, want: %s,got: %s", tt.args.componentName+"-tlssecret", createdDefaultTLS.Name) + } + createIngressActionNo = 3 + } else { + createIngressActionNo = 2 + } + } + createdIngress = fakeKClientSet.Kubernetes.Actions()[createIngressActionNo].(ktesting.CreateAction).GetObject().(*extensionsv1.Ingress) + + if !reflect.DeepEqual(createdIngress.Name, tt.returnedIngress.Name) { + t.Errorf("ingress name not matching, expected: %s, got %s", tt.returnedRoute.Name, createdIngress.Name) + } + if !reflect.DeepEqual(createdIngress.Labels, tt.returnedIngress.Labels) { + t.Errorf("ingress labels not matching, %v", pretty.Compare(tt.returnedIngress.Labels, createdIngress.Labels)) + } + + wantedIngressParams := kclient.IngressParameter{ + ServiceName: serviceName, + IngressDomain: tt.args.host, + PortNumber: intstr.FromInt(tt.args.portNumber), + TLSSecretName: tt.args.tlsSecret, + } + + if !reflect.DeepEqual(createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal, wantedIngressParams.PortNumber.IntVal) { + t.Errorf("ingress port not matching, expected: %s, got %s", tt.returnedRoute.Spec.Port, createdIngress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.StrVal) + } + if tt.args.secure { + if wantedIngressParams.TLSSecretName == "" { + wantedIngressParams.TLSSecretName = tt.args.componentName + "-tlssecret" + } + if !reflect.DeepEqual(createdIngress.Spec.TLS[0].SecretName, wantedIngressParams.TLSSecretName) { + t.Errorf("ingress tls name not matching, expected: %s, got %s", wantedIngressParams.TLSSecretName, createdIngress.Spec.TLS) + } + } + + } else { + if len(fakeClientSet.RouteClientset.Actions()) != 1 { + t.Errorf("expected 1 RouteClientset.Actions() in CreateService, got: %v", fakeClientSet.RouteClientset.Actions()) + } + + if len(fakeKClientSet.Kubernetes.Actions()) != 0 { + t.Errorf("expected 0 Kubernetes.Actions() in CreateService, got: %v", len(fakeKClientSet.Kubernetes.Actions())) + } + + createdRoute := fakeClientSet.RouteClientset.Actions()[0].(ktesting.CreateAction).GetObject().(*routev1.Route) + if !reflect.DeepEqual(createdRoute.Name, tt.returnedRoute.Name) { + t.Errorf("route name not matching, expected: %s, got %s", tt.returnedRoute.Name, createdRoute.Name) + } + if !reflect.DeepEqual(createdRoute.Labels, tt.returnedRoute.Labels) { + t.Errorf("route labels not matching, %v", pretty.Compare(tt.returnedRoute.Labels, createdRoute.Labels)) + } + if !reflect.DeepEqual(createdRoute.Spec.Port, tt.returnedRoute.Spec.Port) { + t.Errorf("route port not matching, expected: %s, got %s", tt.returnedRoute.Spec.Port, createdRoute.Spec.Port) + } + if !reflect.DeepEqual(createdRoute.Spec.To.Name, tt.returnedRoute.Spec.To.Name) { + t.Errorf("route spec not matching, expected: %s, got %s", tt.returnedRoute.Spec.To.Name, createdRoute.Spec.To.Name) + } - createdRoute := fakeClientSet.RouteClientset.Actions()[0].(ktesting.CreateAction).GetObject().(*routev1.Route) - if !reflect.DeepEqual(createdRoute.Name, tt.returnedRoute.Name) { - t.Errorf("route name not matching, expected: %s, got %s", tt.returnedRoute.Name, createdRoute.Name) - } - if !reflect.DeepEqual(createdRoute.Labels, tt.returnedRoute.Labels) { - t.Errorf("route name not matching, %v", pretty.Compare(tt.returnedRoute.Labels, createdRoute.Labels)) - } - if !reflect.DeepEqual(createdRoute.Spec.Port, tt.returnedRoute.Spec.Port) { - t.Errorf("route name not matching, expected: %s, got %s", tt.returnedRoute.Spec.Port, createdRoute.Spec.Port) - } - if !reflect.DeepEqual(createdRoute.Spec.To.Name, tt.returnedRoute.Spec.To.Name) { - t.Errorf("route name not matching, expected: %s, got %s", tt.returnedRoute.Spec.To.Name, createdRoute.Spec.To.Name) } if !reflect.DeepEqual(got, tt.want) { @@ -198,49 +514,6 @@ func TestCreate(t *testing.T) { } } -func TestDelete(t *testing.T) { - type args struct { - urlName string - applicationName string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "first test", - args: args{ - urlName: "component", - applicationName: "appname", - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, fakeClientSet := occlient.FakeNew() - - fakeClientSet.RouteClientset.PrependReactor("delete", "routes", func(action ktesting.Action) (bool, runtime.Object, error) { - return true, nil, nil - }) - - err := Delete(client, &kclient.Client{}, tt.args.urlName, tt.args.applicationName) - if (err != nil) != tt.wantErr { - t.Errorf("Delete() error = %#v, wantErr %#v", err, tt.wantErr) - return - } - - // Check for value with which the function has called - DeletedURL := fakeClientSet.RouteClientset.Actions()[0].(ktesting.DeleteAction).GetName() - if !reflect.DeepEqual(DeletedURL, tt.args.urlName+"-"+tt.args.applicationName) { - t.Errorf("Delete is been called with %#v, expected %#v", DeletedURL, tt.args.urlName+"-"+tt.args.applicationName) - } - }) - } -} -*/ - func TestExists(t *testing.T) { tests := []struct { name string @@ -461,3 +734,610 @@ func TestGetValidPortNumber(t *testing.T) { }) } } + +func TestPush(t *testing.T) { + type args struct { + isRouteSupported bool + isExperimentalModeEnabled bool + } + tests := []struct { + name string + args args + componentName string + applicationName string + existingConfigURLs []config.ConfigURL + existingEnvInfoURLs []envinfo.EnvInfoURL + returnedRoutes *routev1.RouteList + returnedIngress *extensionsv1.IngressList + deletedURLs []URL + createdURLs []URL + wantErr bool + }{ + { + name: "no urls on local config and cluster", + args: args{ + isRouteSupported: true, + }, + componentName: "nodejs", + applicationName: "app", + returnedRoutes: &routev1.RouteList{}, + }, + { + name: "2 urls on local config and 0 on openshift cluster", + componentName: "nodejs", + applicationName: "app", + args: args{isRouteSupported: true}, + existingConfigURLs: []config.ConfigURL{ + { + Name: "example", + Port: 8080, + Secure: false, + }, + { + Name: "example-1", + Port: 8080, + Secure: false, + }, + }, + returnedRoutes: &routev1.RouteList{}, + }, + { + name: "0 url on local config and 2 on openshift cluster", + componentName: "wildfly", + applicationName: "app", + args: args{isRouteSupported: true}, + returnedRoutes: testingutil.GetRouteListWithMultiple("wildfly", "app"), + deletedURLs: []URL{ + getMachineReadableFormat(testingutil.GetSingleRoute("example-app", 8080, "nodejs", "app")), + getMachineReadableFormat(testingutil.GetSingleRoute("example-1-app", 9100, "nodejs", "app")), + }, + }, + { + name: "2 url on local config and 2 on openshift cluster, but they are different", + componentName: "nodejs", + applicationName: "app", + args: args{isRouteSupported: true}, + existingConfigURLs: []config.ConfigURL{ + { + Name: "example-local-0", + Port: 8080, + Secure: false, + }, + { + Name: "example-local-1", + Port: 9090, + Secure: false, + }, + }, + returnedRoutes: testingutil.GetRouteListWithMultiple("nodejs", "app"), + deletedURLs: []URL{ + getMachineReadableFormat(testingutil.GetSingleRoute("example-app", 8080, "nodejs", "app")), + getMachineReadableFormat(testingutil.GetSingleRoute("example-1-app", 9100, "nodejs", "app")), + }, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-0-app", + }, + Spec: URLSpec{ + Port: 8080, + Secure: false, + urlKind: envinfo.ROUTE, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-1-app", + }, + Spec: URLSpec{ + Port: 9090, + Secure: false, + urlKind: envinfo.ROUTE, + }, + }, + }, + }, + + { + name: "0 urls on env file and cluster", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{}, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + }, + { + name: "2 urls on env file and 0 on openshift cluster", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example", + Port: 8080, + Secure: false, + Host: "com", + Kind: envinfo.INGRESS, + }, + { + Name: "example-1", + Port: 9090, + Secure: false, + Host: "com", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + Spec: URLSpec{ + Port: 8080, + Secure: false, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-1", + }, + Spec: URLSpec{ + Port: 9090, + Secure: false, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + }, + }, + { + name: "0 urls on env file and 2 on openshift cluster", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{}, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: fake.GetIngressListWithMultiple("nodejs"), + deletedURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-0", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-1", + }, + }, + }, + }, + { + name: "2 urls on env file and 2 on openshift cluster, but they are different", + componentName: "wildfly", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example-local-0", + Port: 8080, + Secure: false, + Host: "com", + Kind: envinfo.INGRESS, + }, + { + Name: "example-local-1", + Port: 9090, + Secure: false, + Host: "com", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: fake.GetIngressListWithMultiple("wildfly"), + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-0", + }, + Spec: URLSpec{ + Port: 8080, + Secure: false, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-1", + }, + Spec: URLSpec{ + Port: 9090, + Secure: false, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + }, + deletedURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-0", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-1", + }, + }, + }, + }, + { + name: "2 (1 ingress,1 route) urls on env file and 2 on openshift cluster (1 ingress,1 route), but they are different", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example-local-0", + Port: 8080, + Secure: false, + Kind: envinfo.ROUTE, + }, + { + Name: "example-local-1", + Port: 9090, + Secure: false, + Host: "com", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: fake.GetIngressListWithMultiple("nodejs"), + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-0", + }, + Spec: URLSpec{ + Port: 8080, + Secure: false, + urlKind: envinfo.ROUTE, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-local-1", + }, + Spec: URLSpec{ + Port: 9090, + Secure: false, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + }, + deletedURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-0", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-1", + }, + }, + }, + }, + { + name: "create a ingress on a kubernetes cluster", + componentName: "nodejs", + args: args{isRouteSupported: false, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example", + Port: 8080, + Secure: true, + Host: "com", + TLSSecret: "secret", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + Spec: URLSpec{ + Port: 8080, + Secure: true, + Host: "com", + tLSSecret: "secret", + urlKind: envinfo.INGRESS, + }, + }, + }, + }, + + { + name: "url with same name exists on env and cluster but with different specs", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example-local-0", + Port: 8080, + Secure: false, + Kind: envinfo.ROUTE, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{ + Items: []extensionsv1.Ingress{ + *fake.GetSingleIngress("example-local-0", "nodejs"), + }, + }, + createdURLs: []URL{}, + deletedURLs: []URL{}, + wantErr: true, + }, + { + name: "url with same name exists on config and cluster but with different specs", + componentName: "nodejs", + applicationName: "app", + args: args{isRouteSupported: true, isExperimentalModeEnabled: false}, + existingConfigURLs: []config.ConfigURL{ + { + Name: "example-local-0", + Port: 8080, + Secure: false, + }, + }, + returnedRoutes: &routev1.RouteList{ + Items: []routev1.Route{ + testingutil.GetSingleRoute("example-local-0", 9090, "nodejs", "app"), + }, + }, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{}, + deletedURLs: []URL{}, + wantErr: true, + }, + + { + name: "create a secure route url", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example", + Port: 8080, + Secure: true, + Kind: envinfo.ROUTE, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + Spec: URLSpec{ + Port: 8080, + Secure: true, + urlKind: envinfo.ROUTE, + }, + }, + }, + }, + { + name: "create a secure ingress url with empty user given tls secret", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example", + Port: 8080, + Secure: true, + Host: "com", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + Spec: URLSpec{ + Port: 8080, + Secure: true, + Host: "com", + urlKind: envinfo.INGRESS, + }, + }, + }, + }, + { + name: "create a secure ingress url with user given tls secret", + componentName: "nodejs", + args: args{isRouteSupported: true, isExperimentalModeEnabled: true}, + existingEnvInfoURLs: []envinfo.EnvInfoURL{ + { + Name: "example", + Port: 8080, + Secure: true, + Host: "com", + TLSSecret: "secret", + Kind: envinfo.INGRESS, + }, + }, + returnedRoutes: &routev1.RouteList{}, + returnedIngress: &extensionsv1.IngressList{}, + createdURLs: []URL{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + }, + Spec: URLSpec{ + Port: 8080, + Secure: true, + Host: "com", + tLSSecret: "secret", + urlKind: envinfo.INGRESS, + }, + }, + }, + }, + } + for testNum, tt := range tests { + tt.name = fmt.Sprintf("case %d: ", testNum+1) + tt.name + t.Run(tt.name, func(t *testing.T) { + fakeClient, fakeClientSet := occlient.FakeNew() + fakeKClient, fakeKClientSet := kclient.FakeNew() + + fakeKClientSet.Kubernetes.PrependReactor("list", "ingresses", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + return true, tt.returnedIngress, nil + }) + + fakeKClientSet.Kubernetes.PrependReactor("delete", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + + fakeClientSet.RouteClientset.PrependReactor("list", "routes", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, tt.returnedRoutes, nil + }) + + fakeClientSet.RouteClientset.PrependReactor("delete", "routes", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + + fakeKClientSet.Kubernetes.PrependReactor("get", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) { + if tt.existingEnvInfoURLs[0].TLSSecret != "" { + return true, fake.GetSecret(tt.existingEnvInfoURLs[0].TLSSecret), nil + } + return true, fake.GetSecret(tt.componentName + "-tlssecret"), nil + }) + + fakeClientSet.AppsClientset.PrependReactor("get", "deploymentconfigs", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + return true, testingutil.OneFakeDeploymentConfigWithMounts(tt.componentName, "local", tt.applicationName, map[string]*v1.PersistentVolumeClaim{}), nil + }) + + fakeKClientSet.Kubernetes.PrependReactor("get", "deployments", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + return true, testingutil.CreateFakeDeployment(tt.componentName), nil + }) + + if err := Push(fakeClient, fakeKClient, PushParameters{ + ComponentName: tt.componentName, + ApplicationName: tt.applicationName, + ConfigURLs: tt.existingConfigURLs, + EnvURLS: tt.existingEnvInfoURLs, + IsRouteSupported: tt.args.isRouteSupported, + IsExperimentalModeEnabled: tt.args.isExperimentalModeEnabled, + }); (err != nil) != tt.wantErr { + t.Errorf("Push() error = %v, wantErr %v", err, tt.wantErr) + } else { + deletedURLMap := make(map[string]bool) + for _, url := range tt.deletedURLs { + found := false + for _, action := range fakeKClientSet.Kubernetes.Actions() { + value, ok := action.(ktesting.DeleteAction) + if ok && value.GetVerb() == "delete" { + deletedURLMap[value.GetName()] = true + if value.GetName() == url.Name { + found = true + break + } + } + } + + for _, action := range fakeClientSet.RouteClientset.Actions() { + value, ok := action.(ktesting.DeleteAction) + if ok && value.GetVerb() == "delete" { + deletedURLMap[value.GetName()] = true + if value.GetName() == url.Name { + found = true + break + } + } + } + if !found { + t.Errorf("the url %s was not deleted", url.Name) + } + } + + if len(deletedURLMap) != len(tt.deletedURLs) { + t.Errorf("number of deleted urls is different, want: %d,got: %d", len(tt.deletedURLs), len(deletedURLMap)) + } + + createdURLMap := make(map[string]bool) + for _, url := range tt.createdURLs { + found := false + for _, action := range fakeKClientSet.Kubernetes.Actions() { + value, ok := action.(ktesting.CreateAction) + if ok { + createdObject, ok := value.GetObject().(*extensionsv1.Ingress) + if ok { + createdURLMap[createdObject.Name] = true + if createdObject.Name == url.Name && + (createdObject.Spec.TLS != nil) == url.Spec.Secure && + int(createdObject.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal) == url.Spec.Port && + envinfo.INGRESS == url.Spec.urlKind && + fmt.Sprintf("%v.%v", url.Name, url.Spec.Host) == createdObject.Spec.Rules[0].Host { + + if url.Spec.Secure { + secretName := tt.componentName + "-tlssecret" + if url.Spec.tLSSecret != "" { + secretName = url.Spec.tLSSecret + } + if createdObject.Spec.TLS[0].SecretName == secretName { + found = true + break + } + } else { + found = true + break + } + } + } + } + } + + for _, action := range fakeClientSet.RouteClientset.Actions() { + value, ok := action.(ktesting.CreateAction) + if ok { + createdObject, ok := value.GetObject().(*routev1.Route) + if ok { + createdURLMap[createdObject.Name] = true + if createdObject.Name == url.Name && + (createdObject.Spec.TLS != nil) == url.Spec.Secure && + int(createdObject.Spec.Port.TargetPort.IntVal) == url.Spec.Port && + envinfo.ROUTE == url.Spec.urlKind { + found = true + break + } + } + } + } + if !found { + t.Errorf("the url %s was not created with proper specs", url.Name) + } + } + + if len(createdURLMap) != len(tt.createdURLs) { + t.Errorf("number of created urls is different, want: %d,got: %d", len(tt.deletedURLs), len(deletedURLMap)) + } + + if !tt.args.isRouteSupported { + if len(fakeClientSet.RouteClientset.Actions()) > 0 { + t.Errorf("route is not supproted, total actions on the routeClient should be 0") + } + } + } + }) + } +} diff --git a/rpms/openshift-odo.spec b/rpms/openshift-odo.spec index 907dae1ac3a..24e3f961528 100644 --- a/rpms/openshift-odo.spec +++ b/rpms/openshift-odo.spec @@ -3,7 +3,8 @@ %global debug_package %{nil} %global package_name openshift-odo %global product_name odo -%global golang_version 1.12 +%global golang_version ${GOLANG_VERSION} +%global golang_version_nodot ${GOLANG_VERSION_NODOT} %global odo_version ${ODO_RPM_VERSION} %global odo_release ${ODO_RELEASE} %global git_commit ${GIT_COMMIT} @@ -26,7 +27,7 @@ Provides: %{package_name} Obsoletes: %{package_name} %description -OpenShift Do (odo) is a fast, iterative, and straightforward CLI tool for developers who write, build, and deploy applications on OpenShift. +odo is a fast, iterative, and straightforward CLI tool for developers who write, build, and deploy applications on OpenShift. %prep %setup -q -n %{source_dir} diff --git a/scripts/openshiftci-presubmit-integration-tests.sh b/scripts/openshiftci-presubmit-integration-tests.sh new file mode 100644 index 00000000000..eae5b32b05f --- /dev/null +++ b/scripts/openshiftci-presubmit-integration-tests.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# fail if some commands fails +set -e +# show commands +set -x + +export CI="openshift" +make configure-installer-tests-cluster +make bin +mkdir -p $GOPATH/bin +go get -u github.com/onsi/ginkgo/ginkgo +export PATH="$PATH:$(pwd):$GOPATH/bin" +export ARTIFACTS_DIR="/tmp/artifacts" +export CUSTOM_HOMEDIR=$ARTIFACTS_DIR + +# Integration tests +make test-integration +make test-integration-devfile +make test-cmd-login-logout +make test-cmd-project +make test-operator-hub + +odo logout diff --git a/scripts/rpm-prepare.sh b/scripts/rpm-prepare.sh index 39b31165552..09675480509 100755 --- a/scripts/rpm-prepare.sh +++ b/scripts/rpm-prepare.sh @@ -11,12 +11,19 @@ export ODO_RELEASE=${ODO_RELEASE:=1} export GIT_COMMIT=${GIT_COMMIT:=`git rev-parse --short HEAD 2>/dev/null`} export ODO_RPM_VERSION=${ODO_VERSION//-} +# Golang version variables, if you are bumping this, please contact redhat maintainers to ensure that internal +# build systems can handle these versions +export GOLANG_VERSION=${GOLANG_VERSION:-1.12} +export GOLANG_VERSION_NODOT=${GOLANG_VERSION_NODOT:-112} + # Print env for verifcation echo "Printing envs for verification" echo "ODO_VERSION=$ODO_VERSION" echo "ODO_RELEASE=$ODO_RELEASE" echo "GIT_COMMIT=$GIT_COMMIT" echo "ODO_RPM_VERSION=$ODO_RPM_VERSION" +echo "GOLANG_VERSION=$GOLANG_VERSION" +echo "GOLANG_VERSION_NODO=$GOLANG_VERSION_NODOT" OUT_DIR=".rpmbuild" DIST_DIR="$(pwd)/dist" @@ -73,6 +80,8 @@ echo "ODO_VERSION=$ODO_VERSION" > $OUT_DIR/version echo "ODO_RELEASE=$ODO_RELEASE" >> $OUT_DIR/version echo "GIT_COMMIT=$GIT_COMMIT" >> $OUT_DIR/version echo "ODO_RPM_VERSION=$ODO_RPM_VERSION" >> $OUT_DIR/version +echo "GOLANG_VERSION=$GOLANG_VERSION" >> $OUT_DIR/version +echo "GOLANG_VERSION_NODOT=$GOLANG_VERSION_NODOT" >> $OUT_DIR/version # After success copy stuff to actual location diff --git a/scripts/sync-docs.sh b/scripts/sync-docs.sh index 4fdbf5a4243..8ccce4dcb8c 100644 --- a/scripts/sync-docs.sh +++ b/scripts/sync-docs.sh @@ -117,7 +117,7 @@ cd .. # #toc_footers: # - openshiftdo.org -# - Odo (OpenShift Do) on GitHub +# - odo on GitHub # #search: true #--- diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-restart.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-restart.yaml new file mode 100644 index 00000000000..00c4fd521cf --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-restart.yaml @@ -0,0 +1,30 @@ +apiVersion: 1.0.0 +metadata: + name: test-devfile +projects: + - + name: nodejs-web-app + source: + type: git + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - type: dockerimage + image: quay.io/eclipse/che-nodejs10-ubi:nightly + alias: runtime + memoryLimit: 1024Mi + mountSources: true +commands: + - name: devbuild + actions: + - type: exec + component: runtime + command: "npm install" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + - name: devrun + attributes: + restart: "false" + actions: + - type: exec + component: runtime + command: "nodemon app.js" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml index ddb79928576..2be1a18987c 100644 --- a/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml +++ b/tests/examples/source/devfiles/nodejs/devfile-with-volumes.yaml @@ -33,13 +33,19 @@ components: - name: myvol2 containerPath: /data2 commands: - - name: devbuild + - name: devInit + actions: + - type: exec + component: runtime + command: "echo init >> myfile-init.log" + workdir: /data + - name: devBuild actions: - type: exec component: runtime command: "echo hello >> myfile.log" workdir: /data - - name: devrun + - name: devRun actions: - type: exec component: runtime2 diff --git a/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml b/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml new file mode 100644 index 00000000000..a05b737ae1c --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-without-devinit.yaml @@ -0,0 +1,34 @@ +apiVersion: 1.0.0 +metadata: + name: test-devfile +projects: + - + name: nodejs-web-app + source: + type: git + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - type: dockerimage + image: quay.io/eclipse/che-nodejs10-ubi:nightly + endpoints: + - name: "3000/tcp" + port: 3000 + alias: runtime + env: + - name: FOO + value: "bar" + memoryLimit: 1024Mi + mountSources: true +commands: + - name: devbuild + actions: + - type: exec + component: runtime + command: "npm install" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + - name: devrun + actions: + - type: exec + component: runtime + command: "nodemon app.js" + workdir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app \ No newline at end of file diff --git a/tests/examples/source/devfiles/springboot/devfile-init-without-build.yaml b/tests/examples/source/devfiles/springboot/devfile-init-without-build.yaml new file mode 100644 index 00000000000..a6f032a0415 --- /dev/null +++ b/tests/examples/source/devfiles/springboot/devfile-init-without-build.yaml @@ -0,0 +1,55 @@ +--- +apiVersion: 1.0.0 +metadata: + generateName: java-spring-boot +projects: + - + name: springbootproject + source: + type: git + location: "https://github.com/maysunfaisal/springboot.git" +components: + - + type: chePlugin + id: redhat/java/latest + memoryLimit: 1512Mi + - + type: dockerimage + image: maysunfaisal/springbootbuild + alias: tools + memoryLimit: 768Mi + command: ['tail'] + args: [ '-f', '/dev/null'] + mountSources: true + volumes: + - name: springbootpvc + containerPath: /data + - + type: dockerimage + image: maysunfaisal/springbootruntime + alias: runtime + memoryLimit: 768Mi + endpoints: + - name: '8080/tcp' + port: 8080 + mountSources: false + volumes: + - name: springbootpvc + containerPath: /data +commands: + - + name: devInit + actions: + - + type: exec + component: tools + command: "echo hello; touch /data/afile.txt" + workdir: /projects/springbootproject + - + name: devRun + actions: + - + type: exec + component: runtime + command: "/artifacts/bin/start-server.sh" + workdir: / diff --git a/tests/examples/source/devfiles/springboot/devfile-init.yaml b/tests/examples/source/devfiles/springboot/devfile-init.yaml new file mode 100644 index 00000000000..fd4c494bbb7 --- /dev/null +++ b/tests/examples/source/devfiles/springboot/devfile-init.yaml @@ -0,0 +1,63 @@ +--- +apiVersion: 1.0.0 +metadata: + generateName: java-spring-boot +projects: + - + name: springbootproject + source: + type: git + location: "https://github.com/maysunfaisal/springboot.git" +components: + - + type: chePlugin + id: redhat/java/latest + memoryLimit: 1512Mi + - + type: dockerimage + image: maysunfaisal/springbootbuild + alias: tools + memoryLimit: 768Mi + command: ['tail'] + args: [ '-f', '/dev/null'] + mountSources: true + volumes: + - name: springbootpvc + containerPath: /data + - + type: dockerimage + image: maysunfaisal/springbootruntime + alias: runtime + memoryLimit: 768Mi + endpoints: + - name: '8080/tcp' + port: 8080 + mountSources: false + volumes: + - name: springbootpvc + containerPath: /data +commands: + - + name: devinit + actions: + - + type: exec + component: tools + command: "echo hello" + workdir: /projects/springbootproject + - + name: devbuild + actions: + - + type: exec + component: tools + command: "/artifacts/bin/build-container-full.sh" + workdir: /projects/springbootproject + - + name: devrun + actions: + - + type: exec + component: runtime + command: "/artifacts/bin/start-server.sh" + workdir: / diff --git a/tests/helper/helper_generic.go b/tests/helper/helper_generic.go index 452f9b63c81..09fa06ebc62 100644 --- a/tests/helper/helper_generic.go +++ b/tests/helper/helper_generic.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math/rand" + "os" "os/exec" "strings" "time" @@ -159,3 +160,10 @@ func WatchNonRetCmdStdOut(cmdStr string, timeout time.Duration, check func(outpu } } } + +// GetUserHomeDir gets the user home directory +func GetUserHomeDir() string { + homeDir, err := os.UserHomeDir() + Expect(err).NotTo(HaveOccurred()) + return homeDir +} diff --git a/tests/helper/kubernetes_utils.go b/tests/helper/kubernetes_utils.go index 49f78f80e54..5900ec4f150 100644 --- a/tests/helper/kubernetes_utils.go +++ b/tests/helper/kubernetes_utils.go @@ -9,8 +9,10 @@ import ( ) // CopyKubeConfigFile copies default kubeconfig file into current context config file -func CopyKubeConfigFile(kubeConfigFile, tempConfigFile string, info os.FileInfo) string { - err := copyFile(kubeConfigFile, tempConfigFile, info) +func CopyKubeConfigFile(kubeConfigFile, tempConfigFile string) string { + info, err := os.Stat(kubeConfigFile) + Expect(err).NotTo(HaveOccurred()) + err = copyFile(kubeConfigFile, tempConfigFile, info) Expect(err).NotTo(HaveOccurred()) os.Setenv("KUBECONFIG", tempConfigFile) return tempConfigFile diff --git a/tests/integration/cmd_debug_test.go b/tests/integration/cmd_debug_test.go index 4cda11350e0..ef3b6151b8a 100644 --- a/tests/integration/cmd_debug_test.go +++ b/tests/integration/cmd_debug_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "time" . "github.com/onsi/ginkgo" @@ -38,6 +39,33 @@ var _ = Describe("odo debug command tests", func() { }) Context("odo debug on a nodejs:latest component", func() { + It("check that machine output debug information works", func() { + helper.CopyExample(filepath.Join("source", "nodejs"), context) + helper.CmdShouldPass("odo", "component", "create", "nodejs:latest", "--project", project, "--context", context) + helper.CmdShouldPass("odo", "push", "--context", context) + + httpPort, err := util.HttpGetFreePort() + Expect(err).NotTo(HaveOccurred()) + freePort := strconv.Itoa(httpPort) + + stopChannel := make(chan bool) + go func() { + helper.CmdShouldRunAndTerminate(60*time.Second, stopChannel, "odo", "debug", "port-forward", "--local-port", freePort, "--context", context) + }() + + // Make sure that the debug information output, outputs correctly. + // We do *not* check the json output since the debugProcessID will be different each time. + helper.WaitForCmdOut("odo", []string{"debug", "info", "--context", context, "-o", "json"}, 1, true, func(output string) bool { + if strings.Contains(output, `"kind": "OdoDebugInfo"`) && + strings.Contains(output, `"localPort": `+freePort) { + return true + } + return false + }) + + stopChannel <- true + }) + It("should expect a ws connection when tried to connect on different debug port locally and remotely", func() { helper.CopyExample(filepath.Join("source", "nodejs"), context) helper.CmdShouldPass("odo", "component", "create", "nodejs:latest", "--project", project, "--context", context) @@ -119,7 +147,7 @@ var _ = Describe("odo debug command tests", func() { runningString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) Expect(runningString).To(ContainSubstring(freePort)) stopChannel <- true - failString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) + failString := helper.CmdShouldFail("odo", "debug", "info", "--context", context) Expect(failString).To(ContainSubstring("not running")) // according to https://golang.org/pkg/os/#Signal On Windows, sending os.Interrupt to a process with os.Process.Signal is not implemented diff --git a/tests/integration/component.go b/tests/integration/component.go index 5cb07d3f833..03f9a0fe228 100644 --- a/tests/integration/component.go +++ b/tests/integration/component.go @@ -235,6 +235,7 @@ func componentTests(args ...string) { It("should describe the component when it is not pushed", func() { helper.CmdShouldPass("odo", append(args, "create", "nodejs", "cmp-git", "--project", project, "--git", "https://github.com/openshift/nodejs-ex", "--context", context, "--app", "testing")...) helper.CmdShouldPass("odo", "url", "create", "url-1", "--context", context) + helper.CmdShouldPass("odo", "url", "create", "url-2", "--context", context) helper.CmdShouldPass("odo", "storage", "create", "storage-1", "--size", "1Gi", "--path", "/data1", "--context", context) helper.ValidateLocalCmpExist(context, "Type,nodejs", "Name,cmp-git", "Application,testing", "URL,0,Name,url-1") cmpDescribe := helper.CmdShouldPass("odo", append(args, "describe", "--context", context)...) @@ -242,14 +243,20 @@ func componentTests(args ...string) { Expect(cmpDescribe).To(ContainSubstring("cmp-git")) Expect(cmpDescribe).To(ContainSubstring("nodejs")) Expect(cmpDescribe).To(ContainSubstring("url-1")) + Expect(cmpDescribe).To(ContainSubstring("url-2")) Expect(cmpDescribe).To(ContainSubstring("https://github.com/openshift/nodejs-ex")) Expect(cmpDescribe).To(ContainSubstring("storage-1")) cmpDescribeJSON, err := helper.Unindented(helper.CmdShouldPass("odo", append(args, "describe", "-o", "json", "--context", context)...)) Expect(err).Should(BeNil()) - expected, err := helper.Unindented(`{"kind": "Component","apiVersion": "odo.openshift.io/v1alpha1","metadata": {"name": "cmp-git","namespace": "` + project + `","creationTimestamp": null},"spec":{"app": "testing","type":"nodejs","source": "https://github.com/openshift/nodejs-ex","sourceType": "git","url": ["url-1"],"storage": ["storage-1"],"ports": ["8080/TCP"]},"status": {"state": "Not Pushed"}}`) + expected, err := helper.Unindented(`{"kind": "Component","apiVersion": "odo.openshift.io/v1alpha1","metadata": {"name": "cmp-git","namespace": "` + project + `","creationTimestamp": null},"spec":{"app": "testing","type":"nodejs","source": "https://github.com/openshift/nodejs-ex","sourceType": "git","url": ["url-1", "url-2"],"storage": ["storage-1"],"ports": ["8080/TCP"]},"status": {"state": "Not Pushed"}}`) Expect(err).Should(BeNil()) Expect(cmpDescribeJSON).To(Equal(expected)) + + // odo should describe not pushed component if component name is given. + helper.CmdShouldPass("odo", append(args, "describe", "cmp-git", "--context", context)...) + Expect(cmpDescribe).To(ContainSubstring("cmp-git")) + helper.CmdShouldPass("odo", append(args, "delete", "-f", "--all", "--context", context)...) }) diff --git a/tests/integration/debug/cmd_debug_test.go b/tests/integration/debug/cmd_debug_test.go index 6a4c6cbbe86..515ba65ed7e 100644 --- a/tests/integration/debug/cmd_debug_test.go +++ b/tests/integration/debug/cmd_debug_test.go @@ -65,9 +65,6 @@ var _ = Describe("odo debug command serial tests", func() { listenerStarted = true } - debugInfoString := helper.CmdShouldPass("odo", "debug", "info", "--context", context) - Expect(debugInfoString).To(ContainSubstring("")) - freePort := "" helper.WaitForCmdOut("odo", []string{"debug", "info", "--context", context}, 1, true, func(output string) bool { if strings.Contains(output, "Debug is running") { diff --git a/tests/integration/devfile/cmd_devfile_catalog_test.go b/tests/integration/devfile/cmd_devfile_catalog_test.go index b990eaea575..d2aa5f7a85a 100644 --- a/tests/integration/devfile/cmd_devfile_catalog_test.go +++ b/tests/integration/devfile/cmd_devfile_catalog_test.go @@ -22,9 +22,8 @@ var _ = Describe("odo devfile catalog command tests", func() { os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") if os.Getenv("KUBERNETES") == "true" { - info, err := os.Stat(os.Getenv("KUBECONFIG")) - Expect(err).NotTo(HaveOccurred()) - kubeConfigFile := helper.CopyKubeConfigFile(os.Getenv("KUBECONFIG"), filepath.Join(context, "config"), info) + homeDir := helper.GetUserHomeDir() + kubeConfigFile := helper.CopyKubeConfigFile(filepath.Join(homeDir, ".kube", "config"), filepath.Join(context, "config")) project = helper.CreateRandNamespace(kubeConfigFile) } else { project = helper.CreateRandProject() diff --git a/tests/integration/devfile/cmd_devfile_push_test.go b/tests/integration/devfile/cmd_devfile_push_test.go index 1910c950ea7..7e54b07b4ea 100644 --- a/tests/integration/devfile/cmd_devfile_push_test.go +++ b/tests/integration/devfile/cmd_devfile_push_test.go @@ -188,6 +188,69 @@ var _ = Describe("odo devfile push command tests", func() { Expect(cmdOutput).To(ContainSubstring("/myproject/app.jar")) }) + It("should execute devinit command if present", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/maysunfaisal/springboot.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "java-spring-boot", "--project", namespace, cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "springboot"), projectDirPath) + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init.yaml", "--namespace", namespace) + Expect(output).To(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devbuild command \"/artifacts/bin/build-container-full.sh\"")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + }) + + It("should execute devinit and devrun commands if present", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/maysunfaisal/springboot.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "java-spring-boot", "--project", namespace, cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "springboot"), projectDirPath) + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init-without-build.yaml", "--namespace", namespace) + Expect(output).To(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + }) + + It("should only execute devinit command once if component is already created", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/maysunfaisal/springboot.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "java-spring-boot", "--project", namespace, cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "springboot"), projectDirPath) + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init.yaml", "--namespace", namespace) + Expect(output).To(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devbuild command \"/artifacts/bin/build-container-full.sh\"")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + + // Need to force so build and run get triggered again with the component already created. + output = helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init.yaml", "--namespace", namespace, "-f") + Expect(output).NotTo(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devbuild command \"/artifacts/bin/build-container-full.sh\"")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + }) + + It("should be able to handle a missing devinit command", func() { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + helper.RenameFile("devfile.yaml", "devfile-old.yaml") + helper.RenameFile("devfile-without-devinit.yaml", "devfile.yaml") + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--namespace", namespace) + Expect(output).NotTo(ContainSubstring("Executing devinit command")) + Expect(output).To(ContainSubstring("Executing devbuild command \"npm install\"")) + Expect(output).To(ContainSubstring("Executing devrun command \"nodemon app.js\"")) + }) + It("should be able to handle a missing devbuild command", func() { utils.ExecWithMissingBuildCommand(projectDirPath, cmpName, namespace) }) @@ -204,6 +267,10 @@ var _ = Describe("odo devfile push command tests", func() { utils.ExecWithWrongCustomCommand(projectDirPath, cmpName, namespace) }) + It("should not restart the application if restart is false", func() { + utils.ExecWithRestartAttribute(projectDirPath, cmpName, namespace) + }) + It("should create pvc and reuse if it shares the same devfile volume name", func() { helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) helper.Chdir(projectDirPath) @@ -214,7 +281,8 @@ var _ = Describe("odo devfile push command tests", func() { helper.RenameFile("devfile.yaml", "devfile-old.yaml") helper.RenameFile("devfile-with-volumes.yaml", "devfile.yaml") - output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--project", namespace) + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--namespace", namespace) + Expect(output).To(ContainSubstring("Executing devinit command")) Expect(output).To(ContainSubstring("Executing devbuild command")) Expect(output).To(ContainSubstring("Executing devrun command")) @@ -223,6 +291,20 @@ var _ = Describe("odo devfile push command tests", func() { var statErr error var cmdOutput string + oc.CheckCmdOpInRemoteDevfilePod( + podName, + "runtime", + namespace, + []string{"cat", "/data/myfile-init.log"}, + func(cmdOp string, err error) bool { + cmdOutput = cmdOp + statErr = err + return true + }, + ) + Expect(statErr).ToNot(HaveOccurred()) + Expect(cmdOutput).To(ContainSubstring("init")) + oc.CheckCmdOpInRemoteDevfilePod( podName, "runtime2", diff --git a/tests/integration/devfile/cmd_devfile_url_test.go b/tests/integration/devfile/cmd_devfile_url_test.go index 013e4ef99d2..0bada66ad17 100644 --- a/tests/integration/devfile/cmd_devfile_url_test.go +++ b/tests/integration/devfile/cmd_devfile_url_test.go @@ -63,10 +63,10 @@ var _ = Describe("odo devfile url command tests", func() { stdout = helper.CmdShouldFail("odo", "url", "create", url1, "--port", "8080") Expect(stdout).To(ContainSubstring("is not exposed")) - stdout = helper.CmdShouldFail("odo", "url", "create", url1, "--port", "3000") + stdout = helper.CmdShouldFail("odo", "url", "create", url1, "--port", "3000", "--ingress") Expect(stdout).To(ContainSubstring("host must be provided")) - helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host) + helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--ingress") helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml") helper.WaitForCmdOut("odo", []string{"url", "list"}, 1, false, func(output string) bool { if strings.Contains(output, url1) { @@ -93,8 +93,9 @@ var _ = Describe("odo devfile url command tests", func() { helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) - helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host) + helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--ingress") helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--project", namespace) + // odo url list -o json helper.WaitForCmdOut("odo", []string{"url", "list", "-o", "json"}, 1, true, func(output string) bool { desiredURLListJSON := fmt.Sprintf(`{"kind":"List","apiVersion":"udo.udo.io/v1alpha1","metadata":{},"items":[{"kind":"Ingress","apiVersion":"extensions/v1beta1","metadata":{"name":"%s","creationTimestamp":null},"spec":{"rules":[{"host":"%s","http":{"paths":[{"path":"/","backend":{"serviceName":"%s","servicePort":3000}}]}}]},"status":{"loadBalancer":{}}}]}`, url1, url1+"."+host, componentName) @@ -120,7 +121,7 @@ var _ = Describe("odo devfile url command tests", func() { helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) - helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--secure") + helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--secure", "--ingress") stdout = helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--project", namespace) helper.MatchAllInOutput(stdout, []string{"https:", url1 + "." + host}) @@ -145,8 +146,53 @@ var _ = Describe("odo devfile url command tests", func() { helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) - stdout = helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--now") - helper.MatchAllInOutput(stdout, []string{"created for component", "http:", url1 + "." + host}) + stdout = helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--now", "--ingress", "--devfile", "devfile.yaml") + helper.MatchAllInOutput(stdout, []string{"URL " + url1 + " created for component", "http:", url1 + "." + host}) + }) + + It("should create a automatically route on a openShift cluster", func() { + + if os.Getenv("KUBERNETES") == "true" { + Skip("This is a OpenShift specific scenario, skipping") + } + + url1 := helper.RandString(5) + + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) + + helper.CmdShouldPass("odo", "url", "create", url1) + + helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--namespace", namespace) + + output := helper.CmdShouldPass("oc", "get", "routes", "--namespace", namespace) + Expect(output).Should(ContainSubstring(url1)) + + helper.CmdShouldPass("odo", "url", "delete", url1, "-f") + helper.CmdShouldPass("odo", "push", "--devfile", "devfile.yaml", "--namespace", namespace) + + output = helper.CmdShouldPass("oc", "get", "routes", "--namespace", namespace) + Expect(output).ShouldNot(ContainSubstring(url1)) + }) + + It("should create a url for a unsupported devfile component", func() { + url1 := helper.RandString(5) + + helper.CopyExample(filepath.Join("source", "python"), context) + helper.Chdir(context) + + helper.CmdShouldPass("odo", "create", "python", "--project", namespace, componentName) + + helper.CmdShouldPass("odo", "url", "create", url1) + + helper.CmdShouldPass("odo", "push", "--namespace", namespace) + + output := helper.CmdShouldPass("oc", "get", "routes", "--namespace", namespace) + Expect(output).Should(ContainSubstring(url1)) }) }) @@ -163,7 +209,7 @@ var _ = Describe("odo devfile url command tests", func() { helper.CopyExample(filepath.Join("source", "devfiles", "nodejs"), projectDirPath) - helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host) + helper.CmdShouldPass("odo", "url", "create", url1, "--port", "3000", "--host", host, "--ingress") stdout = helper.CmdShouldFail("odo", "url", "describe", url1) helper.MatchAllInOutput(stdout, []string{url1, "exists in local", "odo push"}) diff --git a/tests/integration/devfile/cmd_devfile_watch_test.go b/tests/integration/devfile/cmd_devfile_watch_test.go index d56561359d7..d05adef7e09 100644 --- a/tests/integration/devfile/cmd_devfile_watch_test.go +++ b/tests/integration/devfile/cmd_devfile_watch_test.go @@ -24,9 +24,8 @@ var _ = Describe("odo devfile watch command tests", func() { context = helper.CreateNewContext() os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) if os.Getenv("KUBERNETES") == "true" { - info, err := os.Stat(os.Getenv("KUBECONFIG")) - Expect(err).NotTo(HaveOccurred()) - kubeConfigFile := helper.CopyKubeConfigFile(os.Getenv("KUBECONFIG"), filepath.Join(context, "config"), info) + homeDir := helper.GetUserHomeDir() + kubeConfigFile := helper.CopyKubeConfigFile(filepath.Join(homeDir, ".kube", "config"), filepath.Join(context, "config")) namespace = helper.CreateRandNamespace(kubeConfigFile) } else { namespace = helper.CreateRandProject() diff --git a/tests/integration/devfile/docker/cmd_docker_devfile_catalog_test.go b/tests/integration/devfile/docker/cmd_docker_devfile_catalog_test.go new file mode 100644 index 00000000000..deaca5db10b --- /dev/null +++ b/tests/integration/devfile/docker/cmd_docker_devfile_catalog_test.go @@ -0,0 +1,49 @@ +package docker + +import ( + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openshift/odo/tests/helper" +) + +var _ = Describe("odo docker devfile catalog command tests", func() { + var context string + var currentWorkingDirectory string + + // This is run after every Spec (It) + var _ = BeforeEach(func() { + SetDefaultEventuallyTimeout(10 * time.Minute) + context = helper.CreateNewContext() + currentWorkingDirectory = helper.Getwd() + helper.Chdir(context) + os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) + + // Devfile commands require experimental mode to be set + helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") + helper.CmdShouldPass("odo", "preference", "set", "pushtarget", "docker") + }) + + // This is run after every Spec (It) + var _ = AfterEach(func() { + helper.Chdir(currentWorkingDirectory) + helper.DeleteDir(context) + }) + + Context("When executing catalog list components on Docker", func() { + It("should list all supported devfile components", func() { + output := helper.CmdShouldPass("odo", "catalog", "list", "components") + helper.MatchAllInOutput(output, []string{"Odo Devfile Components", "java-spring-boot", "openLiberty"}) + }) + }) + + Context("When executing catalog list components with -a flag on Docker", func() { + It("should list all supported and unsupported devfile components", func() { + output := helper.CmdShouldPass("odo", "catalog", "list", "components", "-a") + helper.MatchAllInOutput(output, []string{"Odo Devfile Components", "java-spring-boot", "java-maven", "php-mysql"}) + }) + }) +}) diff --git a/tests/integration/devfile/docker/cmd_docker_devfile_push_test.go b/tests/integration/devfile/docker/cmd_docker_devfile_push_test.go index da23fd2b1de..6b64745d4e4 100644 --- a/tests/integration/devfile/docker/cmd_docker_devfile_push_test.go +++ b/tests/integration/devfile/docker/cmd_docker_devfile_push_test.go @@ -151,6 +151,49 @@ var _ = Describe("odo docker devfile push command tests", func() { Expect(stdOut).To(ContainSubstring(("/myproject/app.jar"))) }) + It("should execute the optional devinit, and devrun commands if present", func() { + + helper.CmdShouldPass("git", "clone", "https://github.com/maysunfaisal/springboot.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "java-spring-boot", cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "springboot"), projectDirPath) + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init.yaml") + Expect(output).To(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devbuild command \"/artifacts/bin/build-container-full.sh\"")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + + // Check to see if it's been pushed (foobar.txt abd directory testdir) + containers := dockerClient.GetRunningContainersByCompAlias(cmpName, "runtime") + Expect(len(containers)).To(Equal(1)) + + stdOut := dockerClient.ExecContainer(containers[0], "ps -ef") + Expect(stdOut).To(ContainSubstring(("/myproject/app.jar"))) + }) + + It("should execute devinit and devrun commands if present", func() { + + helper.CmdShouldPass("git", "clone", "https://github.com/maysunfaisal/springboot.git", projectDirPath) + helper.Chdir(projectDirPath) + + helper.CmdShouldPass("odo", "create", "java-spring-boot", cmpName) + + helper.CopyExample(filepath.Join("source", "devfiles", "springboot"), projectDirPath) + + output := helper.CmdShouldPass("odo", "push", "--devfile", "devfile-init-without-build.yaml") + Expect(output).To(ContainSubstring("Executing devinit command \"echo hello")) + Expect(output).To(ContainSubstring("Executing devrun command \"/artifacts/bin/start-server.sh\"")) + + // Check to see if it's been pushed (foobar.txt abd directory testdir) + containers := dockerClient.GetRunningContainersByCompAlias(cmpName, "runtime") + Expect(len(containers)).To(Equal(1)) + + stdOut := dockerClient.ExecContainer(containers[0], "ls /data") + Expect(stdOut).To(ContainSubstring(("afile.txt"))) + }) + It("should be able to handle a missing devbuild command", func() { utils.ExecWithMissingBuildCommand(projectDirPath, cmpName, "") }) diff --git a/tests/integration/devfile/utils/utils.go b/tests/integration/devfile/utils/utils.go index d169bad533e..68950b780a4 100644 --- a/tests/integration/devfile/utils/utils.go +++ b/tests/integration/devfile/utils/utils.go @@ -177,3 +177,26 @@ func ExecPushWithNewFileAndDir(projectDirPath, cmpName, namespace, newFilePath, args = useProjectIfAvailable(args, namespace) helper.CmdShouldPass("odo", args...) } + +// ExecWithRestartAttribute executes odo push with a command attribute restart +func ExecWithRestartAttribute(projectDirPath, cmpName, namespace string) { + helper.CmdShouldPass("git", "clone", "https://github.com/che-samples/web-nodejs-sample.git", projectDirPath) + helper.Chdir(projectDirPath) + + args := []string{"create", "nodejs", cmpName} + args = useProjectIfAvailable(args, namespace) + helper.CmdShouldPass("odo", args...) + + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-restart.yaml"), filepath.Join(projectDirPath, "devfile.yaml")) + + args = []string{"push", "--devfile", "devfile.yaml"} + args = useProjectIfAvailable(args, namespace) + output := helper.CmdShouldPass("odo", args...) + Expect(output).To(ContainSubstring("Executing devrun command \"nodemon app.js\"")) + + args = []string{"push", "-f", "--devfile", "devfile.yaml"} + args = useProjectIfAvailable(args, namespace) + output = helper.CmdShouldPass("odo", args...) + Expect(output).To(ContainSubstring("if not running")) + +} diff --git a/tests/integration/operatorhub/cmd_service_test.go b/tests/integration/operatorhub/cmd_service_test.go index b4553aaf08b..60511ea6caf 100644 --- a/tests/integration/operatorhub/cmd_service_test.go +++ b/tests/integration/operatorhub/cmd_service_test.go @@ -158,4 +158,12 @@ spec: }) }) + + Context("JSON output", func() { + It("listing catalog of services", func() { + jsonOut := helper.CmdShouldPass("odo", "catalog", "list", "services", "-o", "json") + Expect(jsonOut).To(ContainSubstring("mongodb-enterprise")) + Expect(jsonOut).To(ContainSubstring("etcdoperator")) + }) + }) })