[#5561] Remove odo url (#5571)

* Remove `odo url` from CLI layer

* Adapt tests

* Adapt test from deleted `cmd_devfile_delete_test.go`

This test still makes sense, as it tests the removal
of URL-related resources, like Services and Ingresses.

* Remove call to `odo project set ...` in test, as suggested in review

This command will get removed in the near future.

* Remove places where URL manipulation could modify  Devfiles

Also remove dead code

* Remove unused `updateURL` field from EnvInfo struct

* Adapt test by mimicking some behavior that `odo url create` used to perform

`odo url` used to modify the `env.yaml` file,
and `odo dev` currently creates Kubernetes/OCP resources related to URLs.

We may remove this test later on if `odo dev`
no longer needs to create such Ingresses and Routes.

* Remove all places where URLs, Ingresses and Routes are manipulated

* Port test in cmd_dev_test instead and make sure no Ingress/Route is created

* Remove no-longer `test-cmd-devfile-url` target from Makefile

The corresponding test file has been deleted.

* Remove `create url` command reference from V3 docs, as suggested in review

* Use existing `devfile-with-multiple-endpoints.yaml` Devfile  in test, as suggested in review
This commit is contained in:
Armel Soro
2022-03-28 16:39:53 +02:00
committed by GitHub
parent 77d6b6df5c
commit 2132cb516f
43 changed files with 70 additions and 7720 deletions

View File

@@ -223,10 +223,6 @@ test-cmd-devfile-registry: install ## Run odo devfile registry command tests
test-cmd-devfile-test: install ## Run odo devfile test command tests
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo devfile test command tests" tests/integration/devfile/
.PHONY: test-cmd-devfile-url
test-cmd-devfile-url: install ## Run odo url devfile command tests
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo devfile url command tests" tests/integration/devfile/
.PHONY: test-cmd-devfile-debug
test-cmd-devfile-debug: install ## Run odo debug devfile command tests
$(RUN_GINKGO) $(GINKGO_FLAGS) -focus="odo devfile debug command tests" tests/integration/devfile/

View File

@@ -1,66 +0,0 @@
---
title: Create URLs using odo
sidebar_position: 2
sidebar_label: Create URL
---
In the [previous section](./create-component) we created two components — a Spring Boot application (`backend`) listening on port 8080 and a Nodejs application (`frontend`) listening on port 3000 — and pushed them to the Kubernetes cluster. These are also the respective default ports (8080 for Spring Boot and 3000 for Nodejs) for Spring Boot and Nodejs component types. In this guide, we will create URLs to access these components from the host system.
Note that the URLs we create in this section will only help you access the components in web browser; the application itself won't be usable till we create some services and links which we will cover in the next section.
## OpenShift
If you are using [Code Ready Containers (CRC)](https://github.com/code-ready/crc) or another form of OpenShift cluster, odo has already created URLs for you by using the [OpenShift Routes](https://docs.openshift.com/container-platform/latest/networking/routes/route-configuration.html) feature. Execute `odo url list` from the component directory of the `backend` and `frontend` components to get the URLs odo created for these components. If you observe the `odo push` output closely, odo prints the URL in it as well.
Below are example `odo url list` outputs for the backend and frontend components. Note that URLs would be different in your case:
```shell
# backend component
$ odo url list
Found the following URLs for component backend
NAME STATE URL PORT SECURE KIND
8080-tcp Pushed http://8080-tcp-app-myproject.hostname.com 8080 false route
# frontend component
$ odo url list
Found the following URLs for component frontend
NAME STATE URL PORT SECURE KIND
http-3000 Pushed http://http-3000-app-myproject.hostname.com 3000 false route
```
## Kubernetes
If you are using a Kubernetes cluster, you will have to create a URL using `odo url` command. This is because odo can not assume the host information to be used to create a URL. To be able to create URLs on a Kubernetes cluster, please make sure that you have [Ingress Controller](/docs/getting-started/cluster-setup/kubernetes/#enabling-ingress) installed.
If you are working on a [minikube](/docs/getting-started/cluster-setup/kubernetes), Ingress can be enabled using:
```shell
minikube addons enable ingress
```
If you are working on any other kind of Kubernetes cluster, please check with your cluster administrator to enable the Ingress Controller. In this guide, we cover URL creation for minikube setup. For any other Kubernetes cluster, please replace `$(minikube ip).nip.io` in below commands with the host information for your specific cluster.
### Backend component
Our backend component, which is based on Spring Boot, listens on port 8080. `cd` into the directory for this component and execute below command:
```shell
odo url create --port 8080 --host $(minikube ip).nip.io
odo push
```
odo follows a "create & push" workflow for most commands. But in this case, adding `--now` flag to `odo url create` could reduce two commands into a single command:
```shell
odo url create --port 8080 --host $(minikube ip).nip.io --now
````
### Frontend component
Our frontend component, which is based on Nodejs, listens on port 3000. `cd` into the directory for this component and execute below command:
```shell
odo url create --port 3000 --host $(minikube ip).nip.io
odo push
```
Again, if you would prefer to get this done in a single command:
```shell
odo url create --port 3000 --host $(minikube ip).nip.io --now
```

View File

@@ -24,7 +24,6 @@ import (
"github.com/redhat-developer/odo/pkg/envinfo"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/service"
urlpkg "github.com/redhat-developer/odo/pkg/url"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -33,33 +32,6 @@ import (
const NotAvailable = "Not available"
// ApplyConfig applies the component config onto component deployment
// Parameters:
// client: kclient instance
// componentConfig: Component configuration
// envSpecificInfo: Component environment specific information, available if uses devfile
// Returns:
// err: Errors if any else nil
func ApplyConfig(client kclient.ClientInterface, envSpecificInfo envinfo.EnvSpecificInfo) (err error) {
isRouteSupported := false
isRouteSupported, err = client.IsRouteSupported()
if err != nil {
isRouteSupported = false
}
urlClient := urlpkg.NewClient(urlpkg.ClientOptions{
Client: client,
IsRouteSupported: isRouteSupported,
LocalConfigProvider: &envSpecificInfo,
})
return urlpkg.Push(urlpkg.PushParameters{
LocalConfigProvider: &envSpecificInfo,
URLClient: urlClient,
IsRouteSupported: isRouteSupported,
})
}
// ListDevfileStacks returns the devfile component matching a selector.
// The selector could be about selecting components part of an application.
// There are helpers in "applabels" package for this.
@@ -238,11 +210,11 @@ func Exists(client kclient.ClientInterface, componentName, applicationName strin
// GetComponent provides component definition
func GetComponent(client kclient.ClientInterface, componentName string, applicationName string, projectName string) (component Component, err error) {
return getRemoteComponentMetadata(client, componentName, applicationName, true, true)
return getRemoteComponentMetadata(client, componentName, applicationName, true)
}
// getRemoteComponentMetadata provides component metadata from the cluster
func getRemoteComponentMetadata(client kclient.ClientInterface, componentName string, applicationName string, getUrls, getStorage bool) (Component, error) {
func getRemoteComponentMetadata(client kclient.ClientInterface, componentName string, applicationName string, getStorage bool) (Component, error) {
fromCluster, err := GetPushedComponent(client, componentName, applicationName)
if err != nil {
return Component{}, fmt.Errorf("unable to get remote metadata for %s component: %w", componentName, err)
@@ -260,23 +232,6 @@ func getRemoteComponentMetadata(client kclient.ClientInterface, componentName st
// init component
component := newComponentWithType(componentName, componentType)
// URL
if getUrls {
urls, e := fromCluster.GetURLs()
if e != nil {
return Component{}, e
}
component.Spec.URLSpec = urls
urlsNb := len(urls)
if urlsNb > 0 {
res := make([]string, 0, urlsNb)
for _, url := range urls {
res = append(res, url.Name)
}
component.Spec.URL = res
}
}
// Storage
if getStorage {
appStore, e := fromCluster.GetStorage()

View File

@@ -7,7 +7,6 @@ import (
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/storage"
"github.com/redhat-developer/odo/pkg/url"
v1 "k8s.io/api/apps/v1"
v12 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -24,7 +23,6 @@ type provider interface {
// PushedComponent is an abstraction over the cluster representation of the component
type PushedComponent interface {
provider
GetURLs() ([]url.URL, error)
GetApplication() string
GetType() (string, error)
GetStorage() ([]storage.Storage, error)
@@ -32,12 +30,10 @@ type PushedComponent interface {
type defaultPushedComponent struct {
application string
urls []url.URL
storage []storage.Storage
provider provider
client kclient.ClientInterface
storageClient storage.Client
urlClient url.Client
}
func (d defaultPushedComponent) GetLabels() map[string]string {
@@ -64,17 +60,6 @@ func (d defaultPushedComponent) GetLinkedSecrets() []SecretMount {
return d.provider.GetLinkedSecrets()
}
func (d defaultPushedComponent) GetURLs() ([]url.URL, error) {
if d.urls == nil {
urls, err := d.urlClient.ListFromCluster()
if err != nil && !isIgnorableError(err) {
return []url.URL{}, err
}
d.urls = urls.Items
}
return d.urls, nil
}
// GetStorage gets the storage using the storage client of the given pushed component
func (d defaultPushedComponent) GetStorage() ([]storage.Storage, error) {
if d.storage == nil {
@@ -163,13 +148,12 @@ func getType(component provider) (string, error) {
return "", fmt.Errorf("%s component doesn't provide a type annotation; consider pushing the component again", component.GetName())
}
func newPushedComponent(applicationName string, p provider, c kclient.ClientInterface, storageClient storage.Client, urlClient url.Client) PushedComponent {
func newPushedComponent(applicationName string, p provider, c kclient.ClientInterface, storageClient storage.Client) PushedComponent {
return &defaultPushedComponent{
application: applicationName,
provider: p,
client: c,
storageClient: storageClient,
urlClient: urlClient,
}
}
@@ -187,11 +171,7 @@ func GetPushedComponent(c kclient.ClientInterface, componentName, applicationNam
Deployment: d,
})
urlClient := url.NewClient(url.ClientOptions{
Client: c,
Deployment: d,
})
return newPushedComponent(applicationName, &devfileComponent{d: *d}, c, storageClient, urlClient), nil
return newPushedComponent(applicationName, &devfileComponent{d: *d}, c, storageClient), nil
}
func isIgnorableError(err error) bool {

View File

@@ -3,7 +3,6 @@ package component
import (
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/storage"
"github.com/redhat-developer/odo/pkg/url"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -23,8 +22,6 @@ type ComponentSpec struct {
App string `json:"app,omitempty"`
Type string `json:"type,omitempty"`
Source string `json:"source,omitempty"`
URL []string `json:"url,omitempty"`
URLSpec []url.URL `json:"-"`
Storage []string `json:"storage,omitempty"`
StorageSpec []storage.Storage `json:"-"`
Env []corev1.EnvVar `json:"env,omitempty"`

View File

@@ -274,10 +274,6 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
}
parameters.EnvSpecificInfo.SetDevfileObj(a.Devfile)
err = component.ApplyConfig(a.Client, parameters.EnvSpecificInfo)
if err != nil {
return fmt.Errorf("Failed to update config to component deployed: %w", err)
}
// Compare the name of the pod with the one before the rollout. If they differ, it means there's a new pod and a force push is required
if componentExists && podName != pod.GetName() {

View File

@@ -42,8 +42,6 @@ const (
// EnvInfo holds all the env specific information relevant to a specific Component.
type EnvInfo struct {
devfileObj parser.DevfileObj
isRouteSupported bool
updateURL bool // this indicates that the URL create operation should be an update operation
componentSettings ComponentSettings `yaml:"ComponentSettings,omitempty"`
}
@@ -160,13 +158,6 @@ func (esi *EnvSpecificInfo) SetConfiguration(parameter string, value interface{}
return fmt.Errorf("failed to set debug port: %w", err)
}
esi.componentSettings.DebugPort = &val
case "url":
urlValue := value.(localConfigProvider.LocalURL)
if esi.componentSettings.URL != nil {
*esi.componentSettings.URL = append(*esi.componentSettings.URL, urlValue)
} else {
esi.componentSettings.URL = &[]localConfigProvider.LocalURL{urlValue}
}
}
return esi.writeToFile()
@@ -339,11 +330,6 @@ func (ei *EnvInfo) GetDevfileObj() parser.DevfileObj {
return ei.devfileObj
}
// SetIsRouteSupported sets the isRouteSupported value for the envinfo
func (ei *EnvInfo) SetIsRouteSupported(isRouteSupported bool) {
ei.isRouteSupported = isRouteSupported
}
const (
// Name is the name of the setting controlling the component name
Name = "Name"
@@ -357,10 +343,6 @@ const (
DebugPort = "DebugPort"
// DebugPortDescription s the human-readable description for debug port setting
DebugPortDescription = "Set this value to user-defined debug port to assign the debug port to the component"
// URL parameter
URL = "URL"
// URLDescription is the description of URL
URLDescription = "URL to access the component"
// Push parameter
Push = "PUSH"
// PushDescription is the description of push parameter
@@ -372,7 +354,6 @@ var (
Name: NameDescription,
Project: ProjectDescription,
DebugPort: DebugPortDescription,
URL: URLDescription,
Push: PushDescription,
}

View File

@@ -2,22 +2,11 @@ package envinfo
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strconv"
"testing"
"github.com/devfile/library/pkg/devfile/parser/data"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
devfileCtx "github.com/devfile/library/pkg/devfile/parser/context"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/util"
devfileFileSystem "github.com/devfile/library/pkg/testingutil/filesystem"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
)
@@ -29,7 +18,7 @@ func TestSetEnvInfo(t *testing.T) {
}
defer tempEnvFile.Close()
os.Setenv(envInfoEnvName, tempEnvFile.Name())
testURL := localConfigProvider.LocalURL{Name: "testURL", Host: "1.2.3.4.nip.io", TLSSecret: "testTLSSecret"}
testDebugPort := 5005
invalidParam := "invalidParameter"
tests := []struct {
@@ -41,23 +30,23 @@ func TestSetEnvInfo(t *testing.T) {
expectError bool
}{
{
name: fmt.Sprintf("Case 1: %s to test", URL),
parameter: URL,
value: testURL,
name: fmt.Sprintf("Case 1: %s to test", DebugPort),
parameter: DebugPort,
value: strconv.Itoa(testDebugPort),
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{},
},
checkConfigSetting: []string{"URL"},
checkConfigSetting: []string{"debugport"},
expectError: false,
},
{
name: fmt.Sprintf("Case 2: %s to test", invalidParam),
parameter: invalidParam,
value: testURL,
value: strconv.Itoa(testDebugPort),
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{},
},
checkConfigSetting: []string{"URL"},
checkConfigSetting: []string{"debugport"},
expectError: true,
},
}
@@ -97,7 +86,7 @@ func TestUnsetEnvInfo(t *testing.T) {
}
defer tempEnvFile.Close()
os.Setenv(envInfoEnvName, tempEnvFile.Name())
testURL := localConfigProvider.LocalURL{Name: "testURL", Host: "1.2.3.4.nip.io", TLSSecret: "testTLSSecret"}
testDebugPort := 15005
invalidParam := "invalidParameter"
tests := []struct {
@@ -107,11 +96,11 @@ func TestUnsetEnvInfo(t *testing.T) {
expectError bool
}{
{
name: fmt.Sprintf("Case 1: unset %s", URL),
parameter: URL,
name: fmt.Sprintf("Case 1: unset %s", DebugPort),
parameter: DebugPort,
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{
URL: &[]localConfigProvider.LocalURL{testURL},
DebugPort: &testDebugPort,
},
},
expectError: false,
@@ -121,7 +110,7 @@ func TestUnsetEnvInfo(t *testing.T) {
parameter: invalidParam,
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{
URL: &[]localConfigProvider.LocalURL{testURL},
DebugPort: &testDebugPort,
},
},
expectError: true,
@@ -151,150 +140,6 @@ func TestUnsetEnvInfo(t *testing.T) {
}
}
func TestDeleteURLFromMultipleURLs(t *testing.T) {
fs := devfileFileSystem.NewFakeFs()
tempEnvFile, err := ioutil.TempFile("", "odoenvinfo")
if err != nil {
t.Fatal(err)
}
defer tempEnvFile.Close()
os.Setenv(envInfoEnvName, tempEnvFile.Name())
testURL1 := localConfigProvider.LocalURL{Name: "testURL1", Host: "1.2.3.4.nip.io", TLSSecret: "testTLSSecret"}
testURL2 := localConfigProvider.LocalURL{Name: "testURL2", Host: "1.2.3.4.nip.io", TLSSecret: "testTLSSecret"}
tests := []struct {
name string
existingEnvInfo EnvInfo
existingDevfile parser.DevfileObj
deleteParam string
remainingParam string
singleURL bool
wantErr bool
}{
{
name: fmt.Sprintf("Case 1: delete %s from multiple URLs", testURL1.Name),
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{
URL: &[]localConfigProvider.LocalURL{testURL1, testURL2},
},
},
existingDevfile: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "runtime",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: testURL1.Name,
TargetPort: 3000,
},
{
Name: testURL2.Name,
TargetPort: 8080,
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
deleteParam: testURL1.Name,
remainingParam: testURL2.Name,
singleURL: false,
},
{
name: fmt.Sprintf("Case 2: delete %s fro URL array with single element", testURL1.Name),
existingEnvInfo: EnvInfo{
componentSettings: ComponentSettings{
URL: &[]localConfigProvider.LocalURL{testURL1},
},
},
existingDevfile: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "runtime",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: testURL1.Name,
TargetPort: 3000,
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
deleteParam: testURL1.Name,
singleURL: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
esi, err := NewEnvSpecificInfo("")
if err != nil {
t.Error(err)
}
esi.EnvInfo = tt.existingEnvInfo
esi.SetDevfileObj(tt.existingDevfile)
urls, err := esi.ListURLs()
if err != nil {
t.Error(err)
}
oldURLLength := len(urls)
err = esi.DeleteURL(tt.deleteParam)
if err != nil {
t.Error(err)
}
urls, err = esi.ListURLs()
if err != nil {
t.Error(err)
}
newURLLength := len(urls)
if newURLLength+1 != oldURLLength {
t.Errorf("DeleteURL is expected to delete element %s from the URL array.", tt.deleteParam)
}
if tt.singleURL {
if newURLLength != 0 {
t.Errorf("Expect to have empty URL array if delete URL from URL array with only 1 element")
}
} else {
if urls[0].Name != tt.remainingParam {
t.Errorf("Expect to have element %s in the URL array", tt.remainingParam)
}
}
})
}
}
func TestEnvSpecificInfonitDoesntCreateLocalOdoFolder(t *testing.T) {
// cleaning up old odo files if any
filename, _, err := getEnvInfoFile("")
@@ -379,521 +224,6 @@ func TestDeleteEnvDirIfEmpty(t *testing.T) {
}
}
func TestAddEndpointInDevfile(t *testing.T) {
fs := devfileFileSystem.NewFakeFs()
urlName := "testURL"
urlName2 := "testURL2"
tests := []struct {
name string
devObj parser.DevfileObj
endpoint devfilev1.Endpoint
container string
wantComponents []devfilev1.Component
}{
{
name: "Case 1: devfile has single container with existing endpoint",
endpoint: devfilev1.Endpoint{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
container: "testcontainer1",
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: "port-3030",
TargetPort: 3000,
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: "port-3030",
TargetPort: 3000,
},
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
},
},
{
name: "Case 2: devfile has single container with no endpoint",
endpoint: devfilev1.Endpoint{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
container: "testcontainer1",
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
},
},
{
name: "Case 3: devfile has multiple containers",
endpoint: devfilev1.Endpoint{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
container: "testcontainer1",
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
},
},
},
{
Name: "testcontainer2",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: urlName2,
TargetPort: 9090,
Secure: util.GetBoolPtr(true),
Path: "/testpath",
Exposure: devfilev1.InternalEndpointExposure,
Protocol: devfilev1.HTTPSEndpointProtocol,
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
{
Name: "testcontainer2",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: urlName2,
TargetPort: 9090,
Secure: util.GetBoolPtr(true),
Path: "/testpath",
Exposure: devfilev1.InternalEndpointExposure,
Protocol: devfilev1.HTTPSEndpointProtocol,
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := addEndpointInDevfile(tt.devObj, tt.endpoint, tt.container)
if err != nil {
t.Errorf("Unexpected err from UpdateEndpointsInDevfile: %v", err)
}
components, err := tt.devObj.Data.GetComponents(common.DevfileOptions{})
if err != nil {
t.Errorf("Unexpected err from addEndpointInDevfile: %v", err)
}
if !reflect.DeepEqual(components, tt.wantComponents) {
t.Errorf("Expected: %v, got %v", tt.wantComponents, components)
}
})
}
}
func TestRemoveEndpointInDevfile(t *testing.T) {
fs := devfileFileSystem.NewFakeFs()
urlName := "testURL"
urlName2 := "testURL2"
tests := []struct {
name string
devObj parser.DevfileObj
endpoint devfilev1.Endpoint
urlName string
wantComponents []devfilev1.Component
wantErr bool
}{
{
name: "Case 1: devfile has single container with multiple existing endpoint",
urlName: urlName,
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: "port-3030",
TargetPort: 3000,
},
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: "port-3030",
TargetPort: 3000,
},
},
},
},
},
},
wantErr: false,
},
{
name: "Case 2: devfile has single container with a single endpoint",
urlName: urlName,
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{},
},
},
},
},
wantErr: false,
},
{
name: "Case 3: devfile has multiple containers",
urlName: urlName,
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
{
Name: "testcontainer2",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: urlName2,
TargetPort: 9090,
Secure: util.GetBoolPtr(true),
Path: "/testpath",
Exposure: devfilev1.InternalEndpointExposure,
Protocol: devfilev1.HTTPSEndpointProtocol,
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{},
},
},
},
{
Name: "testcontainer2",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Endpoints: []devfilev1.Endpoint{
{
Name: urlName2,
TargetPort: 9090,
Secure: util.GetBoolPtr(true),
Path: "/testpath",
Exposure: devfilev1.InternalEndpointExposure,
Protocol: devfilev1.HTTPSEndpointProtocol,
},
},
},
},
},
},
wantErr: false,
},
{
name: "Case 4: delete an invalid endpoint",
urlName: "invalidurl",
devObj: parser.DevfileObj{
Ctx: devfileCtx.FakeContext(fs, parser.OutputDevfileYamlPath),
Data: func() data.DevfileData {
devfileData, err := data.NewDevfileData(string(data.APISchemaVersion200))
if err != nil {
t.Error(err)
}
err = devfileData.AddComponents([]devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
})
if err != nil {
t.Error(err)
}
return devfileData
}(),
},
wantComponents: []devfilev1.Component{
{
Name: "testcontainer1",
ComponentUnion: devfilev1.ComponentUnion{
Container: &devfilev1.ContainerComponent{
Container: devfilev1.Container{
Image: "quay.io/nodejs-12",
},
Endpoints: []devfilev1.Endpoint{
{
Name: urlName,
TargetPort: 8080,
Secure: util.GetBoolPtr(false),
},
},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := removeEndpointInDevfile(tt.devObj, tt.urlName)
if !tt.wantErr && err != nil {
t.Errorf("Unexpected err from removeEndpointInDevfile: %v", err)
} else if err == nil && tt.wantErr {
t.Error("error was expected, but no error was returned")
}
components, err := tt.devObj.Data.GetComponents(common.DevfileOptions{})
if err != nil {
t.Errorf("Unexpected err from removeEndpointInDevfile: %v", err)
}
if !reflect.DeepEqual(components, tt.wantComponents) {
t.Errorf("Expected: %v, got %v", tt.wantComponents, components)
}
})
}
}
func createDirectoryAndFile(create bool, fs filesystem.Filesystem, odoDir string) error {
if !create {
return nil

View File

@@ -1,9 +1,5 @@
package envinfo
import (
"github.com/redhat-developer/odo/pkg/localConfigProvider"
)
// ComponentSettings holds all component related information
type ComponentSettings struct {
Name string `yaml:"Name,omitempty" json:"name,omitempty"`
@@ -12,7 +8,6 @@ type ComponentSettings struct {
UserCreatedDevfile bool `yaml:"UserCreatedDevfile,omitempty" json:"UserCreatedDevfile,omitempty"`
URL *[]localConfigProvider.LocalURL `yaml:"Url,omitempty" json:"url,omitempty"`
// AppName is the application name. Application is a virtual concept present in odo used
// for grouping of components. A namespace can contain multiple applications
AppName string `yaml:"AppName,omitempty" json:"appName,omitempty"`

View File

@@ -1,512 +0,0 @@
package envinfo
import (
"fmt"
"sort"
"strconv"
"strings"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/odo/util/validation"
"github.com/redhat-developer/odo/pkg/util"
)
// getPorts gets the ports from devfile
func (ei *EnvInfo) getPorts(container string) ([]string, error) {
var portList []string
containerComponents, err := ei.devfileObj.Data.GetDevfileContainerComponents(common.DevfileOptions{})
if err != nil {
return nil, err
}
containerExists := false
portMap := make(map[string]bool)
for _, component := range containerComponents {
if container == "" || container == component.Name {
containerExists = true
for _, endpoint := range component.Container.Endpoints {
portMap[strconv.FormatInt(int64(endpoint.TargetPort), 10)] = true
}
}
}
if !containerExists {
return portList, fmt.Errorf("the container specified: %s does not exist in devfile", container)
}
for port := range portMap {
portList = append(portList, port)
}
sort.Strings(portList)
return portList, nil
}
// GetContainerPorts returns list of the ports of specified container, if it exists
func (ei *EnvInfo) GetContainerPorts(container string) ([]string, error) {
if container == "" {
return nil, fmt.Errorf("please provide a container")
}
return ei.getPorts(container)
}
// GetComponentPorts returns all unique ports declared in all the containers
func (ei *EnvInfo) GetComponentPorts() ([]string, error) {
return ei.getPorts("")
}
// checkValidPort checks and retrieves valid port from devfile when no port is specified
func (ei *EnvInfo) checkValidPort(url *localConfigProvider.LocalURL, portsOf string, ports []string) (err error) {
if url.Port == -1 {
if len(ports) > 1 {
return fmt.Errorf("port for the %s is required as it exposes %d ports: %s", portsOf, len(ports), strings.Trim(strings.Replace(fmt.Sprint(ports), " ", ",", -1), "[]"))
} else if len(ports) <= 0 {
return fmt.Errorf("no port is exposed by the %s, please specify a port", portsOf)
} else {
url.Port, err = strconv.Atoi(strings.Split(ports[0], "/")[0])
if err != nil {
return err
}
}
}
return nil
}
// CompleteURL completes the given URL with default values
func (ei *EnvInfo) CompleteURL(url *localConfigProvider.LocalURL) error {
if url.Kind == "" {
if !ei.isRouteSupported {
url.Kind = localConfigProvider.INGRESS
} else {
url.Kind = localConfigProvider.ROUTE
}
}
if len(url.Path) > 0 && (strings.HasPrefix(url.Path, "/") || strings.HasPrefix(url.Path, "\\")) {
if len(url.Path) <= 1 {
url.Path = ""
} else {
// remove the leading / or \ from provided path
url.Path = string([]rune(url.Path)[1:])
}
}
// add leading / to path, if the path provided is empty, it will be set to / which is the default value of path
url.Path = "/" + url.Path
// get the port if not provided
if url.Container == "" {
ports, err := ei.GetComponentPorts()
if err != nil {
return err
}
err = ei.checkValidPort(url, fmt.Sprintf("component %s", ei.GetName()), ports)
if err != nil {
return err
}
} else {
ports, err := ei.GetContainerPorts(url.Container)
if err != nil {
return err
}
err = ei.checkValidPort(url, fmt.Sprintf("container %s", url.Container), ports)
if err != nil {
return err
}
}
// get the name for the URL if not provided
if len(url.Name) == 0 {
foundURL, err := findInvalidEndpoint(ei, url.Port)
if err != nil {
return err
}
if foundURL.Name != "" {
// found an URL that can be overridden or more info can be added to it
url.Name = foundURL.Name
ei.updateURL = true
} else {
url.Name = getURLName(ei.GetName(), url.Port)
}
}
containerComponents, err := ei.devfileObj.Data.GetDevfileContainerComponents(common.DevfileOptions{})
if err != nil {
return err
}
if len(containerComponents) == 0 {
return fmt.Errorf("no valid components found in the devfile")
}
// if a container name is provided for the URL, return
if len(url.Container) > 0 {
return nil
}
containerPortMap := make(map[int]string)
portMap := make(map[string]bool)
// if a container name for the URL is not provided
// use a container which uses the given URL port in one of it's endpoints
for _, component := range containerComponents {
for _, endpoint := range component.Container.Endpoints {
containerPortMap[endpoint.TargetPort] = component.Name
portMap[strconv.FormatInt(int64(endpoint.TargetPort), 10)] = true
}
}
if containerName, exist := containerPortMap[url.Port]; exist {
url.Container = containerName
}
// container is not provided, or the specified port is not being used under any containers
// pick the first container to store the new endpoint
if len(url.Container) == 0 {
url.Container = containerComponents[0].Name
}
return nil
}
// getURLName returns a url name from the component name and the given port number
func getURLName(componentName string, componentPort int) string {
if componentPort == -1 {
return componentName
}
return fmt.Sprintf("%v-%v", componentName, componentPort)
}
// ValidateURL validates the given URL
func (ei *EnvInfo) ValidateURL(url localConfigProvider.LocalURL) error {
foundContainer := false
containerComponents, err := ei.devfileObj.Data.GetDevfileContainerComponents(common.DevfileOptions{})
if err != nil {
return err
}
if len(containerComponents) == 0 {
return fmt.Errorf("no valid components found in the devfile")
}
// map TargetPort with containerName
containerPortMap := make(map[int]string)
for _, component := range containerComponents {
if len(url.Container) > 0 && !foundContainer {
if component.Name == url.Container {
foundContainer = true
}
}
for _, endpoint := range component.Container.Endpoints {
if endpoint.Name == url.Name && !ei.updateURL {
return fmt.Errorf("url %v already exists in devfile endpoint entry under container %q", url.Name, component.Name)
}
// Check for a duplicate port entry
if endpoint.TargetPort == url.Port && !ei.updateURL {
return fmt.Errorf("port %v already exists in devfile endpoint entry under container %q", url.Port, component.Name)
}
containerPortMap[endpoint.TargetPort] = component.Name
}
}
if len(url.Container) > 0 && !foundContainer {
return fmt.Errorf("the container specified: %v does not exist in devfile", url.Container)
}
if containerName, exist := containerPortMap[url.Port]; exist {
if len(url.Container) > 0 && url.Container != containerName {
return fmt.Errorf("cannot set URL %v under container %v, TargetPort %v is being used under container %v", url.Name, url.Container, url.Port, containerName)
}
}
errorList := make([]string, 0)
if url.TLSSecret != "" && (url.Kind != localConfigProvider.INGRESS || !url.Secure) {
errorList = append(errorList, "TLS secret is only available for secure URLs of Ingress kind")
}
// check if a host is provided for route based URLs
if len(url.Host) > 0 {
if url.Kind == localConfigProvider.ROUTE {
errorList = append(errorList, "host is not supported for URLs of Route Kind")
}
if err := validation.ValidateHost(url.Host); err != nil {
errorList = append(errorList, err.Error())
}
} else if url.Kind == localConfigProvider.INGRESS {
errorList = append(errorList, "host must be provided in order to create URLS of Ingress Kind")
}
// check the protocol of the URL
if len(url.Protocol) > 0 {
switch strings.ToLower(url.Protocol) {
case string(devfilev1.HTTPEndpointProtocol):
break
case string(devfilev1.HTTPSEndpointProtocol):
break
case string(devfilev1.WSEndpointProtocol):
break
case string(devfilev1.WSSEndpointProtocol):
break
case string(devfilev1.TCPEndpointProtocol):
break
case string(devfilev1.UDPEndpointProtocol):
break
default:
errorList = append(errorList, fmt.Sprintf("endpoint protocol only supports %v|%v|%v|%v|%v|%v", devfilev1.HTTPEndpointProtocol, devfilev1.HTTPSEndpointProtocol, devfilev1.WSSEndpointProtocol, devfilev1.WSEndpointProtocol, devfilev1.TCPEndpointProtocol, devfilev1.UDPEndpointProtocol))
}
}
if !ei.updateURL {
urls, err := ei.ListURLs()
if err != nil {
return err
}
for _, localURL := range urls {
if url.Name == localURL.Name {
errorList = append(errorList, fmt.Sprintf("URL %s already exists", url.Name))
}
}
}
if len(errorList) > 0 {
return fmt.Errorf(strings.Join(errorList, "\n"))
}
return nil
}
// GetURL gets the given url from the env.yaml and devfile
func (ei *EnvInfo) GetURL(name string) (*localConfigProvider.LocalURL, error) {
urls, err := ei.ListURLs()
if err != nil {
return nil, err
}
for _, url := range urls {
if name == url.Name {
return &url, nil
}
}
return nil, nil
}
// CreateURL write the given url to the env.yaml and devfile
func (esi *EnvSpecificInfo) CreateURL(url localConfigProvider.LocalURL) error {
if !esi.updateURL {
newEndpointEntry := devfilev1.Endpoint{
Name: url.Name,
Path: url.Path,
Secure: util.GetBoolPtr(url.Secure),
Exposure: devfilev1.PublicEndpointExposure,
TargetPort: url.Port,
Protocol: devfilev1.EndpointProtocol(strings.ToLower(url.Protocol)),
}
err := addEndpointInDevfile(esi.devfileObj, newEndpointEntry, url.Container)
if err != nil {
return fmt.Errorf("failed to write endpoints information into devfile: %w", err)
}
} else {
err := updateEndpointInDevfile(esi.devfileObj, url)
if err != nil {
return err
}
}
err := esi.SetConfiguration("url", localConfigProvider.LocalURL{Name: url.Name, Host: url.Host, TLSSecret: url.TLSSecret, Kind: url.Kind})
if err != nil {
return fmt.Errorf("failed to persist the component settings to env file: %w", err)
}
return nil
}
// ListURLs returns the urls from the env and devfile, returns default if nil
func (ei *EnvInfo) ListURLs() ([]localConfigProvider.LocalURL, error) {
envMap := make(map[string]localConfigProvider.LocalURL)
if ei.componentSettings.URL != nil {
for _, url := range *ei.componentSettings.URL {
envMap[url.Name] = url
}
}
var urls []localConfigProvider.LocalURL
if ei.devfileObj.Data == nil {
return urls, nil
}
devfileComponents, err := ei.devfileObj.Data.GetDevfileContainerComponents(common.DevfileOptions{})
if err != nil {
return urls, err
}
for _, comp := range devfileComponents {
for _, localEndpoint := range comp.Container.Endpoints {
// only exposed endpoint will be shown as a URL in `odo url list`
if localEndpoint.Exposure == devfilev1.NoneEndpointExposure || localEndpoint.Exposure == devfilev1.InternalEndpointExposure {
continue
}
path := "/"
if localEndpoint.Path != "" {
path = localEndpoint.Path
}
secure := false
if util.SafeGetBool(localEndpoint.Secure) || localEndpoint.Protocol == "https" || localEndpoint.Protocol == "wss" {
secure = true
}
url := localConfigProvider.LocalURL{
Name: localEndpoint.Name,
Port: localEndpoint.TargetPort,
Secure: secure,
Path: path,
Container: comp.Name,
}
if envInfoURL, exist := envMap[localEndpoint.Name]; exist {
url.Host = envInfoURL.Host
url.TLSSecret = envInfoURL.TLSSecret
url.Kind = envInfoURL.Kind
} else {
url.Kind = localConfigProvider.ROUTE
}
urls = append(urls, url)
}
}
return urls, nil
}
// DeleteURL is used to delete environment specific info for url from envinfo and devfile
func (esi *EnvSpecificInfo) DeleteURL(name string) error {
err := removeEndpointInDevfile(esi.devfileObj, name)
if err != nil {
return fmt.Errorf("failed to delete URL: %w", err)
}
if esi.componentSettings.URL == nil {
return nil
}
for i, url := range *esi.componentSettings.URL {
if url.Name == name {
s := *esi.componentSettings.URL
s = append(s[:i], s[i+1:]...)
esi.componentSettings.URL = &s
}
}
return esi.writeToFile()
}
// addEndpointInDevfile writes the provided endpoint information into devfile
func addEndpointInDevfile(devObj parser.DevfileObj, endpoint devfilev1.Endpoint, container string) error {
components, err := devObj.Data.GetComponents(common.DevfileOptions{})
if err != nil {
return err
}
for _, component := range components {
if component.Container != nil && component.Name == container {
component.Container.Endpoints = append(component.Container.Endpoints, endpoint)
err := devObj.Data.UpdateComponent(component)
if err != nil {
return err
}
break
}
}
return devObj.WriteYamlDevfile()
}
// removeEndpointInDevfile deletes the specific endpoint information from devfile
func removeEndpointInDevfile(devObj parser.DevfileObj, urlName string) error {
found := false
devfileComponents, err := devObj.Data.GetDevfileContainerComponents(common.DevfileOptions{})
if err != nil {
return err
}
for _, component := range devfileComponents {
for index, enpoint := range component.Container.Endpoints {
if enpoint.Name == urlName {
component.Container.Endpoints = append(component.Container.Endpoints[:index], component.Container.Endpoints[index+1:]...)
err := devObj.Data.UpdateComponent(component)
if err != nil {
return err
}
found = true
break
}
}
if found {
break
}
}
if !found {
return fmt.Errorf("the URL %s does not exist", urlName)
}
return devObj.WriteYamlDevfile()
}
// updateEndpointInDevfile updates the endpoint of the given URL in the devfile
func updateEndpointInDevfile(devObj parser.DevfileObj, url localConfigProvider.LocalURL) error {
components, err := devObj.Data.GetComponents(common.DevfileOptions{})
if err != nil {
return err
}
for _, component := range components {
if component.Container != nil && component.Name == url.Container {
for j := range component.ComponentUnion.Container.Endpoints {
endpoint := component.ComponentUnion.Container.Endpoints[j]
if endpoint.Name == url.Name {
// fill the default values
if endpoint.Exposure == "" {
endpoint.Exposure = devfilev1.PublicEndpointExposure
}
if endpoint.Path == "" {
endpoint.Path = "/"
}
if endpoint.Protocol == "" {
endpoint.Protocol = devfilev1.HTTPEndpointProtocol
}
// prevent write unless required
if endpoint.Exposure != devfilev1.PublicEndpointExposure || url.Secure != util.SafeGetBool(endpoint.Secure) ||
url.Path != endpoint.Path || url.Protocol != string(endpoint.Protocol) {
endpoint = devfilev1.Endpoint{
Name: url.Name,
Path: url.Path,
Secure: &url.Secure,
Exposure: devfilev1.PublicEndpointExposure,
TargetPort: url.Port,
Protocol: devfilev1.EndpointProtocol(strings.ToLower(url.Protocol)),
}
component.ComponentUnion.Container.Endpoints[j] = endpoint
err := devObj.Data.UpdateComponent(component)
if err != nil {
return err
}
return devObj.WriteYamlDevfile()
}
return nil
}
}
}
}
return fmt.Errorf("url %s not found for updating", url.Name)
}
// findInvalidEndpoint finds the URLs which are invalid for the current cluster e.g
// route urls on a vanilla k8s based cluster
// urls with no host information on a vanilla k8s based cluster
func findInvalidEndpoint(ei *EnvInfo, port int) (localConfigProvider.LocalURL, error) {
urls, err := ei.ListURLs()
if err != nil {
return localConfigProvider.LocalURL{}, err
}
for _, url := range urls {
if url.Kind == localConfigProvider.ROUTE && url.Port == port && !ei.isRouteSupported {
return url, nil
}
}
return localConfigProvider.LocalURL{}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,134 +0,0 @@
package fake
import (
"fmt"
"github.com/devfile/library/pkg/devfile/generator"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/kclient/unions"
"github.com/redhat-developer/odo/pkg/url/labels"
"github.com/redhat-developer/odo/pkg/util"
"github.com/redhat-developer/odo/pkg/version"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func GetKubernetesIngressListWithMultiple(componentName, appName string, networkingV1Supported, extensionV1Supported bool) *unions.KubernetesIngressList {
kubernetesIngressList := unions.NewEmptyKubernetesIngressList()
kubernetesIngress1 := unions.NewKubernetesIngressFromParams(generator.IngressParams{
ObjectMeta: metav1.ObjectMeta{
Name: "example-0",
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: "example-0",
applabels.App: appName,
},
},
IngressSpecParams: generator.IngressSpecParams{
IngressDomain: "example-0.com",
ServiceName: "example-0",
PortNumber: intstr.FromInt(8080),
},
})
if !networkingV1Supported {
kubernetesIngress1.NetworkingV1Ingress = nil
}
if !extensionV1Supported {
kubernetesIngress1.ExtensionV1Beta1Ingress = nil
}
kubernetesIngressList.Items = append(kubernetesIngressList.Items, kubernetesIngress1)
kubernetesIngress2 := unions.NewKubernetesIngressFromParams(generator.IngressParams{
ObjectMeta: metav1.ObjectMeta{
Name: "example-1",
Labels: map[string]string{
applabels.ApplicationLabel: "app",
componentlabels.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: "example-1",
applabels.App: "app",
},
},
IngressSpecParams: generator.IngressSpecParams{
IngressDomain: "example-1.com",
ServiceName: "example-1",
PortNumber: intstr.FromInt(9090),
},
})
if !networkingV1Supported {
kubernetesIngress2.NetworkingV1Ingress = nil
}
if !extensionV1Supported {
kubernetesIngress2.ExtensionV1Beta1Ingress = nil
}
kubernetesIngressList.Items = append(kubernetesIngressList.Items, kubernetesIngress2)
return kubernetesIngressList
}
func GetSingleKubernetesIngress(urlName, componentName, appName string, networkingv1Supported, extensionv1Supported bool) *unions.KubernetesIngress {
kubernetesIngress := unions.NewKubernetesIngressFromParams(generator.IngressParams{
ObjectMeta: metav1.ObjectMeta{
Name: urlName,
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: urlName,
applabels.App: appName,
},
},
IngressSpecParams: generator.IngressSpecParams{
IngressDomain: fmt.Sprintf("%s.com", urlName),
ServiceName: urlName,
PortNumber: intstr.FromInt(8080),
},
})
if !networkingv1Supported {
kubernetesIngress.NetworkingV1Ingress = nil
}
if !extensionv1Supported {
kubernetesIngress.ExtensionV1Beta1Ingress = nil
}
return kubernetesIngress
}
// GetSingleSecureKubernetesIngress gets a single secure ingress with the given secret name
// if no secret name is provided, the default one is used
func GetSingleSecureKubernetesIngress(urlName, componentName, appName, secretName string, networkingV1Supported, extensionV1Supported bool) *unions.KubernetesIngress {
if secretName == "" {
suffix := util.GetAdler32Value(urlName + appName + componentName)
secretName = urlName + "-" + suffix + "-tls"
}
kubernetesIngress := unions.NewKubernetesIngressFromParams(generator.IngressParams{
ObjectMeta: metav1.ObjectMeta{
Name: urlName,
Labels: map[string]string{
applabels.ApplicationLabel: appName,
componentlabels.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: urlName,
applabels.App: appName,
},
},
IngressSpecParams: generator.IngressSpecParams{
TLSSecretName: secretName,
IngressDomain: fmt.Sprintf("%s.com", urlName),
ServiceName: urlName,
PortNumber: intstr.FromInt(8080),
},
})
if !networkingV1Supported {
kubernetesIngress.NetworkingV1Ingress = nil
}
if !extensionV1Supported {
kubernetesIngress.ExtensionV1Beta1Ingress = nil
}
return kubernetesIngress
}

View File

@@ -1,147 +0,0 @@
package kclient
import (
"context"
"fmt"
"github.com/redhat-developer/odo/pkg/kclient/unions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GetOneIngressFromSelector gets one ingress with the given selector
// if no or multiple ingresses are found with the given selector, it throws an error
func (c *Client) GetOneIngressFromSelector(selector string) (*unions.KubernetesIngress, error) {
ingresses, err := c.ListIngresses(selector)
if err != nil {
return nil, err
}
if num := len(ingresses.Items); num == 0 {
return nil, fmt.Errorf("no ingress was found for the selector: %v", selector)
} else if num > 1 {
return nil, fmt.Errorf("multiple ingresses exist for the selector: %v. Only one must be present", selector)
}
return ingresses.Items[0], nil
}
//CreateIngress creates a specified Kubernetes Ingress as a networking v1 or extensions v1beta1 ingress depending on what
//is supported, with preference for networking v1 ingress. The passed ingress MUST be a generated one, i.e it must
//have been created using unions.NewKubernetesIngressFromParams
func (c *Client) CreateIngress(ingress unions.KubernetesIngress) (*unions.KubernetesIngress, error) {
var err error
if !ingress.IsGenerated() {
return nil, fmt.Errorf("create ingress should get a generated ingress. If you are hiting this, contact the developer")
}
if ingress.GetName() == "" {
return nil, fmt.Errorf("cannot create an ingress without a name")
}
err = c.checkIngressSupport()
if err != nil {
return nil, err
}
created := false
kubernetesIngress := unions.NewNonGeneratedKubernetesIngress()
if c.isNetworkingV1IngressSupported {
kubernetesIngress.NetworkingV1Ingress, err = c.KubeClient.NetworkingV1().Ingresses(c.Namespace).Create(context.TODO(), ingress.NetworkingV1Ingress, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create networking v1 ingress %w", err)
}
created = true
} else if c.isExtensionV1Beta1IngressSupported {
kubernetesIngress.ExtensionV1Beta1Ingress, err = c.KubeClient.ExtensionsV1beta1().Ingresses(c.Namespace).Create(context.TODO(), ingress.ExtensionV1Beta1Ingress, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create extension v1 beta 1 ingress %w", err)
}
created = true
}
if !created {
return nil, fmt.Errorf("failed to create ingress as none of the options are supported")
}
return kubernetesIngress, nil
}
func (c *Client) DeleteIngress(name string) error {
var err error
err = c.checkIngressSupport()
if err != nil {
return err
}
if c.isNetworkingV1IngressSupported {
err = c.KubeClient.NetworkingV1().Ingresses(c.Namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("failed to delete networking v1 ingress %w", err)
}
} else if c.isExtensionV1Beta1IngressSupported {
err = c.KubeClient.ExtensionsV1beta1().Ingresses(c.Namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("failed to delete extensionv v1beta1 ingress %w", err)
}
}
return nil
}
//ListIngresses lists all the ingresses based on given label selector
func (c *Client) ListIngresses(labelSelector string) (*unions.KubernetesIngressList, error) {
kubernetesIngressList := unions.NewEmptyKubernetesIngressList()
err := c.checkIngressSupport()
if err != nil {
return nil, err
}
// if networking v1 ingress is supported then extension v1 ingress are automatically wrapped
// by net v1 ingresses
if c.isNetworkingV1IngressSupported {
ingressList, err := c.KubeClient.NetworkingV1().Ingresses(c.Namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return nil, fmt.Errorf("unable to list ingresses as networking v1 ingresses %w", err)
} else {
for k := range ingressList.Items {
ki := unions.NewNonGeneratedKubernetesIngress()
ki.NetworkingV1Ingress = &ingressList.Items[k]
kubernetesIngressList.Items = append(kubernetesIngressList.Items, ki)
}
}
return kubernetesIngressList, nil
} else if c.isExtensionV1Beta1IngressSupported {
ingressList, err := c.KubeClient.ExtensionsV1beta1().Ingresses(c.Namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return nil, fmt.Errorf("unable to list ingresses as extensions v1 ingress %w", err)
} else {
for k := range ingressList.Items {
ki := unions.NewNonGeneratedKubernetesIngress()
ki.ExtensionV1Beta1Ingress = &ingressList.Items[k]
kubernetesIngressList.Items = append(kubernetesIngressList.Items, ki)
}
}
return kubernetesIngressList, nil
}
return kubernetesIngressList, fmt.Errorf("ingresses on cluster are not supported")
}
func (c *Client) GetIngress(name string) (*unions.KubernetesIngress, error) {
ki := unions.NewNonGeneratedKubernetesIngress()
err := c.checkIngressSupport()
if err != nil {
return nil, err
}
if c.isNetworkingV1IngressSupported {
ki.NetworkingV1Ingress, err = c.KubeClient.NetworkingV1().Ingresses(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return ki, nil
}
if c.isExtensionV1Beta1IngressSupported {
ki.ExtensionV1Beta1Ingress, err = c.KubeClient.ExtensionsV1beta1().Ingresses(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return ki, nil
}
return nil, fmt.Errorf("could not get supported type of ingress")
}

View File

@@ -1,387 +0,0 @@
package kclient
import (
"fmt"
"testing"
"github.com/redhat-developer/odo/pkg/kclient/unions"
"github.com/devfile/library/pkg/devfile/generator"
extensionsv1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func ingressNameMatchError(expected, got []string) string {
if len(expected) != len(got) {
return "invalid error string format expected and got length should be the same"
}
val := "ingress name does not match the expected name"
for i := 0; i < len(expected); i++ {
val = fmt.Sprintf("%s, expected %s, got %s", val, expected[i], got[i])
}
return fmt.Sprintf("ingress name does not match the expected name, expected: %s, got %s", expected, got)
}
func TestCreateIngress(t *testing.T) {
tests := []struct {
name string
ingressName string
wantErr bool
isNetworkingV1Supported bool
ieExtensionV1Supported bool
}{
{
name: "Case: Valid networking v1 ingress name",
ingressName: "testIngress",
wantErr: false,
isNetworkingV1Supported: true,
ieExtensionV1Supported: false,
},
{
name: "Case: Invalid networking v1 ingress name",
ingressName: "",
wantErr: true,
isNetworkingV1Supported: true,
ieExtensionV1Supported: false,
},
{
name: "Case: Valid extensions v1 beta1 ingress name",
ingressName: "testIngress",
wantErr: false,
isNetworkingV1Supported: false,
ieExtensionV1Supported: true,
},
{
name: "Case: Invalid extensions v1 beta1 ingress name",
ingressName: "",
wantErr: true,
isNetworkingV1Supported: false,
ieExtensionV1Supported: true,
},
{
name: "Case: fail if neither is supported",
ingressName: "testIngress",
wantErr: true,
isNetworkingV1Supported: false,
ieExtensionV1Supported: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNewWithIngressSupports(tt.isNetworkingV1Supported, tt.ieExtensionV1Supported)
fkclient.Namespace = "default"
objectMeta := generator.GetObjectMeta(tt.ingressName, "default", nil, nil)
ingressParams := generator.IngressParams{
ObjectMeta: objectMeta,
IngressSpecParams: generator.IngressSpecParams{ServiceName: tt.ingressName},
}
ingress := unions.NewKubernetesIngressFromParams(ingressParams)
createdIngress, err := fkclient.CreateIngress(*ingress)
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("fkclient.CreateIngress unexpected error %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
if len(fkclientset.Kubernetes.Actions()) != 1 {
t.Errorf("expected 1 action, got: %v", fkclientset.Kubernetes.Actions())
} else {
if createdIngress.GetName() != tt.ingressName {
t.Errorf(ingressNameMatchError([]string{tt.ingressName}, []string{createdIngress.GetName()}))
}
}
}
})
}
}
func TestListIngresses(t *testing.T) {
componentName := "testcomponent"
componentLabel := "componentName"
tests := []struct {
name string
labelSelector string
wantIngress unions.KubernetesIngressList
isNetworkingV1Supported bool
isExtensionV1Supported bool
wantErr bool
}{
{
name: "Case: one networking v1 ingress",
labelSelector: fmt.Sprintf("%v=%v", componentLabel, componentName),
wantIngress: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
{
NetworkingV1Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "testIngress1",
Labels: map[string]string{
componentLabel: componentName,
},
},
},
ExtensionV1Beta1Ingress: nil,
},
},
},
isNetworkingV1Supported: true,
isExtensionV1Supported: false,
wantErr: false,
},
{
name: "Case: One extension v1 beta1 ingress",
labelSelector: fmt.Sprintf("%v=%v", componentLabel, componentName),
wantIngress: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
{
NetworkingV1Ingress: nil,
ExtensionV1Beta1Ingress: &extensionsv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "testIngress1",
Labels: map[string]string{
componentLabel: componentName,
},
},
},
},
},
},
isNetworkingV1Supported: false,
isExtensionV1Supported: true,
wantErr: false,
},
{
name: "Case: two networking v1 ingresses",
labelSelector: fmt.Sprintf("%v=%v", componentLabel, componentName),
wantIngress: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
{
NetworkingV1Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "testIngress1",
Labels: map[string]string{
componentLabel: componentName,
},
},
},
ExtensionV1Beta1Ingress: nil,
},
{
NetworkingV1Ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "testIngress2",
Labels: map[string]string{
componentLabel: componentName,
},
},
},
ExtensionV1Beta1Ingress: nil,
},
},
},
isNetworkingV1Supported: true,
isExtensionV1Supported: false,
wantErr: false,
},
{
name: "Case: fails if none of the ingresses are supported",
labelSelector: fmt.Sprintf("%v=%v", componentLabel, componentName),
wantIngress: unions.KubernetesIngressList{},
isNetworkingV1Supported: false,
isExtensionV1Supported: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNewWithIngressSupports(tt.isNetworkingV1Supported, tt.isExtensionV1Supported)
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("list", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
if tt.labelSelector != action.(ktesting.ListAction).GetListRestrictions().Labels.String() {
return true, nil, fmt.Errorf("selectors are different")
}
if action.GetResource().GroupVersion().Group == "networking.k8s.io" {
return true, tt.wantIngress.GetNetworkingV1IngressList(true), nil
}
ingress := tt.wantIngress.GetExtensionV1Beta1IngresList(true)
return true, ingress, nil
})
ingresses, err := fkclient.ListIngresses(tt.labelSelector)
if tt.wantErr && err == nil {
t.Errorf("fkclient.ListIngress expected err got %s", err)
}
if !tt.wantErr && err != nil {
t.Errorf("fkclient.ListIngresses unexpected error %v", err)
}
if err == nil {
if len(fkclientset.Kubernetes.Actions()) != 1 {
t.Errorf("expected 1 action, got: %v", fkclientset.Kubernetes.Actions())
} else {
if len(tt.wantIngress.Items) != len(ingresses.Items) {
t.Errorf("IngressList length is different, expected %v, got %v", len(tt.wantIngress.Items), len(ingresses.Items))
} else if len(ingresses.Items) == 1 && ingresses.Items[0].GetName() != tt.wantIngress.Items[0].GetName() {
t.Errorf(ingressNameMatchError([]string{tt.wantIngress.Items[0].GetName()}, []string{ingresses.Items[0].GetName()}))
} else if len(ingresses.Items) == 2 && (ingresses.Items[0].GetName() != tt.wantIngress.Items[0].GetName() || ingresses.Items[1].GetName() != tt.wantIngress.Items[1].GetName()) {
t.Errorf(ingressNameMatchError([]string{tt.wantIngress.Items[0].GetName(), tt.wantIngress.Items[1].GetName()}, []string{ingresses.Items[0].GetName(), ingresses.Items[1].GetName()}))
}
}
}
})
}
}
func TestDeleteIngress(t *testing.T) {
tests := []struct {
name string
ingressName string
wantErr bool
isNetworkingV1Supported bool
isExtensionV1Supported bool
}{
{
name: "delete networking v1 test",
ingressName: "testIngress",
wantErr: false,
isNetworkingV1Supported: true,
isExtensionV1Supported: false,
},
{
name: "delete extension v1 beta1 test",
ingressName: "testIngress",
wantErr: false,
isNetworkingV1Supported: false,
isExtensionV1Supported: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNewWithIngressSupports(tt.isNetworkingV1Supported, tt.isExtensionV1Supported)
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("delete", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
err := fkclient.DeleteIngress(tt.ingressName)
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("fkclient.DeleteIngressExtensionV1 unexpected error %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
if len(fkclientset.Kubernetes.Actions()) != 1 {
t.Errorf("expected 1 action, got: %v", fkclientset.Kubernetes.Actions())
} else {
DeletedIngress := fkclientset.Kubernetes.Actions()[0].(ktesting.DeleteAction).GetName()
if DeletedIngress != tt.ingressName {
t.Errorf("Delete action is performed with wrong ingress name, expected: %s, got %s", tt.ingressName, DeletedIngress)
}
}
}
})
}
}
func TestGetIngresses(t *testing.T) {
tests := []struct {
name string
ingressName string
wantErr bool
isNetworkingV1IngressSupported bool
isExtensionV1IngressSupported bool
}{
{
name: "Case: Valid ingress name",
ingressName: "testIngress",
wantErr: false,
isNetworkingV1IngressSupported: true,
isExtensionV1IngressSupported: false,
},
{
name: "Case: Invalid ingress name",
ingressName: "",
wantErr: true,
isNetworkingV1IngressSupported: true,
isExtensionV1IngressSupported: false,
},
{
name: "Case: valid extension v1 ingress name",
ingressName: "testIngress",
wantErr: false,
isExtensionV1IngressSupported: true,
isNetworkingV1IngressSupported: false,
},
{
name: "Case: invalid extension v1 ingress name",
ingressName: "",
wantErr: true,
isExtensionV1IngressSupported: true,
isNetworkingV1IngressSupported: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNewWithIngressSupports(tt.isNetworkingV1IngressSupported, tt.isExtensionV1IngressSupported)
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("get", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
if tt.ingressName == "" {
return true, nil, fmt.Errorf("ingress name is empty")
}
if action.GetResource().Group == "networking.k8s.io" {
ingress := networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: tt.ingressName,
},
}
return true, &ingress, nil
}
ingress := extensionsv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: tt.ingressName,
},
}
return true, &ingress, nil
})
ingress, err := fkclient.GetIngress(tt.ingressName)
// Checks for unexpected error cases
if !tt.wantErr == (err != nil) {
t.Errorf("fkclient.GetIngressExtensionV1 unexpected error %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
if len(fkclientset.Kubernetes.Actions()) != 1 {
t.Errorf("expected 1 action, got: %v", fkclientset.Kubernetes.Actions())
} else {
if ingress.GetName() != tt.ingressName {
t.Errorf(ingressNameMatchError([]string{tt.ingressName}, []string{ingress.GetName()}))
}
}
}
})
}
}

View File

@@ -4,16 +4,12 @@ import (
"io"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/go-openapi/spec"
projectv1 "github.com/openshift/api/project/v1"
routev1 "github.com/openshift/api/route/v1"
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
"github.com/redhat-developer/odo/pkg/kclient/unions"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/discovery"
@@ -56,13 +52,6 @@ type ClientInterface interface {
// events.go
CollectEvents(selector string, events map[string]corev1.Event, quit <-chan int)
// ingress.go
GetOneIngressFromSelector(selector string) (*unions.KubernetesIngress, error)
CreateIngress(ingress unions.KubernetesIngress) (*unions.KubernetesIngress, error)
DeleteIngress(name string) error
ListIngresses(labelSelector string) (*unions.KubernetesIngressList, error)
GetIngress(name string) (*unions.KubernetesIngress, error)
// kclient.go
GetClient() kubernetes.Interface
GetConfig() clientcmd.ClientConfig
@@ -121,14 +110,6 @@ type ClientInterface interface {
IsProjectSupported() (bool, error)
ListProjectNames() ([]string, error)
// routes.go
IsRouteSupported() (bool, error)
GetRoute(name string) (*routev1.Route, error)
CreateRoute(name string, serviceName string, portNumber intstr.IntOrString, labels map[string]string, secureURL bool, path string, ownerReference metav1.OwnerReference) (*routev1.Route, error)
DeleteRoute(name string) error
ListRoutes(labelSelector string) ([]routev1.Route, error)
GetOneRouteFromSelector(selector string) (*routev1.Route, error)
// secrets.go
CreateTLSSecret(tlsCertificate []byte, tlsPrivKey []byte, objectMeta metav1.ObjectMeta) (*corev1.Secret, error)
GetSecret(name, namespace string) (*corev1.Secret, error)

View File

@@ -332,19 +332,3 @@ func (c *Client) IsSSASupported() bool {
return *c.isSSASupported
}
func (c *Client) checkIngressSupport() error {
var err error
if c.checkIngressSupports {
c.isNetworkingV1IngressSupported, err = c.IsResourceSupported("networking.k8s.io", "v1", "ingresses")
if err != nil {
return fmt.Errorf("failed to check networking v1 ingress support %w", err)
}
c.isExtensionV1Beta1IngressSupported, err = c.IsResourceSupported("extensions", "v1beta1", "ingresses")
if err != nil {
return fmt.Errorf("failed to check extensions v1beta1 ingress support %w", err)
}
c.checkIngressSupports = false
}
return nil
}

View File

@@ -12,15 +12,12 @@ import (
spec "github.com/go-openapi/spec"
gomock "github.com/golang/mock/gomock"
v1 "github.com/openshift/api/project/v1"
v10 "github.com/openshift/api/route/v1"
v1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
unions "github.com/redhat-developer/odo/pkg/kclient/unions"
v11 "k8s.io/api/apps/v1"
v12 "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/api/meta"
v13 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
intstr "k8s.io/apimachinery/pkg/util/intstr"
discovery "k8s.io/client-go/discovery"
dynamic "k8s.io/client-go/dynamic"
kubernetes "k8s.io/client-go/kubernetes"
@@ -107,21 +104,6 @@ func (mr *MockClientInterfaceMockRecorder) CreateDynamicResource(exampleCustomRe
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDynamicResource", reflect.TypeOf((*MockClientInterface)(nil).CreateDynamicResource), exampleCustomResource, gvr)
}
// CreateIngress mocks base method.
func (m *MockClientInterface) CreateIngress(ingress unions.KubernetesIngress) (*unions.KubernetesIngress, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateIngress", ingress)
ret0, _ := ret[0].(*unions.KubernetesIngress)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateIngress indicates an expected call of CreateIngress.
func (mr *MockClientInterfaceMockRecorder) CreateIngress(ingress interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateIngress", reflect.TypeOf((*MockClientInterface)(nil).CreateIngress), ingress)
}
// CreateNamespace mocks base method.
func (m *MockClientInterface) CreateNamespace(name string) (*v12.Namespace, error) {
m.ctrl.T.Helper()
@@ -166,21 +148,6 @@ func (mr *MockClientInterfaceMockRecorder) CreatePVC(pvc interface{}) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePVC", reflect.TypeOf((*MockClientInterface)(nil).CreatePVC), pvc)
}
// CreateRoute mocks base method.
func (m *MockClientInterface) CreateRoute(name, serviceName string, portNumber intstr.IntOrString, labels map[string]string, secureURL bool, path string, ownerReference v13.OwnerReference) (*v10.Route, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateRoute", name, serviceName, portNumber, labels, secureURL, path, ownerReference)
ret0, _ := ret[0].(*v10.Route)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateRoute indicates an expected call of CreateRoute.
func (mr *MockClientInterfaceMockRecorder) CreateRoute(name, serviceName, portNumber, labels, secureURL, path, ownerReference interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRoute", reflect.TypeOf((*MockClientInterface)(nil).CreateRoute), name, serviceName, portNumber, labels, secureURL, path, ownerReference)
}
// CreateSecret mocks base method.
func (m *MockClientInterface) CreateSecret(objectMeta v13.ObjectMeta, data map[string]string, ownerReference v13.OwnerReference) error {
m.ctrl.T.Helper()
@@ -281,20 +248,6 @@ func (mr *MockClientInterfaceMockRecorder) DeleteDynamicResource(name, group, ve
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDynamicResource", reflect.TypeOf((*MockClientInterface)(nil).DeleteDynamicResource), name, group, version, resource)
}
// DeleteIngress mocks base method.
func (m *MockClientInterface) DeleteIngress(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIngress", name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIngress indicates an expected call of DeleteIngress.
func (mr *MockClientInterfaceMockRecorder) DeleteIngress(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIngress", reflect.TypeOf((*MockClientInterface)(nil).DeleteIngress), name)
}
// DeleteNamespace mocks base method.
func (m *MockClientInterface) DeleteNamespace(name string, wait bool) error {
m.ctrl.T.Helper()
@@ -337,20 +290,6 @@ func (mr *MockClientInterfaceMockRecorder) DeleteProject(name, wait interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProject", reflect.TypeOf((*MockClientInterface)(nil).DeleteProject), name, wait)
}
// DeleteRoute mocks base method.
func (m *MockClientInterface) DeleteRoute(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteRoute", name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteRoute indicates an expected call of DeleteRoute.
func (mr *MockClientInterfaceMockRecorder) DeleteRoute(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRoute", reflect.TypeOf((*MockClientInterface)(nil).DeleteRoute), name)
}
// DeleteSecret mocks base method.
func (m *MockClientInterface) DeleteSecret(secretName, namespace string) error {
m.ctrl.T.Helper()
@@ -643,21 +582,6 @@ func (mr *MockClientInterfaceMockRecorder) GetDynamicResource(group, version, re
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDynamicResource", reflect.TypeOf((*MockClientInterface)(nil).GetDynamicResource), group, version, resource, name)
}
// GetIngress mocks base method.
func (m *MockClientInterface) GetIngress(name string) (*unions.KubernetesIngress, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIngress", name)
ret0, _ := ret[0].(*unions.KubernetesIngress)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetIngress indicates an expected call of GetIngress.
func (mr *MockClientInterfaceMockRecorder) GetIngress(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIngress", reflect.TypeOf((*MockClientInterface)(nil).GetIngress), name)
}
// GetNamespace mocks base method.
func (m *MockClientInterface) GetNamespace(name string) (*v12.Namespace, error) {
m.ctrl.T.Helper()
@@ -733,21 +657,6 @@ func (mr *MockClientInterfaceMockRecorder) GetOneDeploymentFromSelector(selector
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneDeploymentFromSelector", reflect.TypeOf((*MockClientInterface)(nil).GetOneDeploymentFromSelector), selector)
}
// GetOneIngressFromSelector mocks base method.
func (m *MockClientInterface) GetOneIngressFromSelector(selector string) (*unions.KubernetesIngress, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOneIngressFromSelector", selector)
ret0, _ := ret[0].(*unions.KubernetesIngress)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetOneIngressFromSelector indicates an expected call of GetOneIngressFromSelector.
func (mr *MockClientInterfaceMockRecorder) GetOneIngressFromSelector(selector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneIngressFromSelector", reflect.TypeOf((*MockClientInterface)(nil).GetOneIngressFromSelector), selector)
}
// GetOnePodFromSelector mocks base method.
func (m *MockClientInterface) GetOnePodFromSelector(selector string) (*v12.Pod, error) {
m.ctrl.T.Helper()
@@ -763,21 +672,6 @@ func (mr *MockClientInterfaceMockRecorder) GetOnePodFromSelector(selector interf
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOnePodFromSelector", reflect.TypeOf((*MockClientInterface)(nil).GetOnePodFromSelector), selector)
}
// GetOneRouteFromSelector mocks base method.
func (m *MockClientInterface) GetOneRouteFromSelector(selector string) (*v10.Route, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOneRouteFromSelector", selector)
ret0, _ := ret[0].(*v10.Route)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetOneRouteFromSelector indicates an expected call of GetOneRouteFromSelector.
func (mr *MockClientInterfaceMockRecorder) GetOneRouteFromSelector(selector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneRouteFromSelector", reflect.TypeOf((*MockClientInterface)(nil).GetOneRouteFromSelector), selector)
}
// GetOneService mocks base method.
func (m *MockClientInterface) GetOneService(componentName, appName string) (*v12.Service, error) {
m.ctrl.T.Helper()
@@ -913,21 +807,6 @@ func (mr *MockClientInterfaceMockRecorder) GetRestMappingFromUnstructured(arg0 i
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRestMappingFromUnstructured", reflect.TypeOf((*MockClientInterface)(nil).GetRestMappingFromUnstructured), arg0)
}
// GetRoute mocks base method.
func (m *MockClientInterface) GetRoute(name string) (*v10.Route, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRoute", name)
ret0, _ := ret[0].(*v10.Route)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRoute indicates an expected call of GetRoute.
func (mr *MockClientInterfaceMockRecorder) GetRoute(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoute", reflect.TypeOf((*MockClientInterface)(nil).GetRoute), name)
}
// GetSecret mocks base method.
func (m *MockClientInterface) GetSecret(name, namespace string) (*v12.Secret, error) {
m.ctrl.T.Helper()
@@ -1018,21 +897,6 @@ func (mr *MockClientInterfaceMockRecorder) IsResourceSupported(apiGroup, apiVers
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsResourceSupported", reflect.TypeOf((*MockClientInterface)(nil).IsResourceSupported), apiGroup, apiVersion, resourceName)
}
// IsRouteSupported mocks base method.
func (m *MockClientInterface) IsRouteSupported() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsRouteSupported")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsRouteSupported indicates an expected call of IsRouteSupported.
func (mr *MockClientInterfaceMockRecorder) IsRouteSupported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRouteSupported", reflect.TypeOf((*MockClientInterface)(nil).IsRouteSupported))
}
// IsSSASupported mocks base method.
func (m *MockClientInterface) IsSSASupported() bool {
m.ctrl.T.Helper()
@@ -1121,21 +985,6 @@ func (mr *MockClientInterfaceMockRecorder) ListDynamicResource(group, version, r
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDynamicResource", reflect.TypeOf((*MockClientInterface)(nil).ListDynamicResource), group, version, resource)
}
// ListIngresses mocks base method.
func (m *MockClientInterface) ListIngresses(labelSelector string) (*unions.KubernetesIngressList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListIngresses", labelSelector)
ret0, _ := ret[0].(*unions.KubernetesIngressList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListIngresses indicates an expected call of ListIngresses.
func (mr *MockClientInterfaceMockRecorder) ListIngresses(labelSelector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListIngresses", reflect.TypeOf((*MockClientInterface)(nil).ListIngresses), labelSelector)
}
// ListPVCNames mocks base method.
func (m *MockClientInterface) ListPVCNames(selector string) ([]string, error) {
m.ctrl.T.Helper()
@@ -1181,21 +1030,6 @@ func (mr *MockClientInterfaceMockRecorder) ListProjectNames() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjectNames", reflect.TypeOf((*MockClientInterface)(nil).ListProjectNames))
}
// ListRoutes mocks base method.
func (m *MockClientInterface) ListRoutes(labelSelector string) ([]v10.Route, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListRoutes", labelSelector)
ret0, _ := ret[0].([]v10.Route)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListRoutes indicates an expected call of ListRoutes.
func (mr *MockClientInterfaceMockRecorder) ListRoutes(labelSelector interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoutes", reflect.TypeOf((*MockClientInterface)(nil).ListRoutes), labelSelector)
}
// ListSecrets mocks base method.
func (m *MockClientInterface) ListSecrets(labelSelector string) ([]v12.Secret, error) {
m.ctrl.T.Helper()

View File

@@ -1,90 +0,0 @@
package kclient
import (
"context"
"fmt"
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/generator"
routev1 "github.com/openshift/api/route/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/klog"
)
// IsRouteSupported checks if route resource type is present on the cluster
func (c *Client) IsRouteSupported() (bool, error) {
return c.IsResourceSupported("route.openshift.io", "v1", "routes")
}
// GetRoute gets the route with the given name
func (c *Client) GetRoute(name string) (*routev1.Route, error) {
return c.routeClient.Routes(c.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
}
// CreateRoute creates a route object for the given service and with the given labels
// serviceName is the name of the service for the target reference
// portNumber is the target port of the route
// path is the path of the endpoint URL
// 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, path string, ownerReference metav1.OwnerReference) (*routev1.Route, error) {
routeParams := generator.RouteParams{
ObjectMeta: generator.GetObjectMeta(name, c.Namespace, labels, nil),
RouteSpecParams: generator.RouteSpecParams{
ServiceName: serviceName,
PortNumber: portNumber,
Secure: secureURL,
Path: path,
},
}
route := generator.GetRoute(v1.Endpoint{}, routeParams)
route.SetOwnerReferences(append(route.GetOwnerReferences(), ownerReference))
r, err := c.routeClient.Routes(c.Namespace).Create(context.TODO(), route, metav1.CreateOptions{FieldManager: FieldManager})
if err != nil {
return nil, fmt.Errorf("error creating route: %w", err)
}
return r, nil
}
// DeleteRoute deleted the given route
func (c *Client) DeleteRoute(name string) error {
err := c.routeClient.Routes(c.Namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("unable to delete route: %w", err)
}
return nil
}
// ListRoutes lists all the routes based on the given label selector
func (c *Client) ListRoutes(labelSelector string) ([]routev1.Route, error) {
klog.V(3).Infof("Listing routes with label selector: %v", labelSelector)
routeList, err := c.routeClient.Routes(c.Namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return nil, fmt.Errorf("unable to get route list: %w", err)
}
return routeList.Items, nil
}
// GetOneRouteFromSelector gets one route with the given selector
// if no or multiple routes are found with the given selector, it throws an error
func (c *Client) GetOneRouteFromSelector(selector string) (*routev1.Route, error) {
routes, err := c.ListRoutes(selector)
if err != nil {
return nil, err
}
if num := len(routes); num == 0 {
return nil, fmt.Errorf("no ingress was found for the selector: %v", selector)
} else if num > 1 {
return nil, fmt.Errorf("multiple ingresses exist for the selector: %v. Only one must be present", selector)
}
return &routes[0], nil
}

View File

@@ -1,268 +0,0 @@
package kclient
import (
"reflect"
"testing"
appsV1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
routev1 "github.com/openshift/api/route/v1"
"github.com/redhat-developer/odo/pkg/testingutil"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
ktesting "k8s.io/client-go/testing"
)
// GenerateOwnerReference generates 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(deployment *appsV1.Deployment) metav1.OwnerReference {
ownerReference := metav1.OwnerReference{
APIVersion: "apps.openshift.io/v1",
Kind: "Deployment",
Name: deployment.Name,
UID: deployment.UID,
}
return ownerReference
}
func TestCreateRoute(t *testing.T) {
tests := []struct {
name string
urlName string
service string
portNumber intstr.IntOrString
labels map[string]string
wantErr bool
existingDeployment *appsV1.Deployment
secureURL bool
path string
}{
{
name: "Case : mailserver",
urlName: "mailserver",
service: "mailserver",
portNumber: intstr.FromInt(8080),
labels: map[string]string{
"SLA": "High",
"app.kubernetes.io/instance": "backend",
"app.kubernetes.io/name": "python",
},
wantErr: false,
existingDeployment: testingutil.CreateFakeDeployment("mailserver"),
},
{
name: "Case : blog (urlName is different than service)",
urlName: "example",
service: "blog",
portNumber: intstr.FromInt(9100),
labels: map[string]string{
"SLA": "High",
"app.kubernetes.io/instance": "backend",
"app.kubernetes.io/name": "golang",
},
wantErr: false,
existingDeployment: testingutil.CreateFakeDeployment("blog"),
},
{
name: "Case : secure url",
urlName: "example",
service: "blog",
portNumber: intstr.FromInt(9100),
labels: map[string]string{
"SLA": "High",
"app.kubernetes.io/instance": "backend",
"app.kubernetes.io/name": "golang",
},
wantErr: false,
existingDeployment: testingutil.CreateFakeDeployment("blog"),
secureURL: true,
},
{
name: "Case : specify a path",
urlName: "example",
service: "blog",
portNumber: intstr.FromInt(9100),
labels: map[string]string{
"SLA": "High",
"app.kubernetes.io/instance": "backend",
"app.kubernetes.io/name": "golang",
},
wantErr: false,
existingDeployment: testingutil.CreateFakeDeployment("blog"),
path: "/testpath",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNew()
ownerReferences := GenerateOwnerReference(tt.existingDeployment)
fkclientset.AppsClientset.PrependReactor("get", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.existingDeployment, nil
})
createdRoute, err := fkclient.CreateRoute(tt.urlName, tt.service, tt.portNumber, tt.labels, tt.secureURL, tt.path, ownerReferences)
if tt.secureURL {
wantedTLSConfig := &routev1.TLSConfig{
Termination: routev1.TLSTerminationEdge,
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
}
if !reflect.DeepEqual(createdRoute.Spec.TLS, wantedTLSConfig) {
t.Errorf("tls config is different, wanted %v, got %v", wantedTLSConfig, createdRoute.Spec.TLS)
}
} else {
if createdRoute.Spec.TLS != nil {
t.Errorf("tls config is set for a non secure url")
}
if tt.path == "" && createdRoute.Spec.Path != "/" {
t.Errorf("expect path: /, but got path: %v", createdRoute.Spec.Path)
} else if tt.path != "" && createdRoute.Spec.Path != tt.path {
t.Errorf("expect path: %v, but got path: %v", tt.path, createdRoute.Spec.Path)
}
}
// Checks for error in positive cases
if !tt.wantErr == (err != nil) {
t.Errorf(" client.CreateRoute(string, labels) unexpected error %v, wantErr %v", err, tt.wantErr)
}
// Check for validating actions performed
if len(fkclientset.RouteClientset.Actions()) != 1 {
t.Errorf("expected 1 action in CreateRoute got: %v", fkclientset.RouteClientset.Actions())
}
// Checks for return values in positive cases
if err == nil {
createdRoute := fkclientset.RouteClientset.Actions()[0].(ktesting.CreateAction).GetObject().(*routev1.Route)
// created route should be labeled with labels passed to CreateRoute
if !reflect.DeepEqual(createdRoute.Labels, tt.labels) {
t.Errorf("labels in created route is not matching expected labels, expected: %v, got: %v", tt.labels, createdRoute.Labels)
}
// route name and service that route is pointg to should match
if createdRoute.Spec.To.Name != tt.service {
t.Errorf("route is not matching to expected service name, expected: %s, got %s", tt.service, createdRoute)
}
if createdRoute.Name != tt.urlName {
t.Errorf("route name is not matching to expected route name, expected: %s, got %s", tt.urlName, createdRoute.Name)
}
if createdRoute.Spec.To.Name != tt.service {
t.Errorf("service name is not matching to expected service name, expected: %s, got %s", tt.service, createdRoute.Spec.To.Name)
}
if createdRoute.Spec.Port.TargetPort != tt.portNumber {
t.Errorf("port number is not matching to expected port number, expected: %v, got %v", tt.portNumber, createdRoute.Spec.Port.TargetPort)
}
}
})
}
}
func TestListRoutes(t *testing.T) {
type args struct {
labelSelector string
}
tests := []struct {
name string
args args
returnedRoutes routev1.RouteList
want []routev1.Route
wantErr bool
}{
{
name: "case 1: list multiple routes",
args: args{
labelSelector: "app.kubernetes.io/instance",
},
returnedRoutes: *testingutil.GetRouteListWithMultiple("nodejs", "app"),
want: []routev1.Route{
testingutil.GetSingleRoute("example", 8080, "nodejs", "app"),
testingutil.GetSingleRoute("example-1", 9100, "nodejs", "app"),
},
},
{
name: "case 2: no routes returned",
args: args{
labelSelector: "app.kubernetes.io/instance",
},
returnedRoutes: routev1.RouteList{},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNew()
fkclientset.RouteClientset.PrependReactor("list", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.returnedRoutes, nil
})
got, err := fkclient.ListRoutes(tt.args.labelSelector)
if (err != nil) != tt.wantErr {
t.Errorf("ListRoutes() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ListRoutes() got = %v, want %v", got, tt.want)
}
})
}
}
func TestGetRoute(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
returnedRoute routev1.Route
want routev1.Route
wantErr bool
}{
{
name: "case 1: existing route returned",
args: args{
name: "example",
},
returnedRoute: testingutil.GetSingleRoute("example", 8080, "nodejs", "app"),
want: testingutil.GetSingleRoute("example", 8080, "nodejs", "app"),
},
{
name: "case 2: no existing route returned",
args: args{
name: "example",
},
returnedRoute: routev1.Route{},
want: routev1.Route{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// initialising the fakeclient
fkclient, fkclientset := FakeNew()
fkclientset.RouteClientset.PrependReactor("get", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.returnedRoute, nil
})
got, err := fkclient.GetRoute(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("GetRoute() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, &tt.want) {
t.Errorf("GetRoute() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,37 +1,5 @@
package localConfigProvider
// 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"
)
// LocalURL holds URL related information
type LocalURL struct {
// Name of the URL
Name string `yaml:"Name,omitempty" json:"name,omitempty"`
// Port number for the url of the component, required in case of components which expose more than one service port
Port int `yaml:"Port,omitempty" json:"port,omitempty"`
// Indicates if the URL should be a secure https one
Secure bool `yaml:"Secure,omitempty" json:"secure,omitempty"`
// Cluster host
Host string `yaml:"Host,omitempty" json:"host,omitempty"`
// TLS secret name to create ingress to provide a secure URL
TLSSecret string `yaml:"TLSSecret,omitempty" json:"tlsSecret,omitempty"`
// Exposed port number for docker container, required for local scenarios
ExposedPort int `yaml:"ExposedPort,omitempty" json:"exposedPort,omitempty"`
// Kind is the kind of the URL
Kind URLKind `yaml:"Kind,omitempty" json:"kind,omitempty"`
// Path is the path of the URL
Path string `yaml:"-" json:"-"`
// Container is the container of the URL
Container string `yaml:"-" json:"-"`
// Protocol is the protocol of the URL
Protocol string `yaml:"-" json:"-"`
}
// LocalStorage holds storage related information
type LocalStorage struct {
// Name of the storage
@@ -60,15 +28,6 @@ type LocalConfigProvider interface {
GetDebugPort() int
GetContainers() ([]LocalContainer, error)
GetURL(name string) (*LocalURL, error)
CompleteURL(url *LocalURL) error
ValidateURL(url LocalURL) error
CreateURL(url LocalURL) error
DeleteURL(name string) error
GetContainerPorts(container string) ([]string, error)
GetComponentPorts() ([]string, error)
ListURLs() ([]LocalURL, error)
ListStorage() ([]LocalStorage, error)
Exists() bool

View File

@@ -33,48 +33,6 @@ func (m *MockLocalConfigProvider) EXPECT() *MockLocalConfigProviderMockRecorder
return m.recorder
}
// CompleteURL mocks base method.
func (m *MockLocalConfigProvider) CompleteURL(url *LocalURL) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CompleteURL", url)
ret0, _ := ret[0].(error)
return ret0
}
// CompleteURL indicates an expected call of CompleteURL.
func (mr *MockLocalConfigProviderMockRecorder) CompleteURL(url interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteURL", reflect.TypeOf((*MockLocalConfigProvider)(nil).CompleteURL), url)
}
// CreateURL mocks base method.
func (m *MockLocalConfigProvider) CreateURL(url LocalURL) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateURL", url)
ret0, _ := ret[0].(error)
return ret0
}
// CreateURL indicates an expected call of CreateURL.
func (mr *MockLocalConfigProviderMockRecorder) CreateURL(url interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateURL", reflect.TypeOf((*MockLocalConfigProvider)(nil).CreateURL), url)
}
// DeleteURL mocks base method.
func (m *MockLocalConfigProvider) DeleteURL(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteURL", name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteURL indicates an expected call of DeleteURL.
func (mr *MockLocalConfigProviderMockRecorder) DeleteURL(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteURL", reflect.TypeOf((*MockLocalConfigProvider)(nil).DeleteURL), name)
}
// Exists mocks base method.
func (m *MockLocalConfigProvider) Exists() bool {
m.ctrl.T.Helper()
@@ -103,36 +61,6 @@ func (mr *MockLocalConfigProviderMockRecorder) GetApplication() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplication", reflect.TypeOf((*MockLocalConfigProvider)(nil).GetApplication))
}
// GetComponentPorts mocks base method.
func (m *MockLocalConfigProvider) GetComponentPorts() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetComponentPorts")
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetComponentPorts indicates an expected call of GetComponentPorts.
func (mr *MockLocalConfigProviderMockRecorder) GetComponentPorts() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponentPorts", reflect.TypeOf((*MockLocalConfigProvider)(nil).GetComponentPorts))
}
// GetContainerPorts mocks base method.
func (m *MockLocalConfigProvider) GetContainerPorts(container string) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContainerPorts", container)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContainerPorts indicates an expected call of GetContainerPorts.
func (mr *MockLocalConfigProviderMockRecorder) GetContainerPorts(container interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainerPorts", reflect.TypeOf((*MockLocalConfigProvider)(nil).GetContainerPorts), container)
}
// GetContainers mocks base method.
func (m *MockLocalConfigProvider) GetContainers() ([]LocalContainer, error) {
m.ctrl.T.Helper()
@@ -190,21 +118,6 @@ func (mr *MockLocalConfigProviderMockRecorder) GetNamespace() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespace", reflect.TypeOf((*MockLocalConfigProvider)(nil).GetNamespace))
}
// GetURL mocks base method.
func (m *MockLocalConfigProvider) GetURL(name string) (*LocalURL, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetURL", name)
ret0, _ := ret[0].(*LocalURL)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetURL indicates an expected call of GetURL.
func (mr *MockLocalConfigProviderMockRecorder) GetURL(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetURL", reflect.TypeOf((*MockLocalConfigProvider)(nil).GetURL), name)
}
// ListStorage mocks base method.
func (m *MockLocalConfigProvider) ListStorage() ([]LocalStorage, error) {
m.ctrl.T.Helper()
@@ -219,32 +132,3 @@ func (mr *MockLocalConfigProviderMockRecorder) ListStorage() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStorage", reflect.TypeOf((*MockLocalConfigProvider)(nil).ListStorage))
}
// ListURLs mocks base method.
func (m *MockLocalConfigProvider) ListURLs() ([]LocalURL, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListURLs")
ret0, _ := ret[0].([]LocalURL)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListURLs indicates an expected call of ListURLs.
func (mr *MockLocalConfigProviderMockRecorder) ListURLs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListURLs", reflect.TypeOf((*MockLocalConfigProvider)(nil).ListURLs))
}
// ValidateURL mocks base method.
func (m *MockLocalConfigProvider) ValidateURL(url LocalURL) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ValidateURL", url)
ret0, _ := ret[0].(error)
return ret0
}
// ValidateURL indicates an expected call of ValidateURL.
func (mr *MockLocalConfigProviderMockRecorder) ValidateURL(url interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateURL", reflect.TypeOf((*MockLocalConfigProvider)(nil).ValidateURL), url)
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/redhat-developer/odo/pkg/odo/cli/preference"
"github.com/redhat-developer/odo/pkg/odo/cli/project"
"github.com/redhat-developer/odo/pkg/odo/cli/telemetry"
"github.com/redhat-developer/odo/pkg/odo/cli/url"
"github.com/redhat-developer/odo/pkg/odo/cli/utils"
"github.com/redhat-developer/odo/pkg/odo/cli/version"
"github.com/redhat-developer/odo/pkg/odo/util"
@@ -160,7 +159,6 @@ func odoRootCmd(name, fullName string) *cobra.Command {
login.NewCmdLogin(login.RecommendedCommandName, util.GetFullName(fullName, login.RecommendedCommandName)),
logout.NewCmdLogout(logout.RecommendedCommandName, util.GetFullName(fullName, logout.RecommendedCommandName)),
project.NewCmdProject(project.RecommendedCommandName, util.GetFullName(fullName, project.RecommendedCommandName)),
url.NewCmdURL(url.RecommendedCommandName, util.GetFullName(fullName, url.RecommendedCommandName)),
utils.NewCmdUtils(utils.RecommendedCommandName, util.GetFullName(fullName, utils.RecommendedCommandName)),
version.NewCmdVersion(version.RecommendedCommandName, util.GetFullName(fullName, version.RecommendedCommandName)),
preference.NewCmdPreference(preference.RecommendedCommandName, util.GetFullName(fullName, preference.RecommendedCommandName)),

View File

@@ -1,209 +0,0 @@
package url
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
dfutil "github.com/devfile/library/pkg/util"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/redhat-developer/odo/pkg/url"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
const createRecommendedCommandName = "create"
var (
urlCreateShortDesc = `Create a URL for a component`
urlCreateLongDesc = ktemplates.LongDesc(`Create a URL for a component.
The created URL can be used to access the specified component from outside the cluster.
`)
urlCreateExample = ktemplates.Examples(` # Create a URL with a specific name by automatically detecting the port used by the component
%[1]s example
# Create a URL for the current component with a specific port
%[1]s --port 8080
# Create a URL of ingress kind
%[1]s --port 8080 --ingress
# Create a URL with a specific name and port
%[1]s example --port 8080
# Create a URL with a subdomain and hostname for example.domain.com
%[1]s example --port 8080 --host domain.com
# Create a secure URL for the current component
%[1]s --port 8080 --secure
# Create a URL with a specific path and protocol type
%[1]s --port 8080 --path /hello --protocol http
# Create a URL under a specific container
%[1]s --port 8080 --container runtime
`)
)
// CreateOptions encapsulates the options for the odo url create command
type CreateOptions struct {
*genericclioptions.Context
contextFlag string
// Parameters
urlName string
// Flags
portFlag int
secureFlag bool
hostFlag string // host of the URL
tlsSecretFlag string // tlsSecret is the secret to te used by the URL
pathFlag string // path of the URL
protocolFlag string // protocol of the URL
containerFlag string // container to which the URL belongs
ingressFlag bool
url localConfigProvider.LocalURL
}
// NewURLCreateOptions creates a new CreateOptions instance
func NewURLCreateOptions() *CreateOptions {
return &CreateOptions{}
}
func (o *CreateOptions) SetClientset(clientset *clientset.Clientset) {}
// Complete completes CreateOptions after they've been Created
func (o *CreateOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) {
params := genericclioptions.NewCreateParameters(cmdline).NeedDevfile(o.contextFlag).RequireRouteAvailability()
o.Context, err = genericclioptions.New(params)
if err != nil {
return err
}
var urlType localConfigProvider.URLKind
if o.ingressFlag {
urlType = localConfigProvider.INGRESS
}
// get the name
if len(args) != 0 {
o.urlName = args[0]
}
// create the localURL
o.url = localConfigProvider.LocalURL{
Name: o.urlName,
Port: o.portFlag,
Secure: o.secureFlag,
Host: o.hostFlag,
TLSSecret: o.tlsSecretFlag,
Kind: urlType,
Container: o.containerFlag,
Protocol: o.protocolFlag,
Path: o.pathFlag,
}
// complete the URL
err = o.Context.LocalConfigProvider.CompleteURL(&o.url)
if err != nil {
return err
}
return nil
}
// Validate validates the CreateOptions based on completed values
func (o *CreateOptions) Validate() (err error) {
if !dfutil.CheckOutputFlag(o.GetOutputFlag()) {
return fmt.Errorf("given output format %s is not supported", o.GetOutputFlag())
}
errorList := make([]string, 0)
// Check if url name is more than 63 characters long
if len(o.urlName) > 63 {
errorList = append(errorList, "URL name must be shorter than 63 characters")
}
// validate the URL
err = o.LocalConfigProvider.ValidateURL(o.url)
if err != nil {
errorList = append(errorList, err.Error())
}
if len(errorList) > 0 {
for i := range errorList {
errorList[i] = fmt.Sprintf("\t- %s", errorList[i])
}
return fmt.Errorf("URL creation failed:\n%s", strings.Join(errorList, "\n"))
}
return nil
}
// Run contains the logic for the odo url create command
func (o *CreateOptions) Run(ctx context.Context) (err error) {
// create the URL and write it to the local config
err = o.Context.LocalConfigProvider.CreateURL(o.url)
if err != nil {
return err
}
log.Successf("URL %s created for component: %v", o.url.Name, o.LocalConfigProvider.GetName())
log.Italic("\nTo apply the URL configuration changes, please use `odo dev`")
if log.IsJSON() {
u := url.NewURLFromLocalURL(o.url)
u.Status.State = url.StateTypeNotPushed
machineoutput.OutputSuccess(u)
}
return nil
}
// NewCmdURLCreate implements the odo url create command.
func NewCmdURLCreate(name, fullName string) *cobra.Command {
o := NewURLCreateOptions()
urlCreateCmd := &cobra.Command{
Use: name + " [url name]",
Short: urlCreateShortDesc,
Long: urlCreateLongDesc,
Example: fmt.Sprintf(urlCreateExample, fullName),
Args: cobra.MaximumNArgs(1),
Annotations: map[string]string{"machineoutput": "json"},
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
clientset.Add(urlCreateCmd, clientset.PROJECT, clientset.PREFERENCE)
urlCreateCmd.Flags().IntVarP(&o.portFlag, "port", "", -1, "Port number for the url of the component, required in case of components which expose more than one service port")
urlCreateCmd.Flags().StringVar(&o.tlsSecretFlag, "tls-secret", "", "TLS secret name for the url of the component if the user bring their own TLS secret")
urlCreateCmd.Flags().StringVarP(&o.hostFlag, "host", "", "", "Cluster IP for this URL (alias \"--hostname\")")
// Alias for `--host` in case someone where to enter --hostname by mistake, and then mark it as hidden.
urlCreateCmd.Flags().StringVarP(&o.hostFlag, "hostname", "", "", "Alias for --host")
_ = urlCreateCmd.Flags().MarkHidden("hostname")
urlCreateCmd.Flags().BoolVar(&o.ingressFlag, "ingress", false, "Create an Ingress instead of Route on OpenShift clusters")
urlCreateCmd.Flags().BoolVarP(&o.secureFlag, "secure", "", false, "Create a secure HTTPS URL")
urlCreateCmd.Flags().StringVarP(&o.pathFlag, "path", "", "", "path for this URL")
urlCreateCmd.Flags().StringVarP(&o.protocolFlag, "protocol", "", string(devfilev1.HTTPEndpointProtocol), "protocol for this URL")
urlCreateCmd.Flags().StringVarP(&o.containerFlag, "container", "", "", "container of the endpoint in devfile")
odoutil.AddContextFlag(urlCreateCmd, &o.contextFlag)
completion.RegisterCommandFlagHandler(urlCreateCmd, "context", completion.FileCompletionHandler)
return urlCreateCmd
}

View File

@@ -1,111 +0,0 @@
package url
import (
"context"
"fmt"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli/ui"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
const deleteRecommendedCommandName = "delete"
var (
urlDeleteShortDesc = `Delete a URL`
urlDeleteLongDesc = ktemplates.LongDesc(`Delete the given URL, hence making the service inaccessible.`)
urlDeleteExample = ktemplates.Examples(` # Delete a URL to a component
%[1]s myurl
`)
)
// DeleteOptions encapsulates the options for the odo url delete command
type DeleteOptions struct {
*genericclioptions.Context
contextFlag string
// Parameters
urlName string
// Flags
forceFlag bool
}
// NewURLDeleteOptions creates a new DeleteOptions instance
func NewURLDeleteOptions() *DeleteOptions {
return &DeleteOptions{}
}
func (o *DeleteOptions) SetClientset(clientset *clientset.Clientset) {}
// Complete completes DeleteOptions after they've been Deleted
func (o *DeleteOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) {
o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile(o.contextFlag))
if err != nil {
return err
}
o.urlName = args[0]
return nil
}
// Validate validates the DeleteOptions based on completed values
func (o *DeleteOptions) Validate() (err error) {
url, err := o.Context.LocalConfigProvider.GetURL(o.urlName)
if err != nil {
return err
}
if url == nil {
return fmt.Errorf("the URL %s does not exist within the component %s", o.urlName, o.LocalConfigProvider.GetName())
}
return nil
}
// Run contains the logic for the odo url delete command
func (o *DeleteOptions) Run(ctx context.Context) (err error) {
if o.forceFlag || ui.Proceed(fmt.Sprintf("Are you sure you want to delete the url %v", o.urlName)) {
err := o.LocalConfigProvider.DeleteURL(o.urlName)
if err != nil {
return nil
}
log.Successf("URL %s removed from component %s", o.urlName, o.LocalConfigProvider.GetName())
} else {
return fmt.Errorf("aborting deletion of URL: %v", o.urlName)
}
return
}
// NewCmdURLDelete implements the odo url delete command.
func NewCmdURLDelete(name, fullName string) *cobra.Command {
o := NewURLDeleteOptions()
urlDeleteCmd := &cobra.Command{
Use: name + " [url name]",
Short: urlDeleteShortDesc,
Long: urlDeleteLongDesc,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
Example: fmt.Sprintf(urlDeleteExample, fullName),
}
clientset.Add(urlDeleteCmd, clientset.PROJECT, clientset.PREFERENCE)
urlDeleteCmd.Flags().BoolVarP(&o.forceFlag, "force", "f", false, "Delete url without prompting")
odoutil.AddContextFlag(urlDeleteCmd, &o.contextFlag)
completion.RegisterCommandHandler(urlDeleteCmd, completion.URLCompletionHandler)
completion.RegisterCommandFlagHandler(urlDeleteCmd, "context", completion.FileCompletionHandler)
return urlDeleteCmd
}

View File

@@ -1,148 +0,0 @@
package url
import (
"context"
"fmt"
"io"
"os"
"text/tabwriter"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/machineoutput"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/util/completion"
"github.com/redhat-developer/odo/pkg/url"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
)
const listRecommendedCommandName = "list"
var (
urlListShortDesc = `List URLs`
urlListLongDesc = ktemplates.LongDesc(`Lists all the available URLs which can be used to access the components.`)
urlListExample = ktemplates.Examples(` # List the available URLs
%[1]s
`)
)
// ListOptions encapsulates the options for the odo url list command
type ListOptions struct {
// Context
*genericclioptions.Context
// Flags
contextFlag string
// Backend
client url.Client
}
// NewURLListOptions creates a new URLCreateOptions instance
func NewURLListOptions() *ListOptions {
return &ListOptions{}
}
func (o *ListOptions) SetClientset(clientset *clientset.Clientset) {
}
// Complete completes ListOptions after they've been Listed
func (o *ListOptions) Complete(cmdline cmdline.Cmdline, args []string) (err error) {
o.Context, err = genericclioptions.New(genericclioptions.NewCreateParameters(cmdline).NeedDevfile(o.contextFlag))
if err != nil {
return err
}
routeSupported, err := o.Context.KClient.IsRouteSupported()
if err != nil {
return err
}
o.client = url.NewClient(url.ClientOptions{
LocalConfigProvider: o.Context.LocalConfigProvider,
Client: o.Context.KClient,
IsRouteSupported: routeSupported,
})
return nil
}
// Validate validates the ListOptions based on completed values
func (o *ListOptions) Validate() (err error) {
return odoutil.CheckOutputFlag(o.GetOutputFlag())
}
// Run contains the logic for the odo url list command
func (o *ListOptions) Run(ctx context.Context) (err error) {
componentName := o.Context.LocalConfigProvider.GetName()
urls, err := o.client.List()
if err != nil {
return err
}
if log.IsJSON() {
machineoutput.OutputSuccess(urls)
} else {
err = HumanReadableOutput(os.Stdout, urls, componentName)
if err != nil {
return err
}
if urls.AreOutOfSync() {
log.Info("There are local changes. Please run 'odo dev'.")
}
}
return nil
}
// NewCmdURLList implements the odo url list command.
func NewCmdURLList(name, fullName string) *cobra.Command {
o := NewURLListOptions()
urlListCmd := &cobra.Command{
Use: name,
Short: urlListShortDesc,
Long: urlListLongDesc,
Example: fmt.Sprintf(urlListExample, fullName),
Args: cobra.NoArgs,
Annotations: map[string]string{"machineoutput": "json"},
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
odoutil.AddContextFlag(urlListCmd, &o.contextFlag)
completion.RegisterCommandFlagHandler(urlListCmd, "context", completion.FileCompletionHandler)
return urlListCmd
}
// HumanReadableOutput outputs the list of projects in a human readable format
func HumanReadableOutput(w io.Writer, urls url.URLList, componentName string) error {
if len(urls.Items) == 0 {
return fmt.Errorf("no URLs found for component %v. Refer `odo url create -h` to add one", componentName)
}
log.Infof("Found the following URLs for component %v", componentName)
tabWriterURL := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
fmt.Fprintln(tabWriterURL, "NAME", "\t", "STATE", "\t", "URL", "\t", "PORT", "\t", "SECURE", "\t", "KIND")
// are there changes between local and cluster states?
for _, u := range urls.Items {
if u.Spec.Kind == localConfigProvider.ROUTE {
var urlStr string
if u.Status.State == url.StateTypeNotPushed {
urlStr = "<provided by cluster>"
} else {
urlStr = url.GetURLString(u.Spec.Protocol, u.Spec.Host, "")
}
fmt.Fprintln(tabWriterURL, u.Name, "\t", u.Status.State, "\t", urlStr, "\t", u.Spec.Port, "\t", u.Spec.Secure, "\t", u.Spec.Kind)
} else {
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, "\t", u.Spec.Kind)
}
}
tabWriterURL.Flush()
return nil
}

View File

@@ -1,43 +0,0 @@
package url
import (
"fmt"
ktemplates "k8s.io/kubectl/pkg/util/templates"
odoutil "github.com/redhat-developer/odo/pkg/odo/util"
"github.com/spf13/cobra"
)
// RecommendedCommandName is the recommended url command name
const RecommendedCommandName = "url"
var (
urlShortDesc = `Expose component to the outside world`
urlLongDesc = ktemplates.LongDesc(`Expose component to the outside world.
The URLs that are generated using this command, can be used to access the deployed components from outside the cluster.`)
)
// NewCmdURL returns the top-level url command
func NewCmdURL(name, fullName string) *cobra.Command {
urlCreateCmd := NewCmdURLCreate(createRecommendedCommandName, odoutil.GetFullName(fullName, createRecommendedCommandName))
urlDeleteCmd := NewCmdURLDelete(deleteRecommendedCommandName, odoutil.GetFullName(fullName, deleteRecommendedCommandName))
urlListCmd := NewCmdURLList(listRecommendedCommandName, odoutil.GetFullName(fullName, listRecommendedCommandName))
urlCmd := &cobra.Command{
Use: name,
Short: urlShortDesc,
Long: urlLongDesc,
Example: fmt.Sprintf("%s\n\n%s\n\n%s",
urlCreateCmd.Example,
urlDeleteCmd.Example,
urlListCmd.Example,
),
}
// Add a defined annotation in order to appear in the help menu
urlCmd.SetUsageTemplate(odoutil.CmdUsageTemplate)
urlCmd.AddCommand(urlCreateCmd, urlDeleteCmd, urlListCmd)
return urlCmd
}

View File

@@ -55,23 +55,17 @@ type internalCxt struct {
// CreateParameters defines the options which can be provided while creating the context
type CreateParameters struct {
cmdline cmdline.Cmdline
componentContext string
routeAvailability bool
devfile bool
offline bool
appIfNeeded bool
cmdline cmdline.Cmdline
componentContext string
devfile bool
offline bool
appIfNeeded bool
}
func NewCreateParameters(cmdline cmdline.Cmdline) CreateParameters {
return CreateParameters{cmdline: cmdline}
}
func (o CreateParameters) RequireRouteAvailability() CreateParameters {
o.routeAvailability = true
return o
}
func (o CreateParameters) NeedDevfile(ctx string) CreateParameters {
o.devfile = true
o.componentContext = ctx
@@ -123,14 +117,6 @@ func New(parameters CreateParameters) (*Context, error) {
return nil, err
}
}
if parameters.routeAvailability {
isRouteSupported, err := ctx.KClient.IsRouteSupported()
if err != nil {
return nil, err
}
ctx.EnvSpecificInfo.SetIsRouteSupported(isRouteSupported)
}
}
ctx.devfilePath = location.DevfileLocation(parameters.componentContext)

View File

@@ -1,74 +0,0 @@
package testingutil
import (
"fmt"
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/pkg/devfile/generator"
routev1 "github.com/openshift/api/route/v1"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/url/labels"
"github.com/redhat-developer/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.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: urlName,
},
},
Spec: routev1.RouteSpec{
Host: "example.com",
To: routev1.RouteTargetReference{
Kind: "Service",
Name: fmt.Sprintf("%s-%s", componentName, applicationName),
},
Port: &routev1.RoutePort{
TargetPort: intstr.FromInt(port),
},
Path: "/",
},
}
}
// GetSingleSecureRoute returns a secure route generated with the given parameters
func GetSingleSecureRoute(urlName string, port int, componentName, applicationName string) routev1.Route {
generatedRoute := *generator.GetRoute(v1.Endpoint{}, generator.RouteParams{
ObjectMeta: metav1.ObjectMeta{
Name: urlName,
Labels: map[string]string{
applabels.ApplicationLabel: applicationName,
componentlabels.KubernetesInstanceLabel: componentName,
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
labels.URLLabel: urlName,
applabels.App: applicationName,
},
},
RouteSpecParams: generator.RouteSpecParams{
ServiceName: componentName,
PortNumber: intstr.FromInt(port),
Secure: true,
},
})
generatedRoute.Spec.Host = "example.com"
return generatedRoute
}

View File

@@ -1,330 +0,0 @@
package url
import (
"errors"
"fmt"
"sort"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/kclient/unions"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
urlLabels "github.com/redhat-developer/odo/pkg/url/labels"
"github.com/redhat-developer/odo/pkg/util"
"github.com/devfile/library/pkg/devfile/generator"
dfutil "github.com/devfile/library/pkg/util"
routev1 "github.com/openshift/api/route/v1"
appsV1 "k8s.io/api/apps/v1"
iextensionsv1 "k8s.io/api/extensions/v1beta1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/klog"
)
// kubernetesClient contains information required for devfile based URL based operations
type kubernetesClient struct {
generic
isRouteSupported bool
client kclient.ClientInterface
// if we don't have access to the local config
// we can use the deployment to call ListFromCluster() and
// directly list storage from the cluster without the local config
deployment *appsV1.Deployment
}
// ListFromCluster lists both route and ingress based URLs from the cluster
func (k kubernetesClient) ListFromCluster() (URLList, error) {
if k.componentName == "" || k.appName == "" {
return URLList{}, fmt.Errorf("the component name, the app name or both are empty")
}
labelSelector := componentlabels.GetSelector(k.componentName, k.appName)
klog.V(4).Infof("Listing ingresses with label selector: %v", labelSelector)
ingresses, err := k.client.ListIngresses(labelSelector)
if err != nil {
return URLList{}, fmt.Errorf("unable to list ingress: %w", err)
}
var routes []routev1.Route
if k.isRouteSupported {
routes, err = k.client.ListRoutes(labelSelector)
if err != nil {
return URLList{}, fmt.Errorf("unable to list routes: %w", err)
}
}
var clusterURLs []URL
clusterURLs = append(clusterURLs, NewURLsFromKubernetesIngressList(ingresses)...)
for _, r := range routes {
// ignore the routes created by ingresses
if r.OwnerReferences != nil && r.OwnerReferences[0].Kind == "Ingress" {
continue
}
clusterURL := NewURL(r)
clusterURLs = append(clusterURLs, clusterURL)
}
return NewURLList(clusterURLs), nil
}
// List lists both route/ingress based URLs and local URLs with respective states
func (k kubernetesClient) List() (URLList, error) {
// get the URLs present on the cluster
clusterURLMap := make(map[string]URL)
var clusterURLs URLList
var err error
if k.client != nil {
clusterURLs, err = k.ListFromCluster()
if err != nil {
return URLList{}, fmt.Errorf("unable to list routes: %w", err)
}
}
for _, url := range clusterURLs.Items {
clusterURLMap[url.Name] = url
}
localMap := make(map[string]URL)
if k.localConfigProvider.Exists() {
// get the URLs present on the localConfigProvider
localURLS, err := k.localConfigProvider.ListURLs()
if err != nil {
return URLList{}, err
}
for _, url := range localURLS {
if !k.isRouteSupported && url.Kind == localConfigProvider.ROUTE {
continue
}
localURL := NewURLFromEnvinfoURL(url, k.componentName)
if localURL.Spec.Protocol == "" {
if localURL.Spec.Secure {
localURL.Spec.Protocol = "https"
} else {
localURL.Spec.Protocol = "http"
}
}
localMap[url.Name] = localURL
}
}
// find the URLs which are present on the cluster but not on the localConfigProvider
// if not found on the localConfigProvider, mark them as 'StateTypeLocallyDeleted'
// else mark them as 'StateTypePushed'
var urls sortableURLs
for URLName, clusterURL := range clusterURLMap {
_, found := localMap[URLName]
if found {
// URL is in both local env file and cluster
clusterURL.Status.State = StateTypePushed
urls = append(urls, clusterURL)
} else {
// URL is on the cluster but not in local env file
clusterURL.Status.State = StateTypeLocallyDeleted
urls = append(urls, clusterURL)
}
}
// find the URLs which are present on the localConfigProvider but not on the cluster
// if not found on the cluster, mark them as 'StateTypeNotPushed'
for localName, localURL := range localMap {
_, remoteURLFound := clusterURLMap[localName]
if !remoteURLFound {
// URL is in the local env file but not pushed to cluster
localURL.Status.State = StateTypeNotPushed
urls = append(urls, localURL)
}
}
// sort urls by name to get consistent output
sort.Sort(urls)
urlList := NewURLList(urls)
return urlList, nil
}
// Delete deletes the URL with the given name and kind
func (k kubernetesClient) Delete(name string, kind localConfigProvider.URLKind) error {
if k.componentName == "" || k.appName == "" {
return fmt.Errorf("the component name, the app name or both are empty")
}
selector := util.ConvertLabelsToSelector(urlLabels.GetLabels(name, k.componentName, k.appName, false))
switch kind {
case localConfigProvider.INGRESS:
ingress, err := k.client.GetOneIngressFromSelector(selector)
if err != nil {
return err
}
return k.client.DeleteIngress(ingress.GetName())
case localConfigProvider.ROUTE:
route, err := k.client.GetOneRouteFromSelector(selector)
if err != nil {
return err
}
return k.client.DeleteRoute(route.Name)
default:
return fmt.Errorf("url type is not supported")
}
}
// Create creates a route or ingress based on the given URL
func (k kubernetesClient) Create(url URL) (string, error) {
if k.componentName == "" || k.appName == "" {
return "", fmt.Errorf("the component name, the app name or both are empty")
}
if url.Spec.Kind != localConfigProvider.INGRESS && url.Spec.Kind != localConfigProvider.ROUTE {
return "", fmt.Errorf("urlKind %s is not supported for URL creation", url.Spec.Kind)
}
if !url.Spec.Secure && url.Spec.TLSSecret != "" {
return "", fmt.Errorf("secret name can only be used for secure URLs")
}
labels := urlLabels.GetLabels(url.Name, k.componentName, k.appName, true)
if url.Spec.Kind == localConfigProvider.INGRESS {
return k.createIngress(url, labels)
} else {
if !k.isRouteSupported {
return "", errors.New("routes are not available on non OpenShift clusters")
}
return k.createRoute(url, labels)
}
}
// createIngress creates a ingress for the given URL with the given labels
func (k kubernetesClient) createIngress(url URL, labels map[string]string) (string, error) {
if url.Spec.Host == "" {
return "", errors.New("the host cannot be empty")
}
service, err := k.client.GetOneService(k.componentName, k.appName)
if err != nil {
return "", err
}
ingressDomain := fmt.Sprintf("%v.%v", url.Name, url.Spec.Host)
// generate the owner reference
if k.deployment == nil {
k.deployment, err = k.client.GetOneDeployment(k.componentName, k.appName)
if err != nil {
return "", err
}
}
ownerReference := generator.GetOwnerReference(k.deployment)
if url.Spec.Secure {
if len(url.Spec.TLSSecret) != 0 {
// get the user given secret
_, err = k.client.GetSecret(url.Spec.TLSSecret, k.client.GetCurrentNamespace())
if err != nil {
return "", fmt.Errorf("unable to get the provided secret %q: %w", url.Spec.TLSSecret, err)
}
} else {
// get the default secret
defaultTLSSecretName := getDefaultTLSSecretName(url.Name, k.componentName, k.appName)
_, err = k.client.GetSecret(defaultTLSSecretName, k.client.GetCurrentNamespace())
// create tls secret if it does not exist
if kerrors.IsNotFound(err) {
selfSignedCert, e := kclient.GenerateSelfSignedCertificate(url.Spec.Host)
if e != nil {
return "", fmt.Errorf("unable to generate self-signed certificate for clutser %q: %w"+url.Spec.Host, e)
}
// create tls secret
secretLabels := componentlabels.GetLabels(k.componentName, k.appName, true)
objectMeta := metav1.ObjectMeta{
Name: defaultTLSSecretName,
Labels: secretLabels,
OwnerReferences: []v1.OwnerReference{
ownerReference,
},
}
secret, e := k.client.CreateTLSSecret(selfSignedCert.CertPem, selfSignedCert.KeyPem, objectMeta)
if e != nil {
return "", fmt.Errorf("unable to create tls secret: %w", e)
}
url.Spec.TLSSecret = secret.Name
} else if err != nil {
return "", err
} else {
// tls secret found for this component
url.Spec.TLSSecret = defaultTLSSecretName
}
}
}
suffix := util.GetAdler32Value(url.Name + k.appName + k.componentName)
ingressName, err := dfutil.NamespaceOpenShiftObject(url.Name, suffix)
if err != nil {
return "", err
}
objectMeta := generator.GetObjectMeta(k.componentName, k.client.GetCurrentNamespace(), labels, nil)
// to avoid error due to duplicate ingress name defined in different devfile components
objectMeta.Name = ingressName
objectMeta.OwnerReferences = append(objectMeta.OwnerReferences, ownerReference)
ingressParam := generator.IngressParams{
ObjectMeta: objectMeta,
IngressSpecParams: generator.IngressSpecParams{
ServiceName: service.Name,
IngressDomain: ingressDomain,
PortNumber: intstr.FromInt(url.Spec.Port),
TLSSecretName: url.Spec.TLSSecret,
Path: url.Spec.Path,
},
}
ingress := unions.NewKubernetesIngressFromParams(ingressParam)
// Pass in the namespace name, link to the service (componentName) and labels to create a ingress
i, err := k.client.CreateIngress(*ingress)
if err != nil {
return "", fmt.Errorf("unable to create ingress %w", err)
}
return i.GetURLString(), nil
}
// createRoute creates a route for the given URL with the given labels
func (k kubernetesClient) createRoute(url URL, labels map[string]string) (string, error) {
// to avoid error due to duplicate ingress name defined in different devfile components
// we avoid using the getResourceName() and use the previous method from s2i
// as the host name, which is automatically created on openshift,
// can become more than 63 chars, which is invalid
suffix := util.GetAdler32Value(url.Name + k.appName + k.componentName)
routeName, err := dfutil.NamespaceOpenShiftObject(url.Name, suffix)
if err != nil {
return "", fmt.Errorf("unable to create namespaced name: %w", err)
}
if k.deployment == nil {
k.deployment, err = k.client.GetOneDeployment(k.componentName, k.appName)
if err != nil {
return "", err
}
}
ownerReference := generator.GetOwnerReference(k.deployment)
service, err := k.client.GetOneService(k.componentName, k.appName)
if err != nil {
return "", err
}
// Pass in the namespace name, link to the service (componentName) and labels to create a route
route, err := k.client.CreateRoute(routeName, service.Name, intstr.FromInt(url.Spec.Port), labels, url.Spec.Secure, url.Spec.Path, ownerReference)
if err != nil {
if kerrors.IsAlreadyExists(err) {
return "", fmt.Errorf("url named %q already exists in the same app named %q", url.Name, k.appName)
}
return "", fmt.Errorf("unable to create route: %w", err)
}
return GetURLString(GetProtocol(*route, iextensionsv1.Ingress{}), route.Spec.Host, ""), nil
}

View File

@@ -1,985 +0,0 @@
package url
import (
"fmt"
"reflect"
"testing"
"github.com/redhat-developer/odo/pkg/kclient/unions"
networkingv1 "k8s.io/api/networking/v1"
"github.com/devfile/library/pkg/devfile/generator"
"github.com/golang/mock/gomock"
"github.com/kylelemons/godebug/pretty"
routev1 "github.com/openshift/api/route/v1"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/kclient/fake"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/testingutil"
urlLabels "github.com/redhat-developer/odo/pkg/url/labels"
"github.com/redhat-developer/odo/pkg/version"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1 "k8s.io/api/extensions/v1beta1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
ktesting "k8s.io/client-go/testing"
)
func getFakeURL(name string, host string, port int, path string, protocol string, kind localConfigProvider.URLKind, urlState StateType) URL {
return URL{
TypeMeta: v1.TypeMeta{
Kind: "URL",
APIVersion: "odo.dev/v1alpha1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
},
Spec: URLSpec{
Host: host,
Protocol: protocol,
Kind: kind,
Path: path,
Port: port,
},
Status: URLStatus{
State: urlState,
},
}
}
func Test_kubernetesClient_ListCluster(t *testing.T) {
componentName := "nodejs"
appName := "app"
ingress0 := fake.GetSingleKubernetesIngress("testIngress0", componentName, appName, true, false)
ingress1 := fake.GetSingleKubernetesIngress("testIngress1", componentName, appName, true, false)
route0 := testingutil.GetSingleRoute("testRoute0", 8080, componentName, appName)
route1 := testingutil.GetSingleRoute("testRoute1", 8080, componentName, appName)
routeOwnedByIngress := testingutil.GetSingleRoute("testRoute1-ingress", 8080, componentName, appName)
routeOwnedByIngress.SetOwnerReferences([]v1.OwnerReference{
{
Kind: "Ingress",
},
})
type fields struct {
generic generic
isRouteSupported bool
}
tests := []struct {
name string
fields fields
returnedIngresses unions.KubernetesIngressList
returnedRoutes routev1.RouteList
want URLList
wantErr bool
}{
{
name: "case 1: list ingresses when route resource is not supported",
fields: fields{
generic: generic{
appName: "app",
componentName: componentName,
},
isRouteSupported: false,
},
returnedIngresses: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
ingress0,
ingress1,
},
},
want: NewURLList([]URL{
NewURLFromKubernetesIngress(ingress0, false),
NewURLFromKubernetesIngress(ingress1, false),
}),
},
{
name: "case 2: only route based URLs are pushed",
fields: fields{
generic: generic{
appName: "app",
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route0,
route1,
},
},
want: NewURLList([]URL{
NewURL(route0),
NewURL(route1)},
),
},
{
name: "case 3: both route and ingress based URLs are pushed",
fields: fields{
generic: generic{
appName: "app",
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route0,
route1,
},
},
returnedIngresses: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
ingress0,
ingress1,
},
},
want: NewURLList([]URL{
NewURLFromKubernetesIngress(ingress0, false),
NewURLFromKubernetesIngress(ingress1, false),
NewURL(route0),
NewURL(route1),
}),
},
{
name: "case 4: no urls are pushed",
fields: fields{
generic: generic{
appName: "app",
componentName: componentName,
},
isRouteSupported: true,
},
want: NewURLList(nil),
},
{
name: "case 5: ignore the routes with ingress kind owners",
fields: fields{
generic: generic{
appName: "app",
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route0,
routeOwnedByIngress,
},
},
want: NewURLList([]URL{
NewURL(route0)},
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fkclient, fkclientset := kclient.FakeNewWithIngressSupports(true, false)
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("list", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
if action.GetResource().GroupVersion().Group == "networking.k8s.io" {
return true, tt.returnedIngresses.GetNetworkingV1IngressList(true), nil
}
return true, tt.returnedIngresses.GetExtensionV1Beta1IngresList(true), nil
})
fkclientset.RouteClientset.PrependReactor("list", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.returnedRoutes, nil
})
k := kubernetesClient{
generic: tt.fields.generic,
isRouteSupported: tt.fields.isRouteSupported,
client: fkclient,
}
got, err := k.ListFromCluster()
if (err != nil) != tt.wantErr {
t.Errorf("ListFromCluster() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ListFromCluster() error: %v", pretty.Compare(got, tt.want))
}
})
}
}
func Test_kubernetesClient_List(t *testing.T) {
componentName := "nodejs"
appName := "app"
route0 := testingutil.GetSingleRoute("testRoute0", 8080, componentName, appName)
route1 := testingutil.GetSingleRoute("testRoute1", 8080, componentName, appName)
ingress0 := fake.GetSingleKubernetesIngress("testIngress0", componentName, appName, true, false)
type fields struct {
generic generic
isRouteSupported bool
}
tests := []struct {
name string
fields fields
returnedRoutes routev1.RouteList
returnedIngress unions.KubernetesIngressList
returnedLocalURLs []localConfigProvider.LocalURL
want URLList
wantErr bool
}{
{
name: "case 1: two urls in local config and none pushed",
fields: fields{
generic: generic{
appName: appName,
componentName: componentName,
},
isRouteSupported: true,
},
returnedLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-1",
Port: 8080,
Secure: false,
Host: "com",
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-2",
Port: 8080,
Secure: false,
Host: "com",
Kind: localConfigProvider.INGRESS,
},
},
want: NewURLList([]URL{
getFakeURL("example-1", "example-1.com", 8080, "", "http", localConfigProvider.INGRESS, StateTypeNotPushed),
getFakeURL("example-2", "example-2.com", 8080, "", "http", localConfigProvider.INGRESS, StateTypeNotPushed)}),
},
{
name: "case 2: two urls pushed but are deleted locally",
fields: fields{
generic: generic{
appName: appName,
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route0,
route1,
},
},
returnedLocalURLs: []localConfigProvider.LocalURL{},
want: NewURLList([]URL{
getFakeURL("testRoute0", "example.com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeLocallyDeleted),
getFakeURL("testRoute1", "example.com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeLocallyDeleted)}),
},
{
name: "case 3: two urls which are pushed",
fields: fields{
generic: generic{
appName: appName,
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route0,
},
},
returnedIngress: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
ingress0,
},
},
returnedLocalURLs: []localConfigProvider.LocalURL{
{
Name: "testRoute0",
Port: 8080,
Secure: false,
Path: "/",
Protocol: "http",
Kind: localConfigProvider.ROUTE,
},
{
Name: "testIngress0",
Port: 8080,
Secure: false,
Host: "com",
Kind: localConfigProvider.INGRESS,
},
},
want: NewURLList([]URL{
getFakeURL("testIngress0", "testIngress0.com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypePushed),
getFakeURL("testRoute0", "example.com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypePushed),
}),
},
{
name: "case 4: three URLs with mixed states",
fields: fields{
generic: generic{
appName: appName,
componentName: componentName,
},
isRouteSupported: true,
},
returnedRoutes: routev1.RouteList{
Items: []routev1.Route{
route1,
},
},
returnedIngress: unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
ingress0,
},
},
returnedLocalURLs: []localConfigProvider.LocalURL{
{
Name: "testRoute0",
Port: 8080,
Secure: false,
Path: "/",
Kind: localConfigProvider.ROUTE,
},
{
Name: "testIngress0",
Port: 8080,
Secure: false,
Host: "com",
Kind: localConfigProvider.INGRESS,
},
},
want: NewURLList([]URL{
getFakeURL("testIngress0", "testIngress0.com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypePushed),
getFakeURL("testRoute0", "", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed),
getFakeURL("testRoute1", "example.com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeLocallyDeleted),
}),
},
{
name: "case 5: ignore routes when route resources are not supported",
fields: fields{
generic: generic{
appName: appName,
componentName: componentName,
},
isRouteSupported: false,
},
returnedLocalURLs: []localConfigProvider.LocalURL{
{
Name: "testRoute0",
Port: 8080,
Secure: false,
Host: "com",
Kind: localConfigProvider.ROUTE,
},
{
Name: "testIngress0",
Port: 8080,
Secure: false,
Host: "com",
Path: "/",
Kind: localConfigProvider.INGRESS,
},
},
want: NewURLList([]URL{
getFakeURL("testIngress0", "testIngress0.com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed),
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLocalConfig := localConfigProvider.NewMockLocalConfigProvider(ctrl)
mockLocalConfig.EXPECT().ListURLs().Return(tt.returnedLocalURLs, nil)
mockLocalConfig.EXPECT().Exists().Return(true)
fkclient, fkclientset := kclient.FakeNewWithIngressSupports(true, false)
fkclient.Namespace = "default"
fkclientset.Kubernetes.PrependReactor("list", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
if action.GetResource().GroupVersion().Group == "networking.k8s.io" {
return true, tt.returnedIngress.GetNetworkingV1IngressList(true), nil
}
return true, tt.returnedIngress.GetExtensionV1Beta1IngresList(true), nil
})
fkclientset.RouteClientset.PrependReactor("list", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &tt.returnedRoutes, nil
})
tt.fields.generic.localConfigProvider = mockLocalConfig
k := kubernetesClient{
generic: tt.fields.generic,
isRouteSupported: tt.fields.isRouteSupported,
client: fkclient,
}
got, err := k.List()
if (err != nil) != tt.wantErr {
t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("List() error: %v", pretty.Compare(got, tt.want))
}
})
}
}
func Test_kubernetesClient_createIngress(t *testing.T) {
type fields struct {
generic generic
isRouteSupported bool
}
type args struct {
url URL
}
tests := []struct {
name string
fields fields
args args
createdIngress *unions.KubernetesIngress
defaultTLSExists bool
userGivenTLSExists bool
want string
wantErr bool
}{
{
name: "Case 1: Create a ingress, with same name as component",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: getFakeURL("nodejs", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed),
},
createdIngress: fake.GetSingleKubernetesIngress("nodejs-322d0648", "nodejs", "app", true, false),
want: "http://nodejs.com",
wantErr: false,
},
{
name: "Case 2: Create a ingress, with different name as component",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed),
},
createdIngress: fake.GetSingleKubernetesIngress("example-38d306b1", "nodejs", "app", true, false),
want: "http://example.com",
wantErr: false,
},
{
name: "Case 3: Create a secure ingress, default tls exists",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed)
url.Spec.Secure = true
return url
}(),
},
createdIngress: fake.GetSingleKubernetesIngress("example-38d306b1", "nodejs", "app", true, false),
defaultTLSExists: true,
want: "https://example.com",
wantErr: false,
},
{
name: "Case 4: Create a secure ingress and default tls doesn't exist",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed)
url.Spec.Secure = true
return url
}(),
},
createdIngress: fake.GetSingleKubernetesIngress("example-38d306b1", "nodejs", "app", true, false),
defaultTLSExists: false,
want: "https://example.com",
wantErr: false,
},
{
name: "Case 5: Fail when while creating ingress when user given tls secret doesn't exists",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed)
url.Spec.Secure = true
url.Spec.TLSSecret = "user-secret"
return url
}(),
},
defaultTLSExists: false,
userGivenTLSExists: false,
want: "http://example.com",
wantErr: true,
},
{
name: "Case 6: Create a secure ingress, user tls secret does exists",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed)
url.Spec.Secure = true
url.Spec.TLSSecret = "user-secret"
return url
}(),
},
createdIngress: fake.GetSingleKubernetesIngress("example-38d306b1", "nodejs", "app", true, false),
defaultTLSExists: false,
userGivenTLSExists: true,
want: "https://example.com",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var serviceName string
if tt.args.url.Spec.Kind == localConfigProvider.INGRESS {
serviceName = tt.fields.generic.componentName
}
fakeKClient, fakeKClientSet := kclient.FakeNewWithIngressSupports(true, false)
k := kubernetesClient{
generic: tt.fields.generic,
isRouteSupported: tt.fields.isRouteSupported,
client: fakeKClient,
}
fakeKClientSet.Kubernetes.PrependReactor("list", "services", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &corev1.ServiceList{
Items: []corev1.Service{
testingutil.FakeKubeService("nodejs", "nodejs-app"),
},
}, nil
})
fakeKClientSet.Kubernetes.PrependReactor("get", "secrets", func(action ktesting.Action) (bool, runtime.Object, error) {
var secretName string
if tt.args.url.Spec.TLSSecret == "" {
secretName = getDefaultTLSSecretName(tt.args.url.Name, tt.fields.generic.componentName, tt.fields.generic.appName)
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.url.Spec.TLSSecret
if action.(ktesting.GetAction).GetName() != tt.args.url.Spec.TLSSecret {
return true, nil, fmt.Errorf("get for secrets called with invalid name, want: %s,got: %s", tt.args.url.Spec.TLSSecret, action.(ktesting.GetAction).GetName())
}
}
if tt.args.url.Spec.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
})
fakeKClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &appsv1.DeploymentList{Items: []appsv1.Deployment{*testingutil.CreateFakeDeployment("nodejs")}}, nil
})
got, err := k.createIngress(tt.args.url, urlLabels.GetLabels(tt.args.url.Name, k.componentName, k.appName, true))
if (err != nil) != tt.wantErr {
t.Errorf("createIngress() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
return
}
if got != tt.want {
t.Errorf("createIngress() got = %v, want %v", got, tt.want)
}
wantKubernetesActionLength := 0
if !tt.args.url.Spec.Secure {
wantKubernetesActionLength = 3
} else {
if tt.args.url.Spec.TLSSecret != "" && tt.userGivenTLSExists {
wantKubernetesActionLength = 4
} else if !tt.defaultTLSExists {
wantKubernetesActionLength = 5
} else {
wantKubernetesActionLength = 4
}
}
if len(fakeKClientSet.Kubernetes.Actions()) != wantKubernetesActionLength {
t.Errorf("expected %v Kubernetes.Actions() in Create, got: %v", wantKubernetesActionLength, len(fakeKClientSet.Kubernetes.Actions()))
}
if len(fakeKClientSet.RouteClientset.Actions()) != 0 {
t.Errorf("expected 0 RouteClientset.Actions() in CreateService, got: %v", fakeKClientSet.RouteClientset.Actions())
}
var createdIngress *networkingv1.Ingress
createIngressActionNo := 0
if !tt.args.url.Spec.Secure {
createIngressActionNo = 2
} else {
if tt.args.url.Spec.TLSSecret != "" {
createIngressActionNo = 3
} else if !tt.defaultTLSExists {
createdDefaultTLS := fakeKClientSet.Kubernetes.Actions()[3].(ktesting.CreateAction).GetObject().(*corev1.Secret)
if createdDefaultTLS.Name != getDefaultTLSSecretName(tt.args.url.Name, tt.fields.generic.componentName, tt.fields.generic.appName) {
t.Errorf("default tls created with different name, want: %s,got: %s", tt.fields.generic.componentName+"-tlssecret", createdDefaultTLS.Name)
}
createIngressActionNo = 4
} else {
createIngressActionNo = 3
}
}
createdIngress = fakeKClientSet.Kubernetes.Actions()[createIngressActionNo].(ktesting.CreateAction).GetObject().(*networkingv1.Ingress)
tt.createdIngress.NetworkingV1Ingress.Labels["odo.openshift.io/url-name"] = tt.args.url.Name
if !reflect.DeepEqual(createdIngress.Name, tt.createdIngress.GetName()) {
t.Errorf("ingress name not matching, expected: %s, got %s", tt.createdIngress.GetName(), createdIngress.Name)
}
if !reflect.DeepEqual(createdIngress.Labels, tt.createdIngress.NetworkingV1Ingress.Labels) {
t.Errorf("ingress labels not matching, %v", pretty.Compare(tt.createdIngress.NetworkingV1Ingress.Labels, createdIngress.Labels))
}
wantedIngressSpecParams := generator.IngressSpecParams{
ServiceName: serviceName,
IngressDomain: tt.args.url.Spec.Host,
PortNumber: intstr.FromInt(tt.args.url.Spec.Port),
TLSSecretName: tt.args.url.Spec.TLSSecret,
}
if tt.args.url.Spec.Secure {
if wantedIngressSpecParams.TLSSecretName == "" {
wantedIngressSpecParams.TLSSecretName = getDefaultTLSSecretName(tt.args.url.Name, tt.fields.generic.componentName, tt.fields.generic.appName)
}
if !reflect.DeepEqual(createdIngress.Spec.TLS[0].SecretName, wantedIngressSpecParams.TLSSecretName) {
t.Errorf("ingress tls name not matching, expected: %s, got %s", wantedIngressSpecParams.TLSSecretName, createdIngress.Spec.TLS)
}
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Create() = %#v, want %#v", got, tt.want)
}
})
}
}
func Test_kubernetesClient_createRoute(t *testing.T) {
type fields struct {
generic generic
isRouteSupported bool
}
type args struct {
url URL
}
tests := []struct {
name string
fields fields
args args
returnedRoute *routev1.Route
want string
wantErr bool
}{
{
name: "Case 1: Component name same as urlName",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed),
},
returnedRoute: &routev1.Route{
ObjectMeta: v1.ObjectMeta{
Name: "example-38d306b1",
Labels: map[string]string{
"app.kubernetes.io/part-of": "app",
"app.kubernetes.io/instance": "nodejs",
applabels.App: "app",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
"odo.openshift.io/url-name": "example",
},
},
Spec: routev1.RouteSpec{
To: routev1.RouteTargetReference{
Kind: "Service",
Name: "nodejs-app",
},
Port: &routev1.RoutePort{
TargetPort: intstr.FromInt(8080),
},
},
},
want: "http://host",
wantErr: false,
},
{
name: "Case 2: Component name different than urlName",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: getFakeURL("example-url", "com", 9100, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed),
},
returnedRoute: &routev1.Route{
ObjectMeta: v1.ObjectMeta{
Name: "example-url-556a0831",
Labels: map[string]string{
"app.kubernetes.io/part-of": "app",
"app.kubernetes.io/instance": "nodejs",
applabels.App: "app",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
"odo.openshift.io/url-name": "example-url",
},
},
Spec: routev1.RouteSpec{
To: routev1.RouteTargetReference{
Kind: "Service",
Name: "nodejs-app",
},
Port: &routev1.RoutePort{
TargetPort: intstr.FromInt(9100),
},
},
},
want: "http://host",
wantErr: false,
},
{
name: "Case 3: a secure URL",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: func() URL {
url := getFakeURL("example-url", "com", 9100, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed)
url.Spec.Secure = true
return url
}(),
},
returnedRoute: &routev1.Route{
ObjectMeta: v1.ObjectMeta{
Name: "example-url-556a0831",
Labels: map[string]string{
"app.kubernetes.io/part-of": "app",
"app.kubernetes.io/instance": "nodejs",
applabels.App: "app",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
"odo.openshift.io/url-name": "example-url",
},
},
Spec: routev1.RouteSpec{
TLS: &routev1.TLSConfig{
Termination: routev1.TLSTerminationEdge,
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
To: routev1.RouteTargetReference{
Kind: "Service",
Name: "nodejs-app",
},
Port: &routev1.RoutePort{
TargetPort: intstr.FromInt(9100),
},
},
},
want: "https://host",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeKClient, fakeKClientSet := kclient.FakeNew()
fakeKClientSet.RouteClientset.PrependReactor("create", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
route := action.(ktesting.CreateAction).GetObject().(*routev1.Route)
route.Spec.Host = "host"
return true, route, nil
})
fakeKClientSet.Kubernetes.PrependReactor("list", "services", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &corev1.ServiceList{
Items: []corev1.Service{
testingutil.FakeKubeService("nodejs", "nodejs-app"),
},
}, nil
})
fakeKClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &appsv1.DeploymentList{Items: []appsv1.Deployment{*testingutil.CreateFakeDeployment("nodejs")}}, nil
})
k := kubernetesClient{
generic: tt.fields.generic,
isRouteSupported: tt.fields.isRouteSupported,
client: fakeKClient,
}
got, err := k.createRoute(tt.args.url, urlLabels.GetLabels(tt.args.url.Name, k.componentName, k.appName, true))
if (err != nil) != tt.wantErr {
t.Errorf("createRoute() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("createRoute() got = %v, want %v", got, tt.want)
}
if len(fakeKClientSet.RouteClientset.Actions()) != 1 {
t.Errorf("expected 1 RouteClientset.Actions() in CreateService, got: %v", len(fakeKClientSet.RouteClientset.Actions()))
}
createdRoute := fakeKClientSet.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)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Create() = %#v, want %#v", got, tt.want)
}
})
}
}
func Test_kubernetesClient_Create(t *testing.T) {
type fields struct {
generic generic
isRouteSupported bool
}
type args struct {
url URL
}
tests := []struct {
name string
fields fields
args args
returnedIngress *extensionsv1.Ingress
want string
wantErr bool
}{
{
name: "Case 1: invalid url kind",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}},
args: args{
url: getFakeURL("nodejs", "com", 8080, "/", "http", "blah", StateTypeNotPushed),
},
wantErr: true,
},
{
name: "Case 2: route is not supported on the cluster",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}, isRouteSupported: false},
args: args{
url: getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed),
},
wantErr: true,
},
{
name: "Case 3: secretName used without secure flag",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}, isRouteSupported: false},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed)
url.Spec.TLSSecret = "secret"
return url
}(),
},
wantErr: true,
},
{
name: "Case 4: create a route",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}, isRouteSupported: true},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.ROUTE, StateTypeNotPushed)
return url
}(),
},
want: "http://host",
wantErr: false,
},
{
name: "Case 5: create a ingress",
fields: fields{generic: generic{componentName: "nodejs", appName: "app"}, isRouteSupported: true},
args: args{
url: func() URL {
url := getFakeURL("example", "com", 8080, "/", "http", localConfigProvider.INGRESS, StateTypeNotPushed)
return url
}(),
},
want: "http://example.com",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeKClient, fakeKClientSet := kclient.FakeNew()
fakeKClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, &appsv1.DeploymentList{Items: []appsv1.Deployment{*testingutil.CreateFakeDeployment("nodejs")}}, nil
})
fakeKClientSet.Kubernetes.PrependReactor("list", "services", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &corev1.ServiceList{
Items: []corev1.Service{
testingutil.FakeKubeService("nodejs", "nodejs-app"),
},
}, nil
})
fakeKClientSet.RouteClientset.PrependReactor("create", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
route := action.(ktesting.CreateAction).GetObject().(*routev1.Route)
route.Spec.Host = "host"
return true, route, nil
})
k := kubernetesClient{
generic: tt.fields.generic,
isRouteSupported: tt.fields.isRouteSupported,
client: fakeKClient,
}
got, err := k.Create(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
return
}
if got != tt.want {
t.Errorf("Create() got = %v, want %v", got, tt.want)
}
if tt.args.url.Spec.Kind == localConfigProvider.INGRESS {
requiredIngress := fake.GetSingleKubernetesIngress(tt.args.url.Name, tt.fields.generic.componentName, tt.fields.generic.appName, true, false)
createdIngress := fakeKClientSet.Kubernetes.Actions()[2].(ktesting.CreateAction).GetObject().(*extensionsv1.Ingress)
requiredIngress.NetworkingV1Ingress.Labels["odo.openshift.io/url-name"] = tt.args.url.Name
if !reflect.DeepEqual(createdIngress.Labels, requiredIngress.NetworkingV1Ingress.Labels) {
t.Errorf("ingress name not matching, expected: %s, got %s", requiredIngress.NetworkingV1Ingress.Labels, createdIngress.Labels)
}
} else if tt.args.url.Spec.Kind == localConfigProvider.ROUTE {
requiredRoute := testingutil.GetSingleRoute(tt.args.url.Name, tt.args.url.Spec.Port, tt.fields.generic.componentName, tt.fields.generic.appName)
requiredRoute.Labels["app"] = tt.fields.generic.appName
createdRoute := fakeKClientSet.RouteClientset.Actions()[0].(ktesting.CreateAction).GetObject().(*routev1.Route)
if !reflect.DeepEqual(createdRoute.Labels, requiredRoute.Labels) {
t.Errorf("route labels not matching, %v", pretty.Compare(requiredRoute.Labels, createdRoute.Labels))
}
}
})
}
}

View File

@@ -1,17 +0,0 @@
package labels
import (
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
)
// URLLabel is the label key that is applied to all url resources
// that are created
const URLLabel = "odo.openshift.io/url-name"
// GetLabels gets the labels to be applied to the given url besides the
// component labels and application labels.
func GetLabels(urlName string, componentName string, applicationName string, additional bool) map[string]string {
labels := componentlabels.GetLabels(componentName, applicationName, additional)
labels[URLLabel] = urlName
return labels
}

View File

@@ -1,75 +0,0 @@
package labels
import (
"reflect"
"testing"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
componentlabels "github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/version"
)
func TestGetLabels(t *testing.T) {
type args struct {
urlName string
componentName string
applicationName string
additional bool
}
tests := []struct {
name string
args args
want map[string]string
}{
{
name: "Case 1: Everything filled",
args: args{
urlName: "urlname",
componentName: "componentname",
applicationName: "applicationame",
additional: false,
},
want: map[string]string{
applabels.ApplicationLabel: "applicationame",
componentlabels.KubernetesInstanceLabel: "componentname",
URLLabel: "urlname",
},
}, {
name: "Case 2: No URL name",
args: args{
urlName: "",
componentName: "componentname",
applicationName: "applicationame",
additional: false,
},
want: map[string]string{
applabels.ApplicationLabel: "applicationame",
componentlabels.KubernetesInstanceLabel: "componentname",
URLLabel: "",
},
}, {
name: "Case 3: Everything with additional",
args: args{
urlName: "urlname",
componentName: "componentname",
applicationName: "applicationame",
additional: true,
},
want: map[string]string{
applabels.ApplicationLabel: "applicationame",
applabels.App: "applicationame",
applabels.ManagedBy: "odo",
applabels.ManagerVersion: version.VERSION,
componentlabels.KubernetesInstanceLabel: "componentname",
URLLabel: "urlname",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetLabels(tt.args.urlName, tt.args.componentName, tt.args.applicationName, tt.args.additional); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetLabels() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,94 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/url/url.go
// Package url is a generated GoMock package.
package url
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
localConfigProvider "github.com/redhat-developer/odo/pkg/localConfigProvider"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockClient) Create(url URL) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", url)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockClientMockRecorder) Create(url interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), url)
}
// Delete mocks base method.
func (m *MockClient) Delete(arg0 string, arg1 localConfigProvider.URLKind) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockClientMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), arg0, arg1)
}
// List mocks base method.
func (m *MockClient) List() (URLList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].(URLList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockClientMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List))
}
// ListFromCluster mocks base method.
func (m *MockClient) ListFromCluster() (URLList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListFromCluster")
ret0, _ := ret[0].(URLList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListFromCluster indicates an expected call of ListFromCluster.
func (mr *MockClientMockRecorder) ListFromCluster() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFromCluster", reflect.TypeOf((*MockClient)(nil).ListFromCluster))
}

View File

@@ -1,71 +0,0 @@
package url
import (
"github.com/redhat-developer/odo/pkg/localConfigProvider"
routev1 "github.com/openshift/api/route/v1"
"github.com/redhat-developer/odo/pkg/kclient"
)
type statusURL struct {
name string
url string
port int
secure bool
kind string
}
func getURLsForKubernetes(client kclient.ClientInterface, lcProvider localConfigProvider.LocalConfigProvider, ignoreUnpushed bool) ([]statusURL, error) {
var err error
componentName := lcProvider.GetName()
routesSupported := false
if routesSupported, err = client.IsRouteSupported(); err != nil {
// Fallback to Kubernetes client on error
routesSupported = false
}
urlClient := NewClient(ClientOptions{
LocalConfigProvider: lcProvider,
Client: client,
IsRouteSupported: routesSupported,
})
urls, err := urlClient.List()
if err != nil {
return nil, err
}
urlList := []statusURL{}
for _, u := range urls.Items {
// Ignore unpushed URLs, they necessarily are unreachable
if u.Status.State != StateTypePushed && ignoreUnpushed {
continue
}
var properURL, protocol string
if u.Spec.Kind != localConfigProvider.ROUTE {
protocol = GetProtocol(routev1.Route{}, ConvertExtensionV1IngressURLToIngress(u, componentName))
properURL = GetURLString(protocol, "", u.Spec.Host)
} else {
protocol = u.Spec.Protocol
properURL = GetURLString(protocol, u.Spec.Host, "")
}
statusURLVal := statusURL{
name: u.Name,
url: properURL,
kind: string(u.Spec.Kind),
port: u.Spec.Port,
secure: protocol == "https",
}
urlList = append(urlList, statusURLVal)
}
return urlList, nil
}

View File

@@ -1,200 +0,0 @@
package url
import (
"reflect"
"testing"
"github.com/redhat-developer/odo/pkg/kclient/unions"
"github.com/golang/mock/gomock"
routev1 "github.com/openshift/api/route/v1"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/testingutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kclient_fake "github.com/redhat-developer/odo/pkg/kclient/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery/fake"
ktesting "k8s.io/client-go/testing"
)
type fakeDiscovery struct {
*fake.FakeDiscovery
}
var fakeDiscoveryWithProject = &fakeDiscovery{
FakeDiscovery: &fake.FakeDiscovery{
Fake: &ktesting.Fake{
Resources: []*metav1.APIResourceList{
{
GroupVersion: "route.openshift.io/v1",
APIResources: []metav1.APIResource{{
Name: "routes",
SingularName: "route",
Namespaced: false,
Kind: "Route",
ShortNames: []string{"route"},
}},
},
},
},
},
}
func TestGetURLsForKubernetes(t *testing.T) {
componentName := "my-component"
testURL1 := localConfigProvider.LocalURL{Name: "example-1", Port: 9090, Host: "com", Kind: "ingress", Secure: true}
testURL2 := localConfigProvider.LocalURL{Name: "example-2", Port: 9090, Host: "com", Kind: "ingress", Secure: false}
testURL3 := localConfigProvider.LocalURL{Name: "routeurl2", Port: 8080, Kind: "route"}
testURL4 := localConfigProvider.LocalURL{Name: "example", Port: 8080, Kind: "route"}
tests := []struct {
name string
envURLs []localConfigProvider.LocalURL
routeList *routev1.RouteList
ingressList *unions.KubernetesIngressList
expectedStatusURL statusURL
}{
{
name: "1) Cluster with https URL defined in env info",
envURLs: []localConfigProvider.LocalURL{testURL1},
ingressList: &unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{},
},
expectedStatusURL: statusURL{
name: testURL1.Name,
kind: "ingress",
port: testURL1.Port,
secure: testURL1.Secure,
url: "https://example-1.com",
},
routeList: &routev1.RouteList{
Items: []routev1.Route{},
},
},
{
name: "2) Cluster with https URL defined in env info",
envURLs: []localConfigProvider.LocalURL{testURL2},
ingressList: &unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{},
},
expectedStatusURL: statusURL{
name: testURL2.Name,
kind: "ingress",
port: testURL2.Port,
secure: testURL2.Secure,
url: "http://example-2.com",
},
routeList: &routev1.RouteList{
Items: []routev1.Route{},
},
},
{
name: "3) Cluster with route defined in env info",
envURLs: []localConfigProvider.LocalURL{testURL3},
ingressList: &unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{},
},
expectedStatusURL: statusURL{
name: testURL3.Name,
kind: "route",
port: testURL3.Port,
secure: false,
url: "http://",
},
routeList: &routev1.RouteList{
Items: []routev1.Route{},
},
},
{
name: "4) Cluster with route defined",
envURLs: []localConfigProvider.LocalURL{},
ingressList: &unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{},
},
expectedStatusURL: statusURL{
name: testURL4.Name,
kind: "route",
port: testURL4.Port,
secure: false,
url: "http://example.com",
},
routeList: &routev1.RouteList{
Items: []routev1.Route{
testingutil.GetSingleRoute(testURL4.Name, testURL4.Port, componentName, "app"),
},
},
},
{
name: "5) Cluster with ingress defined",
envURLs: []localConfigProvider.LocalURL{},
ingressList: &unions.KubernetesIngressList{
Items: []*unions.KubernetesIngress{
kclient_fake.GetKubernetesIngressListWithMultiple(componentName, "app", true, false).Items[0],
},
},
routeList: &routev1.RouteList{
Items: []routev1.Route{},
},
expectedStatusURL: statusURL{
name: "example-0",
kind: "ingress",
port: 8080,
secure: false,
url: "http://example-0.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLocalConfig := localConfigProvider.NewMockLocalConfigProvider(ctrl)
mockLocalConfig.EXPECT().GetName().Return(componentName).AnyTimes()
mockLocalConfig.EXPECT().GetApplication().Return("app")
mockLocalConfig.EXPECT().Exists().Return(true)
mockLocalConfig.EXPECT().ListURLs().Return(tt.envURLs, nil)
// Initialising the fakeclient
fkclient, fkclientset := kclient.FakeNewWithIngressSupports(true, false)
fkclient.Namespace = "default"
fkclient.SetDiscoveryInterface(fakeDiscoveryWithProject)
// Return the test's ingress list when requested
fkclientset.Kubernetes.PrependReactor("list", "ingresses", func(action ktesting.Action) (bool, runtime.Object, error) {
if action.GetResource().GroupVersion().Group == "networking.k8s.io" {
return true, tt.ingressList.GetNetworkingV1IngressList(true), nil
}
return true, tt.ingressList.GetExtensionV1Beta1IngresList(true), nil
})
// Return the test's route list when requested
fkclientset.RouteClientset.PrependReactor("list", "routes", func(action ktesting.Action) (bool, runtime.Object, error) {
return true, tt.routeList, nil
})
statusUrls, err := getURLsForKubernetes(fkclient, mockLocalConfig, false)
if err != nil {
t.Fatalf("Error occurred: %v", err)
}
if len(statusUrls) == 0 {
t.Fatalf("statusURLs has unexpected size 0, must be 1")
}
if !reflect.DeepEqual(tt.expectedStatusURL, statusUrls[0]) {
t.Fatalf("Mismatching status URL - expected: %v, actual: %v", tt.expectedStatusURL, statusUrls[0])
}
})
}
}

View File

@@ -1,222 +0,0 @@
package url
import (
"fmt"
"reflect"
routev1 "github.com/openshift/api/route/v1"
"github.com/redhat-developer/odo/pkg/kclient/unions"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/machineoutput"
urlLabels "github.com/redhat-developer/odo/pkg/url/labels"
iextensionsv1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const URLKind = "URL"
// URL is an abstraction giving network access to the component from outside the cluster
type URL struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec URLSpec `json:"spec,omitempty"`
Status URLStatus `json:"status,omitempty"`
}
// URLSpec contains the specifications of a URL
type URLSpec struct {
Host string `json:"host,omitempty"`
Protocol string `json:"protocol,omitempty"`
Port int `json:"port,omitempty"`
Secure bool `json:"secure"`
Kind localConfigProvider.URLKind `json:"kind,omitempty"`
TLSSecret string `json:"tlssecret,omitempty"`
ExternalPort int `json:"externalport,omitempty"`
Path string `json:"path,omitempty"`
}
// URLList is a list of urls
type URLList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []URL `json:"items"`
}
// URLStatus is the current status of a url
type URLStatus struct {
// "Pushed" or "Not Pushed" or "Locally Delted"
State StateType `json:"state"`
}
type StateType string
const (
// StateTypePushed means that URL is present both locally and on cluster/container
StateTypePushed = "Pushed"
// StateTypeNotPushed means that URL is only in local config, but not on the cluster/container
StateTypeNotPushed = "Not Pushed"
// StateTypeLocallyDeleted means that URL was deleted from the local config, but it is still present on the cluster/container
StateTypeLocallyDeleted = "Locally Deleted"
)
// NewURL gives machine readable URL definition
func NewURL(r routev1.Route) URL {
return URL{
TypeMeta: metav1.TypeMeta{
Kind: URLKind,
APIVersion: machineoutput.APIVersion,
},
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,
Path: r.Spec.Path,
Kind: localConfigProvider.ROUTE,
},
}
}
func NewURLList(urls []URL) URLList {
return URLList{
TypeMeta: metav1.TypeMeta{
Kind: machineoutput.ListKind,
APIVersion: machineoutput.APIVersion,
},
ListMeta: metav1.ListMeta{},
Items: urls,
}
}
// NewURLFromEnvinfoURL creates a URL from a EnvinfoURL
func NewURLFromEnvinfoURL(envinfoURL localConfigProvider.LocalURL, serviceName string) URL {
hostString := fmt.Sprintf("%s.%s", envinfoURL.Name, envinfoURL.Host)
// default to route kind if none is provided
kind := envinfoURL.Kind
if kind == "" {
kind = localConfigProvider.ROUTE
}
url := URL{
TypeMeta: metav1.TypeMeta{
Kind: URLKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: envinfoURL.Name,
},
Spec: URLSpec{
Host: envinfoURL.Host,
Protocol: envinfoURL.Protocol,
Port: envinfoURL.Port,
Secure: envinfoURL.Secure,
Kind: kind,
TLSSecret: envinfoURL.TLSSecret,
Path: envinfoURL.Path,
},
}
if kind == localConfigProvider.INGRESS {
url.Spec.Host = hostString
if envinfoURL.Secure && len(envinfoURL.TLSSecret) > 0 {
url.Spec.TLSSecret = envinfoURL.TLSSecret
} else if envinfoURL.Secure {
url.Spec.TLSSecret = fmt.Sprintf("%s-tlssecret", serviceName)
}
}
return url
}
// NewURLFromLocalURL creates a URL from a localConfigProvider.LocalURL
func NewURLFromLocalURL(localURL localConfigProvider.LocalURL) URL {
return URL{
TypeMeta: metav1.TypeMeta{
Kind: URLKind,
APIVersion: machineoutput.APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: localURL.Name,
},
Spec: URLSpec{
Host: localURL.Host,
Protocol: localURL.Protocol,
Port: localURL.Port,
Secure: localURL.Secure,
Kind: localURL.Kind,
TLSSecret: localURL.TLSSecret,
Path: localURL.Path,
},
}
}
func NewURLsFromKubernetesIngressList(kil *unions.KubernetesIngressList) []URL {
var urlList []URL
for _, item := range kil.Items {
urlItem := NewURLFromKubernetesIngress(item, true)
if !reflect.DeepEqual(urlItem, URL{}) {
urlList = append(urlList, urlItem)
}
}
return urlList
}
func NewURLFromKubernetesIngress(ki *unions.KubernetesIngress, skipIfGenerated bool) URL {
if skipIfGenerated && ki.IsGenerated() {
return URL{}
}
u := URL{
TypeMeta: metav1.TypeMeta{
Kind: URLKind,
APIVersion: machineoutput.APIVersion,
},
}
if ki.NetworkingV1Ingress != nil {
u.ObjectMeta = metav1.ObjectMeta{Name: ki.NetworkingV1Ingress.Labels[urlLabels.URLLabel]}
u.Spec = URLSpec{
Host: ki.NetworkingV1Ingress.Spec.Rules[0].Host,
Port: int(ki.NetworkingV1Ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number),
Secure: ki.NetworkingV1Ingress.Spec.TLS != nil,
Path: ki.NetworkingV1Ingress.Spec.Rules[0].HTTP.Paths[0].Path,
Kind: localConfigProvider.INGRESS,
}
if len(ki.NetworkingV1Ingress.Spec.TLS) > 0 {
u.Spec.TLSSecret = ki.NetworkingV1Ingress.Spec.TLS[0].SecretName
}
if u.Spec.Secure {
u.Spec.Protocol = "https"
} else {
u.Spec.Protocol = "http"
}
} else if ki.ExtensionV1Beta1Ingress != nil {
u.ObjectMeta = metav1.ObjectMeta{Name: ki.ExtensionV1Beta1Ingress.Labels[urlLabels.URLLabel]}
u.Spec = URLSpec{
Host: ki.ExtensionV1Beta1Ingress.Spec.Rules[0].Host,
Port: int(ki.ExtensionV1Beta1Ingress.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort.IntVal),
Secure: ki.ExtensionV1Beta1Ingress.Spec.TLS != nil,
Path: ki.ExtensionV1Beta1Ingress.Spec.Rules[0].HTTP.Paths[0].Path,
Kind: localConfigProvider.INGRESS,
}
if len(ki.ExtensionV1Beta1Ingress.Spec.TLS) > 0 {
u.Spec.TLSSecret = ki.ExtensionV1Beta1Ingress.Spec.TLS[0].SecretName
}
if u.Spec.Secure {
u.Spec.Protocol = "https"
} else {
u.Spec.Protocol = "http"
}
}
return u
}
func (urls URLList) AreOutOfSync() bool {
outOfSync := false
for _, u := range urls.Items {
if u.Status.State != StateTypePushed {
outOfSync = true
break
}
}
return outOfSync
}

View File

@@ -1,156 +0,0 @@
package url
import (
"fmt"
"reflect"
applabels "github.com/redhat-developer/odo/pkg/application/labels"
"github.com/redhat-developer/odo/pkg/component/labels"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
v1 "k8s.io/api/apps/v1"
"k8s.io/klog"
)
const apiVersion = "odo.dev/v1alpha1"
// generic contains information required for all the URL clients
type generic struct {
appName string
componentName string
localConfigProvider localConfigProvider.LocalConfigProvider
}
type Client interface {
Create(url URL) (string, error)
Delete(string, localConfigProvider.URLKind) error
ListFromCluster() (URLList, error)
List() (URLList, error)
}
type ClientOptions struct {
Client kclient.ClientInterface
IsRouteSupported bool
LocalConfigProvider localConfigProvider.LocalConfigProvider
Deployment *v1.Deployment
}
// NewClient gets the appropriate URL client based on the parameters
func NewClient(options ClientOptions) Client {
var genericInfo generic
if options.LocalConfigProvider != nil {
genericInfo = generic{
appName: options.LocalConfigProvider.GetApplication(),
componentName: options.LocalConfigProvider.GetName(),
localConfigProvider: options.LocalConfigProvider,
}
}
if options.Deployment != nil {
genericInfo.appName = options.Deployment.Labels[applabels.ApplicationLabel]
genericInfo.componentName = options.Deployment.Labels[labels.KubernetesInstanceLabel]
}
return kubernetesClient{
generic: genericInfo,
isRouteSupported: options.IsRouteSupported,
client: options.Client,
}
}
type PushParameters struct {
LocalConfigProvider localConfigProvider.LocalConfigProvider
URLClient Client
IsRouteSupported bool
}
// Push creates and deletes the required URLs
func Push(parameters PushParameters) error {
urlLOCAL := make(map[string]URL)
localConfigProviderURLs, err := parameters.LocalConfigProvider.ListURLs()
if err != nil {
return err
}
// get the local URLs
for _, url := range localConfigProviderURLs {
if !parameters.IsRouteSupported && url.Kind == localConfigProvider.ROUTE {
continue
}
urlLOCAL[url.Name] = NewURLFromLocalURL(url)
}
urlCLUSTER := make(map[string]URL)
// get the URLs on the cluster
urlList, err := parameters.URLClient.ListFromCluster()
if err != nil {
return err
}
for _, url := range urlList.Items {
urlCLUSTER[url.Name] = url
}
// find URLs to delete
for urlName, urlSpec := range urlCLUSTER {
val, ok := urlLOCAL[urlName]
configMismatch := false
if ok {
// since the host stored in an ingress
// is the combination of name and host of the url
if val.Spec.Kind == localConfigProvider.INGRESS {
// in case of a secure ingress type URL with no user given tls secret
// the default secret name is used during creation
// thus setting it to the local URLs to avoid config mismatch
if val.Spec.Secure && val.Spec.TLSSecret == "" {
val.Spec.TLSSecret = getDefaultTLSSecretName(urlName, parameters.LocalConfigProvider.GetName(), parameters.LocalConfigProvider.GetApplication())
}
val.Spec.Host = fmt.Sprintf("%v.%v", urlName, val.Spec.Host)
} else if val.Spec.Kind == localConfigProvider.ROUTE {
// we don't allow the host input for route based URLs
// removing it for the urls from the cluster to avoid config mismatch
urlSpec.Spec.Host = ""
}
if val.Spec.Protocol == "" {
if val.Spec.Secure {
val.Spec.Protocol = "https"
} else {
val.Spec.Protocol = "http"
}
}
if !reflect.DeepEqual(val.Spec, urlSpec.Spec) {
configMismatch = true
klog.V(4).Infof("config and cluster mismatch for url %s", urlName)
}
}
if !ok || configMismatch {
// delete the url
err := parameters.URLClient.Delete(urlName, urlSpec.Spec.Kind)
if err != nil {
return err
}
delete(urlCLUSTER, urlName)
continue
}
}
// find URLs to create
for urlName, urlInfo := range urlLOCAL {
_, ok := urlCLUSTER[urlName]
if !ok {
_, err := parameters.URLClient.Create(urlInfo)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,678 +0,0 @@
package url
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/kclient/fake"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/testingutil"
kappsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
ktesting "k8s.io/client-go/testing"
)
func TestPush(t *testing.T) {
type deleteParameters struct {
string
localConfigProvider.URLKind
}
type args struct {
isRouteSupported bool
networkingV1IngressSupported bool
extensionV1IngressSupported bool
}
tests := []struct {
name string
args args
componentName string
applicationName string
existingLocalURLs []localConfigProvider.LocalURL
existingClusterURLs URLList
deletedItems []deleteParameters
createdURLs []URL
wantErr bool
}{
{
name: "no urls on local config and cluster",
args: args{
isRouteSupported: true,
networkingV1IngressSupported: false,
extensionV1IngressSupported: true,
},
componentName: "nodejs",
applicationName: "app",
},
{
name: "2 urls on local config and 0 on openshift cluster",
componentName: "nodejs",
applicationName: "app",
args: args{
isRouteSupported: true,
networkingV1IngressSupported: true,
extensionV1IngressSupported: false,
},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
},
{
Name: "example-1",
Port: 9090,
Secure: false,
Kind: localConfigProvider.ROUTE,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
}),
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-1",
Port: 9090,
Secure: false,
Kind: localConfigProvider.ROUTE,
}),
},
},
{
name: "0 url on local config and 2 on openshift cluster",
componentName: "wildfly",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: false, extensionV1IngressSupported: true},
existingClusterURLs: NewURLList([]URL{
NewURL(testingutil.GetSingleRoute("example", 8080, "wildfly", "app")),
NewURL(testingutil.GetSingleRoute("example-1", 9100, "wildfly", "app")),
}),
deletedItems: []deleteParameters{
{"example", localConfigProvider.ROUTE},
{"example-1", localConfigProvider.ROUTE},
},
},
{
name: "2 url on local config and 2 on openshift cluster, but they are different",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-local-0",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
},
{
Name: "example-local-1",
Port: 9090,
Secure: false,
Kind: localConfigProvider.ROUTE,
},
},
existingClusterURLs: NewURLList([]URL{
NewURL(testingutil.GetSingleRoute("example", 8080, "wildfly", "app")),
NewURL(testingutil.GetSingleRoute("example-1", 9100, "wildfly", "app")),
}),
deletedItems: []deleteParameters{
{"example", localConfigProvider.ROUTE},
{"example-1", localConfigProvider.ROUTE},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-0",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
}),
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-1",
Port: 9090,
Secure: false,
Kind: localConfigProvider.ROUTE,
}),
},
},
{
name: "5 urls (both types and different configurations) on config and openshift cluster are in sync",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Secure: false,
Path: "/",
Host: "com",
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-1",
Port: 9100,
Secure: false,
Path: "/",
Kind: localConfigProvider.ROUTE,
},
{
Name: "example-default-secret",
Port: 8080,
Secure: true,
Path: "/",
Host: "com",
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-user-secret",
Port: 8080,
Secure: true,
Path: "/",
Host: "com",
TLSSecret: "secret-name",
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-11",
Port: 9100,
Secure: true,
Path: "/",
Kind: localConfigProvider.ROUTE,
},
},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example", "nodejs", "app", true, false), false),
NewURLFromKubernetesIngress(fake.GetSingleSecureKubernetesIngress("example-default-secret", "nodejs", "app", "", true, false), false),
NewURLFromKubernetesIngress(fake.GetSingleSecureKubernetesIngress("example-user-secret", "nodejs", "app", "secret-name", true, false), false),
NewURL(testingutil.GetSingleRoute("example-1", 9100, "nodejs", "app")),
NewURL(testingutil.GetSingleSecureRoute("example-11", 9100, "nodejs", "app")),
}),
createdURLs: []URL{},
},
{
name: "0 urls on env file and cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{},
},
{
name: "2 urls on env file and 0 on openshift cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
}),
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
}),
},
},
{
name: "0 urls on env file and 2 on openshift cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-0", "nodejs", "app", true, false), false),
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-1", "nodejs", "app", true, false), false),
}),
deletedItems: []deleteParameters{
{"example-0", localConfigProvider.INGRESS},
{"example-1", localConfigProvider.INGRESS},
},
},
{
name: "2 urls on env file and 2 on openshift cluster, but they are different",
componentName: "wildfly",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-local-0",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
},
{
Name: "example-local-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
},
},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-0", "nodejs", "app", true, false), false),
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-1", "nodejs", "app", true, false), false),
}),
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-0",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
}),
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
}),
},
deletedItems: []deleteParameters{
{"example-0", localConfigProvider.INGRESS},
{"example-1", localConfigProvider.INGRESS},
},
},
{
name: "2 urls on env file and openshift cluster are in sync",
componentName: "wildfly",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-0",
Host: "com",
Port: 8080,
Secure: false,
Protocol: "http",
Kind: localConfigProvider.INGRESS,
Path: "/",
},
{
Name: "example-1",
Host: "com",
Port: 9090,
Secure: false,
Protocol: "http",
Kind: localConfigProvider.INGRESS,
Path: "/",
},
},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetKubernetesIngressListWithMultiple("wildfly", "app", true, false).Items[0], false),
NewURLFromKubernetesIngress(fake.GetKubernetesIngressListWithMultiple("wildfly", "app", true, false).Items[1], false),
}),
createdURLs: []URL{},
deletedItems: []deleteParameters{},
},
{
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",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-local-0",
Port: 8080,
Kind: localConfigProvider.ROUTE,
},
{
Name: "example-local-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
},
},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-0", "nodejs", "app", true, false), false),
NewURL(testingutil.GetSingleRoute("example-1", 9090, "nodejs", "app")),
}),
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-0",
Port: 8080,
Kind: localConfigProvider.ROUTE,
}),
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-1",
Host: "com",
Port: 9090,
Kind: localConfigProvider.INGRESS,
}),
},
deletedItems: []deleteParameters{
{"example-0", localConfigProvider.INGRESS},
{"example-1", localConfigProvider.ROUTE},
},
},
{
name: "create a ingress on a kubernetes cluster",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: false, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
TLSSecret: "secret",
Port: 8080,
Secure: true,
Kind: localConfigProvider.INGRESS,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
TLSSecret: "secret",
Port: 8080,
Secure: true,
Kind: localConfigProvider.INGRESS,
}),
},
},
{
name: "url with same name exists on env and cluster but with different specs",
componentName: "nodejs",
applicationName: "app",
args: args{
isRouteSupported: true,
networkingV1IngressSupported: true,
extensionV1IngressSupported: false,
},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-local-0",
Port: 8080,
Kind: localConfigProvider.ROUTE,
},
},
existingClusterURLs: NewURLList([]URL{
NewURLFromKubernetesIngress(fake.GetSingleKubernetesIngress("example-local-0", "nodejs", "app", true, false), false),
}),
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-0",
Port: 8080,
Kind: localConfigProvider.ROUTE,
}),
},
deletedItems: []deleteParameters{
{"example-local-0", localConfigProvider.INGRESS},
},
wantErr: false,
},
{
name: "url with same name exists on config and cluster but with different specs",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example-local-0",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
},
},
existingClusterURLs: NewURLList([]URL{
NewURL(testingutil.GetSingleRoute("example-local-0-app", 9090, "nodejs", "app")),
}),
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example-local-0",
Port: 8080,
Secure: false,
Kind: localConfigProvider.ROUTE,
}),
},
deletedItems: []deleteParameters{
{"example-local-0-app", localConfigProvider.ROUTE},
},
wantErr: false,
},
{
name: "create a secure route url",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Secure: true,
Kind: localConfigProvider.ROUTE,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Port: 8080,
Secure: true,
Kind: localConfigProvider.ROUTE,
}),
},
},
{
name: "create a secure ingress url with empty user given tls secret",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
Secure: true,
Port: 8080,
Kind: localConfigProvider.INGRESS,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
Secure: true,
Port: 8080,
Kind: localConfigProvider.INGRESS,
}),
},
},
{
name: "create a secure ingress url with user given tls secret",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
TLSSecret: "secret",
Port: 8080,
Secure: true,
Kind: localConfigProvider.INGRESS,
},
},
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
TLSSecret: "secret",
Port: 8080,
Secure: true,
Kind: localConfigProvider.INGRESS,
}),
},
},
{
name: "no host defined for ingress should not create any URL",
componentName: "nodejs",
args: args{isRouteSupported: false, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Kind: localConfigProvider.ROUTE,
},
},
wantErr: false,
createdURLs: []URL{},
},
{
name: "should create route in openshift cluster if endpoint is defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Kind: localConfigProvider.ROUTE,
Secure: false,
},
},
wantErr: false,
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Port: 8080,
Kind: localConfigProvider.ROUTE,
Secure: false,
}),
},
},
{
name: "should create ingress if endpoint is defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
},
},
wantErr: false,
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
Port: 8080,
Kind: localConfigProvider.INGRESS,
}),
},
},
{
name: "should create route in openshift cluster with path defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Port: 8080,
Secure: false,
Path: "/testpath",
Kind: localConfigProvider.ROUTE,
},
},
wantErr: false,
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Port: 8080,
Secure: false,
Path: "/testpath",
Kind: localConfigProvider.ROUTE,
}),
},
},
{
name: "should create ingress with path defined in devfile",
componentName: "nodejs",
applicationName: "app",
args: args{isRouteSupported: true, networkingV1IngressSupported: true, extensionV1IngressSupported: false},
existingLocalURLs: []localConfigProvider.LocalURL{
{
Name: "example",
Host: "com",
Port: 8080,
Secure: false,
Path: "/testpath",
Kind: localConfigProvider.INGRESS,
},
},
wantErr: false,
createdURLs: []URL{
NewURLFromLocalURL(localConfigProvider.LocalURL{
Name: "example",
Host: "com",
Port: 8080,
Secure: false,
Path: "/testpath",
Kind: localConfigProvider.INGRESS,
}),
},
},
}
for _, tt := range tests {
//tt.name = fmt.Sprintf("case %d: ", testNum+1) + tt.name
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLocalConfigProvider := localConfigProvider.NewMockLocalConfigProvider(ctrl)
mockLocalConfigProvider.EXPECT().GetName().Return(tt.componentName).AnyTimes()
mockLocalConfigProvider.EXPECT().GetApplication().Return(tt.applicationName).AnyTimes()
mockLocalConfigProvider.EXPECT().ListURLs().Return(tt.existingLocalURLs, nil)
mockURLClient := NewMockClient(ctrl)
mockURLClient.EXPECT().ListFromCluster().Return(tt.existingClusterURLs, nil)
for i := range tt.createdURLs {
mockURLClient.EXPECT().Create(tt.createdURLs[i]).Times(1)
}
for i := range tt.deletedItems {
mockURLClient.EXPECT().Delete(gomock.Eq(tt.deletedItems[i].string), gomock.Eq(tt.deletedItems[i].URLKind)).Times(1)
}
_, fakeKClientSet := kclient.FakeNewWithIngressSupports(tt.args.networkingV1IngressSupported, tt.args.extensionV1IngressSupported)
fakeKClientSet.Kubernetes.PrependReactor("list", "deployments", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &kappsv1.DeploymentList{
Items: []kappsv1.Deployment{
*testingutil.CreateFakeDeployment(tt.componentName),
},
}, nil
})
if err := Push(PushParameters{
LocalConfigProvider: mockLocalConfigProvider,
URLClient: mockURLClient,
IsRouteSupported: tt.args.isRouteSupported,
}); (err != nil) != tt.wantErr {
t.Errorf("Push() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -1,139 +0,0 @@
package url
import (
"fmt"
"reflect"
routev1 "github.com/openshift/api/route/v1"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
"github.com/redhat-developer/odo/pkg/util"
iextensionsv1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
type sortableURLs []URL
func (s sortableURLs) Len() int {
return len(s)
}
func (s sortableURLs) Less(i, j int) bool {
return s[i].Name <= s[j].Name
}
func (s sortableURLs) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// GetProtocol returns the protocol string
func GetProtocol(route routev1.Route, ingress iextensionsv1.Ingress) string {
if !reflect.DeepEqual(ingress, iextensionsv1.Ingress{}) && ingress.Spec.TLS != nil {
return "https"
} else if !reflect.DeepEqual(route, routev1.Route{}) && route.Spec.TLS != nil {
return "https"
}
return "http"
}
// ConvertEnvInfoURL converts EnvInfoURL to URL
func ConvertEnvInfoURL(envInfoURL localConfigProvider.LocalURL, serviceName string) URL {
hostString := fmt.Sprintf("%s.%s", envInfoURL.Name, envInfoURL.Host)
// default to route kind if none is provided
kind := envInfoURL.Kind
if kind == "" {
kind = localConfigProvider.ROUTE
}
url := URL{
TypeMeta: metav1.TypeMeta{
Kind: "url",
APIVersion: apiVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: envInfoURL.Name,
},
Spec: URLSpec{
Host: envInfoURL.Host,
Protocol: envInfoURL.Protocol,
Port: envInfoURL.Port,
Secure: envInfoURL.Secure,
Kind: kind,
TLSSecret: envInfoURL.TLSSecret,
Path: envInfoURL.Path,
},
}
if kind == localConfigProvider.INGRESS {
url.Spec.Host = hostString
if envInfoURL.Secure && len(envInfoURL.TLSSecret) > 0 {
url.Spec.TLSSecret = envInfoURL.TLSSecret
} else if envInfoURL.Secure {
url.Spec.TLSSecret = fmt.Sprintf("%s-tlssecret", serviceName)
}
}
return url
}
// GetURLString returns a string representation of given url
func GetURLString(protocol, URL, ingressDomain string) string {
if protocol == "" && URL == "" && ingressDomain == "" {
return ""
}
if URL == "" {
return protocol + "://" + ingressDomain
}
return protocol + "://" + URL
}
// getDefaultTLSSecretName returns the name of the default tls secret name
func getDefaultTLSSecretName(urlName string, componentName, appName string) string {
suffix := util.GetAdler32Value(urlName + appName + componentName)
return urlName + "-" + suffix + "-tls"
}
// ConvertExtensionV1IngressURLToIngress converts IngressURL to Ingress
func ConvertExtensionV1IngressURLToIngress(ingressURL URL, serviceName string) iextensionsv1.Ingress {
port := intstr.IntOrString{
Type: intstr.Int,
IntVal: int32(ingressURL.Spec.Port),
}
ingress := iextensionsv1.Ingress{
TypeMeta: metav1.TypeMeta{
Kind: "Ingress",
APIVersion: "extensions/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: ingressURL.Name,
},
Spec: iextensionsv1.IngressSpec{
Rules: []iextensionsv1.IngressRule{
{
Host: ingressURL.Spec.Host,
IngressRuleValue: iextensionsv1.IngressRuleValue{
HTTP: &iextensionsv1.HTTPIngressRuleValue{
Paths: []iextensionsv1.HTTPIngressPath{
{
Path: ingressURL.Spec.Path,
Backend: iextensionsv1.IngressBackend{
ServiceName: serviceName,
ServicePort: port,
},
},
},
},
},
},
},
},
}
if len(ingressURL.Spec.TLSSecret) > 0 {
ingress.Spec.TLS = []iextensionsv1.IngressTLS{
{
Hosts: []string{
ingressURL.Spec.Host,
},
SecretName: ingressURL.Spec.TLSSecret,
},
}
}
return ingress
}

View File

@@ -1,138 +0,0 @@
package url
import (
"fmt"
"reflect"
"testing"
"github.com/redhat-developer/odo/pkg/localConfigProvider"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestConvertEnvinfoURL(t *testing.T) {
serviceName := "testService"
urlName := "testURL"
host := "com"
secretName := "test-tls-secret"
tests := []struct {
name string
envInfoURL localConfigProvider.LocalURL
wantURL URL
}{
{
name: "Case 1: insecure URL",
envInfoURL: localConfigProvider.LocalURL{
Name: urlName,
Host: host,
Port: 8080,
Secure: false,
Kind: localConfigProvider.INGRESS,
},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
ObjectMeta: metav1.ObjectMeta{Name: urlName},
Spec: URLSpec{Host: fmt.Sprintf("%s.%s", urlName, host), Port: 8080, Secure: false, Kind: localConfigProvider.INGRESS},
},
},
{
name: "Case 2: secure Ingress URL without tls secret defined",
envInfoURL: localConfigProvider.LocalURL{
Name: urlName,
Host: host,
Port: 8080,
Secure: true,
Kind: localConfigProvider.INGRESS,
},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
ObjectMeta: metav1.ObjectMeta{Name: urlName},
Spec: URLSpec{Host: fmt.Sprintf("%s.%s", urlName, host), Port: 8080, Secure: true, TLSSecret: fmt.Sprintf("%s-tlssecret", serviceName), Kind: localConfigProvider.INGRESS},
},
},
{
name: "Case 3: secure Ingress URL with tls secret defined",
envInfoURL: localConfigProvider.LocalURL{
Name: urlName,
Host: host,
Port: 8080,
Secure: true,
TLSSecret: secretName,
Kind: localConfigProvider.INGRESS,
},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
ObjectMeta: metav1.ObjectMeta{Name: urlName},
Spec: URLSpec{Host: fmt.Sprintf("%s.%s", urlName, host), Port: 8080, Secure: true, TLSSecret: secretName, Kind: localConfigProvider.INGRESS},
},
},
{
name: "Case 4: Insecure route URL",
envInfoURL: localConfigProvider.LocalURL{
Name: urlName,
Port: 8080,
Kind: localConfigProvider.ROUTE,
},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
ObjectMeta: metav1.ObjectMeta{Name: urlName},
Spec: URLSpec{Port: 8080, Secure: false, Kind: localConfigProvider.ROUTE},
},
},
{
name: "Case 4: Secure route URL",
envInfoURL: localConfigProvider.LocalURL{
Name: urlName,
Port: 8080,
Secure: true,
Kind: localConfigProvider.ROUTE,
},
wantURL: URL{
TypeMeta: metav1.TypeMeta{Kind: "url", APIVersion: "odo.dev/v1alpha1"},
ObjectMeta: metav1.ObjectMeta{Name: urlName},
Spec: URLSpec{Port: 8080, Secure: true, Kind: localConfigProvider.ROUTE},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
url := ConvertEnvInfoURL(tt.envInfoURL, serviceName)
if !reflect.DeepEqual(url, tt.wantURL) {
t.Errorf("Expected %v, got %v", tt.wantURL, url)
}
})
}
}
func TestGetURLString(t *testing.T) {
cases := []struct {
name string
protocol string
URL string
ingressDomain string
expected string
}{
{
name: "all blank without s2i",
protocol: "",
URL: "",
ingressDomain: "",
expected: "",
},
{
name: "devfile case",
protocol: "http",
URL: "",
ingressDomain: "spring-8080.192.168.39.247.nip.io",
expected: "http://spring-8080.192.168.39.247.nip.io",
},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
output := GetURLString(testCase.protocol, testCase.URL, testCase.ingressDomain)
if output != testCase.expected {
t.Errorf("Expected: %v, got %v", testCase.expected, output)
}
})
}
}

View File

@@ -15,10 +15,6 @@ mockgen -source=pkg/storage/storage.go \
-package storage \
-destination pkg/storage/mock_Client.go
mockgen -source=pkg/url/url.go \
-package url \
-destination pkg/url/mock_Client.go
mockgen -source=pkg/devfile/image/image.go \
-package image \
-destination pkg/devfile/image/mock_Backend.go

View File

@@ -76,37 +76,6 @@ func GetPreferenceValue(key string) string {
return ""
}
// DetermineRouteURL takes context path as argument and returns the http URL
// where the current component exposes it's service this URL can
// then be used in order to interact with the deployed service running in Openshift
func DetermineRouteURL(context string) string {
urls := DetermineRouteURLs(context)
// only return the 1st element if it exists
if len(urls) > 0 {
return urls[0]
}
return ""
}
// DetermineRouteURLs takes context path as argument and returns the URLs
// where the current component exposes it's service, these URLs can
// then be used in order to interact with the deployed service running in Openshift
func DetermineRouteURLs(context string) []string {
var stdOut string
if context != "" {
stdOut = Cmd("odo", "url", "list", "--context", context).ShouldPass().Out()
} else {
stdOut = Cmd("odo", "url", "list").ShouldPass().Out()
}
reURL := regexp.MustCompile(`\s+http(s?)://.\S+`)
odoURLs := reURL.FindAllString(stdOut, -1)
for i := range odoURLs {
odoURLs[i] = strings.TrimSpace(odoURLs[i])
}
return odoURLs
}
// CreateRandProject create new project with random name (10 letters)
// without writing to the config file (without switching project)
func CreateRandProject() string {

View File

@@ -2,7 +2,6 @@ package devfile
import (
"fmt"
segment "github.com/redhat-developer/odo/pkg/segment/context"
"io"
"net/http"
"os"
@@ -10,6 +9,8 @@ import (
"sort"
"strings"
segment "github.com/redhat-developer/odo/pkg/segment/context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/pkg/util"
@@ -1085,4 +1086,49 @@ var _ = Describe("odo dev command tests", func() {
})
})
*/
//Test reused and adapted from the now-removed `cmd_devfile_delete_test.go`.
// cf. https://github.com/redhat-developer/odo/blob/24fd02673d25eb4c7bb166ec3369554a8e64b59c/tests/integration/devfile/cmd_devfile_delete_test.go#L172-L238
When("a component with endpoints is bootstrapped and pushed", func() {
BeforeEach(func() {
cmpName = "nodejs-with-endpoints"
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path",
helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass()
devSession, _, _, _, err := helper.StartDevMode()
Expect(err).ShouldNot(HaveOccurred())
devSession.Kill()
})
It("should not create Ingress or Route resources in the cluster", func() {
// Pod should exist
podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project)
Expect(podName).NotTo(BeEmpty())
services := commonVar.CliRunner.GetServices(commonVar.Project)
Expect(services).To(SatisfyAll(
Not(BeEmpty()),
ContainSubstring(fmt.Sprintf("%s-app", cmpName)),
))
ingressesOut := commonVar.CliRunner.Run("get", "ingress",
"-n", commonVar.Project,
"-o", "custom-columns=NAME:.metadata.name",
"--no-headers").Out.Contents()
ingresses, err := helper.ExtractLines(string(ingressesOut))
Expect(err).To(BeNil())
Expect(ingresses).To(BeEmpty())
if !helper.IsKubernetesCluster() {
routesOut := commonVar.CliRunner.Run("get", "routes",
"-n", commonVar.Project,
"-o", "custom-columns=NAME:.metadata.name",
"--no-headers").Out.Contents()
routes, err := helper.ExtractLines(string(routesOut))
Expect(err).To(BeNil())
Expect(routes).To(BeEmpty())
}
})
})
})