Dynamic registry support (#2940)

* Draft PR for dynamic registry support

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Handle migration

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Handle migration with experimental

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Improve error handling

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Add unit tests

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix unit tests

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix unit tests

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Add integration tests

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update "odo delete" and display registry name

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Add confirmation dialog for update and delete

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix catalog test

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update help page and delete functions

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Help page cleanup

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* update confirmation page

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Add URL validation

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix unit test

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Use built-in library for URL parsing

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update message

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update help page and use const default registry

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update registry URL

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update help page

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update registry tests

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Add github registry example

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update template

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix typo

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Update k8s packages

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>

* Fix registry test

Signed-off-by: jingfu wang <jingfu.j.wang@ibm.com>
This commit is contained in:
Jingfu Wang
2020-05-09 07:07:54 -04:00
committed by GitHub
parent bf6bf02bc4
commit bfa9922b1b
20 changed files with 1004 additions and 26 deletions

View File

@@ -130,7 +130,7 @@ jobs:
# scenario of docker devfile url testing needs only Kube config file. So the test has been
# added here just to make sure docker devfile url command test gets a proper kube config file.
# without creating a separate OpenShift cluster.
name: "devfile catalog, create, push, delete and docker devfile url command integration tests"
name: "devfile catalog, create, push, delete, registry and docker devfile url command integration tests"
script:
- ./scripts/oc-cluster.sh
- make bin
@@ -143,6 +143,7 @@ jobs:
- travis_wait make test-cmd-devfile-push
- travis_wait make test-cmd-devfile-watch
- travis_wait make test-cmd-devfile-delete
- travis_wait make test-cmd-devfile-registry
- odo logout
- <<: *base-test

View File

@@ -217,6 +217,11 @@ test-cmd-devfile-watch:
.PHONY: test-cmd-devfile-delete
test-cmd-devfile-delete:
ginkgo $(GINKGO_FLAGS) -focus="odo devfile delete command tests" tests/integration/devfile/
# Run odo devfile registry command tests
.PHONY: test-cmd-devfile-registry
test-cmd-devfile-registry:
ginkgo $(GINKGO_FLAGS) -focus="odo devfile registry command tests" tests/integration/devfile/
# Run odo storage command tests
.PHONY: test-cmd-storage

View File

@@ -7,7 +7,9 @@ import (
"strings"
imagev1 "github.com/openshift/api/image/v1"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/util"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
@@ -15,10 +17,32 @@ import (
"k8s.io/klog"
)
// DevfileRegistries contains the links of all devfile registries
var DevfileRegistries = []string{
"https://raw.githubusercontent.com/elsony/devfile-registry/master",
"https://che-devfile-registry.openshift.io/",
// GetDevfileRegistries gets devfile registries from preference file,
// if registry name is specified return the specific registry, otherwise return all registries
func GetDevfileRegistries(registryName string) (map[string]string, error) {
devfileRegistries := make(map[string]string)
cfg, err := preference.New()
if err != nil {
return nil, err
}
if cfg.OdoSettings.RegistryList != nil {
for _, registry := range *cfg.OdoSettings.RegistryList {
if len(registryName) != 0 {
if registryName == registry.Name {
devfileRegistries[registry.Name] = registry.URL
return devfileRegistries, nil
}
} else {
devfileRegistries[registry.Name] = registry.URL
}
}
} else {
return nil, nil
}
return devfileRegistries, nil
}
// GetDevfileIndex loads the devfile registry index.json
@@ -103,16 +127,26 @@ func IsDevfileComponentSupported(devfile Devfile) bool {
}
// ListDevfileComponents lists all the available devfile components
func ListDevfileComponents() (DevfileComponentTypeList, error) {
func ListDevfileComponents(registryName string) (DevfileComponentTypeList, error) {
var catalogDevfileList DevfileComponentTypeList
catalogDevfileList.DevfileRegistries = DevfileRegistries
var err error
for _, devfileRegistry := range DevfileRegistries {
// Get devfile registries
catalogDevfileList.DevfileRegistries, err = GetDevfileRegistries(registryName)
if err != nil {
return catalogDevfileList, err
}
if catalogDevfileList.DevfileRegistries == nil {
return catalogDevfileList, nil
}
for registryName, registryURL := range catalogDevfileList.DevfileRegistries {
// Load the devfile registry index.json
devfileIndexLink := devfileRegistry + "/devfiles/index.json"
devfileIndexLink := registryURL + "/devfiles/index.json"
devfileIndex, err := GetDevfileIndex(devfileIndexLink)
if err != nil {
return DevfileComponentTypeList{}, err
log.Warningf("Registry %s is not set up properly with error: %v", registryName, err)
break
}
// 1. Load devfiles that indexed in devfile registry index.json
@@ -122,14 +156,15 @@ func ListDevfileComponents() (DevfileComponentTypeList, error) {
devfileIndexEntryLink := devfileIndexEntry.Links.Link
// Load the devfile
devfileLink := devfileRegistry + devfileIndexEntryLink
// TODO: We send http get resquest in this function mutiple times
devfileLink := registryURL + devfileIndexEntryLink
// TODO: We send http get resquest in this function multiple times
// since devfile registry uses different links to host different devfiles,
// this can reduce the performance especially when we load devfiles from
// big registry. We may need to rethink and optimize this in the future
devfile, err := GetDevfile(devfileLink)
if err != nil {
return DevfileComponentTypeList{}, err
log.Warningf("Registry %s is not set up properly with error: %v", registryName, err)
break
}
// Populate devfile component with devfile data and form devfile component list
@@ -139,7 +174,10 @@ func ListDevfileComponents() (DevfileComponentTypeList, error) {
Description: devfileIndexEntry.Description,
Link: devfileIndexEntryLink,
Support: IsDevfileComponentSupported(devfile),
Registry: devfileRegistry,
Registry: Registry{
Name: registryName,
URL: registryURL,
},
}
catalogDevfileList.Items = append(catalogDevfileList.Items, catalogDevfile)

View File

@@ -1,13 +1,16 @@
package catalog
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"
imagev1 "github.com/openshift/api/image/v1"
"github.com/openshift/odo/pkg/occlient"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/testingutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -168,6 +171,67 @@ func TestSliceSupportedTags(t *testing.T) {
}
}
func TestGetDevfileRegistries(t *testing.T) {
tempConfigFile, err := ioutil.TempFile("", "odoconfig")
if err != nil {
t.Fatal("Fail to create temporary config file")
}
defer os.Remove(tempConfigFile.Name())
defer tempConfigFile.Close()
_, err = tempConfigFile.Write([]byte(
`kind: Preference
apiversion: odo.openshift.io/v1alpha1
OdoSettings:
Experimental: true
RegistryList:
- Name: CheDevfileRegistry
URL: https://che-devfile-registry.openshift.io/
- Name: DefaultDevfileRegistry
URL: https://raw.githubusercontent.com/elsony/devfile-registry/master`,
))
if err != nil {
t.Error(err)
}
os.Setenv(preference.GlobalConfigEnvName, tempConfigFile.Name())
defer os.Unsetenv(preference.GlobalConfigEnvName)
tests := []struct {
name string
registryName string
want map[string]string
}{
{
name: "Case 1: Test get all devfile registries",
registryName: "",
want: map[string]string{
"CheDevfileRegistry": "https://che-devfile-registry.openshift.io/",
"DefaultDevfileRegistry": "https://raw.githubusercontent.com/elsony/devfile-registry/master",
},
},
{
name: "Case 2: Test get specific devfile registry",
registryName: "CheDevfileRegistry",
want: map[string]string{
"CheDevfileRegistry": "https://che-devfile-registry.openshift.io/",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetDevfileRegistries(tt.registryName)
if err != nil {
t.Errorf("Error message is %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Got: %v, want: %v", got, tt.want)
}
})
}
}
func TestGetDevfileIndex(t *testing.T) {
// Start a local HTTP server
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {

View File

@@ -12,6 +12,12 @@ type ComponentType struct {
Spec ComponentSpec `json:"spec,omitempty"`
}
// Registry is the main struct of devfile registry
type Registry struct {
Name string
URL string
}
// DevfileComponentType is the main struct for devfile catalog components
type DevfileComponentType struct {
Name string
@@ -19,7 +25,7 @@ type DevfileComponentType struct {
Description string
Link string
Support bool
Registry string
Registry Registry
}
// DevfileIndexEntry is the main struct of index.json from devfile registry
@@ -66,7 +72,7 @@ type ComponentTypeList struct {
// DevfileComponentTypeList lists all the DevfileComponentType's
type DevfileComponentTypeList struct {
DevfileRegistries []string
DevfileRegistries map[string]string
Items []DevfileComponentType
}

View File

@@ -57,10 +57,14 @@ func (o *ListComponentsOptions) Complete(name string, cmd *cobra.Command, args [
}
if experimental.IsExperimentalModeEnabled() {
o.catalogDevfileList, err = catalog.ListDevfileComponents()
o.catalogDevfileList, err = catalog.ListDevfileComponents("")
if err != nil {
return err
}
if o.catalogDevfileList.DevfileRegistries == nil {
log.Warning("Please run 'odo registry add <registry name> <registry URL>' to add registry for listing devfile components\n")
}
}
return
@@ -130,7 +134,7 @@ func (o *ListComponentsOptions) Run() (err error) {
if len(supDevfileCatalogList) != 0 || (o.listAllDevfileComponents && len(unsupDevfileCatalogList) != 0) {
fmt.Fprintln(w, "Odo Devfile Components:")
fmt.Fprintln(w, "NAME", "\t", "DESCRIPTION", "\t", "SUPPORTED")
fmt.Fprintln(w, "NAME", "\t", "DESCRIPTION", "\t", "REGISTRY", "\t", "SUPPORTED")
if len(supDevfileCatalogList) != 0 {
supported = "YES"
@@ -192,6 +196,6 @@ func (o *ListComponentsOptions) printCatalogList(w io.Writer, catalogList []cata
func (o *ListComponentsOptions) printDevfileCatalogList(w io.Writer, catalogDevfileList []catalog.DevfileComponentType, supported string) {
for _, devfileComponent := range catalogDevfileList {
fmt.Fprintln(w, devfileComponent.Name, "\t", devfileComponent.Description, "\t", supported)
fmt.Fprintln(w, devfileComponent.Name, "\t", devfileComponent.Description, "\t", devfileComponent.Registry.Name, "\t", supported)
}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/openshift/odo/pkg/odo/cli/plugins"
"github.com/openshift/odo/pkg/odo/cli/preference"
"github.com/openshift/odo/pkg/odo/cli/project"
"github.com/openshift/odo/pkg/odo/cli/registry"
"github.com/openshift/odo/pkg/odo/cli/service"
"github.com/openshift/odo/pkg/odo/cli/storage"
"github.com/openshift/odo/pkg/odo/cli/url"
@@ -23,6 +24,7 @@ import (
"github.com/openshift/odo/pkg/odo/cli/version"
"github.com/openshift/odo/pkg/odo/util"
odoutil "github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/odo/util/experimental"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -197,6 +199,12 @@ func odoRootCmd(name, fullName string) *cobra.Command {
debug.NewCmdDebug(debug.RecommendedCommandName, util.GetFullName(fullName, debug.RecommendedCommandName)),
)
if experimental.IsExperimentalModeEnabled() {
rootCmd.AddCommand(
registry.NewCmdRegistry(registry.RecommendedCommandName, util.GetFullName(fullName, registry.RecommendedCommandName)),
)
}
odoutil.VisitCommands(rootCmd, reconfigureCmdWithSubcmd)
return rootCmd

View File

@@ -63,7 +63,7 @@ type DevfileMetadata struct {
componentNamespace string
devfileSupport bool
devfileLink string
devfileRegistry string
devfileRegistry catalog.Registry
downloadSource bool
}
@@ -329,10 +329,13 @@ func (co *CreateOptions) Complete(name string, cmd *cobra.Command, args []string
co.CommonPushOptions.componentContext = co.componentContext
}
catalogDevfileList, err := catalog.ListDevfileComponents()
catalogDevfileList, err := catalog.ListDevfileComponents(co.devfileMetadata.devfileRegistry.Name)
if err != nil {
return err
}
if catalogDevfileList.DevfileRegistries == nil {
log.Warning("Please run `odo registry add <registry name> <registry URL>` to add a registry then create a devfile components\n")
}
var componentType string
var componentName string
@@ -437,6 +440,8 @@ func (co *CreateOptions) Complete(name string, cmd *cobra.Command, args []string
}
}
registrySpinner := log.Spinnerf("Creating a devfile component from registry: %s", co.devfileMetadata.devfileRegistry.Name)
if co.devfileMetadata.devfileSupport {
err = co.InitEnvInfoFromContext()
if err != nil {
@@ -444,11 +449,13 @@ func (co *CreateOptions) Complete(name string, cmd *cobra.Command, args []string
}
spinner.End(true)
registrySpinner.End(true)
return nil
}
spinner.End(false)
log.Italic("\nPlease run 'odo catalog list components' for a list of supported devfile component types")
registrySpinner.End(false)
log.Italic("\nPlease run `odo catalog list components` for a list of supported devfile component types")
}
if len(args) == 0 || !cmd.HasFlags() {
@@ -773,7 +780,7 @@ func (co *CreateOptions) Run() (err error) {
// Download devfile.yaml file and create env.yaml file
if co.devfileMetadata.devfileSupport {
if !util.CheckPathExists(DevfilePath) {
err := util.DownloadFile(co.devfileMetadata.devfileRegistry+co.devfileMetadata.devfileLink, DevfilePath)
err := util.DownloadFile(co.devfileMetadata.devfileRegistry.URL+co.devfileMetadata.devfileLink, DevfilePath)
if err != nil {
return errors.Wrap(err, "Faile to download devfile.yaml for devfile component")
}
@@ -899,6 +906,7 @@ func NewCmdCreate(name, fullName string) *cobra.Command {
componentCreateCmd.Flags().StringSliceVar(&co.componentEnvVars, "env", []string{}, "Environmental variables for the component. For example --env VariableName=Value")
if experimental.IsExperimentalModeEnabled() {
componentCreateCmd.Flags().StringVar(&co.devfileMetadata.devfileRegistry.Name, "registry", "", "Create devfile component from specific registry")
componentCreateCmd.Flags().BoolVar(&co.devfileMetadata.downloadSource, "downloadSource", false, "Download sample project from devfile. (ex. odo component create <component_type> [component_name] --downloadSource")
}

View File

@@ -0,0 +1,95 @@
package registry
import (
// Built-in packages
"fmt"
// Third-party packages
"github.com/pkg/errors"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
// odo packages
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/util"
)
const addCommandName = "add"
// "odo registry add" command description and examples
var (
addLongDesc = ktemplates.LongDesc(`Add devfile registry`)
addExample = ktemplates.Examples(`# Add devfile registry
%[1]s CheRegistry https://che-devfile-registry.openshift.io
%[1]s CheRegistryFromGitHub https://raw.githubusercontent.com/eclipse/che-devfile-registry/master
`)
)
// AddOptions encapsulates the options for the "odo registry add" command
type AddOptions struct {
operation string
registryName string
registryURL string
forceFlag bool
}
// NewAddOptions creates a new AddOptions instance
func NewAddOptions() *AddOptions {
return &AddOptions{}
}
// Complete completes AddOptions after they've been created
func (o *AddOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.operation = "add"
o.registryName = args[0]
o.registryURL = args[1]
return
}
// Validate validates the AddOptions based on completed values
func (o *AddOptions) Validate() (err error) {
err = util.ValidateURL(o.registryURL)
if err != nil {
return err
}
return
}
// Run contains the logic for "odo registry add" command
func (o *AddOptions) Run() (err error) {
cfg, err := preference.New()
if err != nil {
return errors.Wrap(err, "unable to add registry")
}
err = cfg.RegistryHandler(o.operation, o.registryName, o.registryURL, o.forceFlag)
if err != nil {
return err
}
log.Info("New registry successfully added")
return nil
}
// NewCmdAdd implements the "odo registry add" command
func NewCmdAdd(name, fullName string) *cobra.Command {
o := NewAddOptions()
registryAddCmd := &cobra.Command{
Use: fmt.Sprintf("%s <registry name> <registry URL>", name),
Short: addLongDesc,
Long: addLongDesc,
Example: fmt.Sprintf(fmt.Sprint(addExample), fullName),
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
return registryAddCmd
}

View File

@@ -0,0 +1,86 @@
package registry
import (
// Built-in packages
"fmt"
// Third-party packages
"github.com/pkg/errors"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
// odo packages
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/preference"
)
const deleteCommandName = "delete"
// "odo registry delete" command description and examples
var (
deleteLongDesc = ktemplates.LongDesc(`Delete devfile registry`)
deleteExample = ktemplates.Examples(`# Delete devfile registry
%[1]s CheRegistry
`)
)
// DeleteOptions encapsulates the options for the "odo registry delete" command
type DeleteOptions struct {
operation string
registryName string
registryURL string
forceFlag bool
}
// NewDeleteOptions creates a new DeleteOptions instance
func NewDeleteOptions() *DeleteOptions {
return &DeleteOptions{}
}
// Complete completes DeleteOptions after they've been created
func (o *DeleteOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.operation = "delete"
o.registryName = args[0]
o.registryURL = ""
return
}
// Validate validates the DeleteOptions based on completed values
func (o *DeleteOptions) Validate() (err error) {
return
}
// Run contains the logic for "odo registry delete" command
func (o *DeleteOptions) Run() (err error) {
cfg, err := preference.New()
if err != nil {
return errors.Wrap(err, "unable to delete registry")
}
err = cfg.RegistryHandler(o.operation, o.registryName, o.registryURL, o.forceFlag)
if err != nil {
return err
}
return nil
}
// NewCmdDelete implements the "odo registry delete" command
func NewCmdDelete(name, fullName string) *cobra.Command {
o := NewDeleteOptions()
registryDeleteCmd := &cobra.Command{
Use: fmt.Sprintf("%s <registry name>", name),
Short: deleteLongDesc,
Long: deleteLongDesc,
Example: fmt.Sprintf(fmt.Sprint(deleteExample), fullName),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
registryDeleteCmd.Flags().BoolVarP(&o.forceFlag, "force", "f", false, "Don't ask for confirmation, delete the registry directly")
return registryDeleteCmd
}

View File

@@ -0,0 +1,88 @@
package registry
import (
// Built-in packages
"fmt"
"io"
"os"
"text/tabwriter"
// Third-party packages
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
// odo packages
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/odo/util"
"github.com/openshift/odo/pkg/preference"
)
const listCommandName = "list"
// "odo registry list" command description and examples
var (
listDesc = ktemplates.LongDesc(`List devfile registry`)
listExample = ktemplates.Examples(`# List devfile registry
%[1]s
`)
)
// ListOptions encapsulates the options for "odo registry list" command
type ListOptions struct {
}
// NewListOptions creates a new ListOptions instance
func NewListOptions() *ListOptions {
return &ListOptions{}
}
// Complete completes ListOptions after they've been created
func (o *ListOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
return
}
// Validate validates the ListOptions based on completed values
func (o *ListOptions) Validate() (err error) {
return
}
// Run contains the logic for "odo registry list" command
func (o *ListOptions) Run() (err error) {
cfg, err := preference.New()
if err != nil {
util.LogErrorAndExit(err, "")
}
w := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent)
fmt.Fprintln(w, "NAME", "\t", "URL")
o.printRegistryList(w, cfg.OdoSettings.RegistryList)
w.Flush()
return
}
func (o *ListOptions) printRegistryList(w io.Writer, registryList *[]preference.Registry) {
if registryList == nil {
return
}
for _, registry := range *registryList {
fmt.Fprintln(w, registry.Name, "\t", registry.URL)
}
}
// NewCmdList implements the "odo registry list" command
func NewCmdList(name, fullName string) *cobra.Command {
o := NewListOptions()
registryListCmd := &cobra.Command{
Use: name,
Short: listDesc,
Long: listDesc,
Example: fmt.Sprintf(fmt.Sprint(listExample), fullName),
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
return registryListCmd
}

View File

@@ -0,0 +1,44 @@
package registry
import (
// Built-in packages
"fmt"
// Third-party packages
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
// odo packages
"github.com/openshift/odo/pkg/odo/util"
)
// RecommendedCommandName is the recommended registry command name
const RecommendedCommandName = "registry"
var registryDesc = ktemplates.LongDesc(`Configure devfile registry`)
// NewCmdRegistry implements the registry configuration command
func NewCmdRegistry(name, fullName string) *cobra.Command {
registryAddCmd := NewCmdAdd(addCommandName, util.GetFullName(fullName, addCommandName))
registryListCmd := NewCmdList(listCommandName, util.GetFullName(fullName, listCommandName))
registryUpdateCmd := NewCmdUpdate(updateCommandName, util.GetFullName(fullName, updateCommandName))
registryDeleteCmd := NewCmdDelete(deleteCommandName, util.GetFullName(fullName, deleteCommandName))
registryCmd := &cobra.Command{
Use: name,
Short: registryDesc,
Long: registryDesc,
Example: fmt.Sprintf("%s\n\n%s\n\n%s\n\n%s",
registryAddCmd.Example,
registryListCmd.Example,
registryUpdateCmd.Example,
registryDeleteCmd.Example,
),
}
registryCmd.AddCommand(registryAddCmd, registryListCmd, registryUpdateCmd, registryDeleteCmd)
registryCmd.SetUsageTemplate(util.CmdUsageTemplate)
registryCmd.Annotations = map[string]string{"command": "main"}
return registryCmd
}

View File

@@ -0,0 +1,92 @@
package registry
import (
// Built-in packages
"fmt"
// Third-party packages
"github.com/pkg/errors"
"github.com/spf13/cobra"
ktemplates "k8s.io/kubectl/pkg/util/templates"
// odo packages
"github.com/openshift/odo/pkg/odo/genericclioptions"
"github.com/openshift/odo/pkg/preference"
"github.com/openshift/odo/pkg/util"
)
const updateCommandName = "update"
// "odo registry update" command description and examples
var (
updateLongDesc = ktemplates.LongDesc(`Update devfile registry URL`)
updateExample = ktemplates.Examples(`# Update devfile registry URL
%[1]s CheRegistry https://che-devfile-registry-update.openshift.io
`)
)
// UpdateOptions encapsulates the options for the "odo registry update" command
type UpdateOptions struct {
operation string
registryName string
registryURL string
forceFlag bool
}
// NewUpdateOptions creates a new UpdateOptions instance
func NewUpdateOptions() *UpdateOptions {
return &UpdateOptions{}
}
// Complete completes UpdateOptions after they've been created
func (o *UpdateOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) {
o.operation = "update"
o.registryName = args[0]
o.registryURL = args[1]
return
}
// Validate validates the UpdateOptions based on completed values
func (o *UpdateOptions) Validate() (err error) {
err = util.ValidateURL(o.registryURL)
if err != nil {
return err
}
return
}
// Run contains the logic for "odo registry update" command
func (o *UpdateOptions) Run() (err error) {
cfg, err := preference.New()
if err != nil {
return errors.Wrap(err, "unable to update registry")
}
err = cfg.RegistryHandler(o.operation, o.registryName, o.registryURL, o.forceFlag)
if err != nil {
return err
}
return nil
}
// NewCmdUpdate implements the "odo registry update" command
func NewCmdUpdate(name, fullName string) *cobra.Command {
o := NewUpdateOptions()
registryUpdateCmd := &cobra.Command{
Use: fmt.Sprintf("%s <registry name> <registry URL>", name),
Short: updateLongDesc,
Long: updateLongDesc,
Example: fmt.Sprintf(fmt.Sprint(updateExample), fullName),
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
genericclioptions.GenericRun(o, cmd, args)
},
}
registryUpdateCmd.Flags().BoolVarP(&o.forceFlag, "force", "f", false, "Don't ask for confirmation, update the registry directly")
return registryUpdateCmd
}

View File

@@ -12,6 +12,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
"github.com/openshift/odo/pkg/log"
"github.com/openshift/odo/pkg/odo/cli/ui"
"github.com/openshift/odo/pkg/util"
)
@@ -64,6 +66,18 @@ const (
// KubePushTarget represents the value of the push target when it's set to Kube
KubePushTarget = "kube"
// CheDevfileRegistryName is the name of Che devfile registry
CheDevfileRegistryName = "CheDevfileRegistry"
// CheDevfileRegistryURL is the URL of Che devfile registry
CheDevfileRegistryURL = "https://che-devfile-registry.openshift.io"
// DefaultDevfileRegistryName is the name of default devfile registry
DefaultDevfileRegistryName = "DefaultDevfileRegistry"
// DefaultDevfileRegistryURL is the URL of default devfile registry
DefaultDevfileRegistryURL = "https://raw.githubusercontent.com/elsony/devfile-registry/master"
)
// TimeoutSettingDescription is human-readable description for the timeout setting
@@ -117,6 +131,15 @@ type OdoSettings struct {
// PushTarget for telling odo which platform to push to (either kube or docker)
PushTarget *string `yaml:"PushTarget,omitempty"`
// RegistryList for telling odo to connect to all the registries in the registry list
RegistryList *[]Registry `yaml:"RegistryList,omitempty"`
}
// Registry includes the registry metadata
type Registry struct {
Name string `yaml:"Name,omitempty"`
URL string `yaml:"URL,omitempty"`
}
// Preference stores all the preferences related to odo
@@ -171,7 +194,8 @@ func NewPreferenceInfo() (*PreferenceInfo, error) {
Preference: NewPreference(),
Filename: preferenceFile,
}
// if the preference file doesn't exist then we dont worry about it and return
// If the preference file doesn't exist then we return with default preference
if _, err = os.Stat(preferenceFile); os.IsNotExist(err) {
return &c, nil
}
@@ -180,9 +204,124 @@ func NewPreferenceInfo() (*PreferenceInfo, error) {
if err != nil {
return nil, err
}
if c.OdoSettings.Experimental != nil && *c.OdoSettings.Experimental {
if c.OdoSettings.RegistryList == nil {
// Handle user has preference file but doesn't use dynamic registry before
defaultRegistryList := []Registry{
{
Name: CheDevfileRegistryName,
URL: CheDevfileRegistryURL,
},
{
Name: DefaultDevfileRegistryName,
URL: DefaultDevfileRegistryURL,
},
}
c.OdoSettings.RegistryList = &defaultRegistryList
}
}
return &c, nil
}
// RegistryHandler handles registry add, update and delete operations
func (c *PreferenceInfo) RegistryHandler(operation string, registryName string, registryURL string, forceFlag bool) error {
var registryList []Registry
var err error
registryExist := false
// Registry list is empty
if c.OdoSettings.RegistryList == nil {
registryList, err = handleWithoutRegistryExist(registryList, operation, registryName, registryURL)
if err != nil {
return err
}
} else {
// The target registry exists in the registry list
registryList = *c.OdoSettings.RegistryList
for index, registry := range registryList {
if registry.Name == registryName {
registryExist = true
registryList, err = handleWithRegistryExist(index, registryList, operation, registryName, registryURL, forceFlag)
if err != nil {
return err
}
}
}
// The target registry doesn't exist in the registry list
if !registryExist {
registryList, err = handleWithoutRegistryExist(registryList, operation, registryName, registryURL)
if err != nil {
return err
}
}
}
c.OdoSettings.RegistryList = &registryList
err = util.WriteToFile(&c.Preference, c.Filename)
if err != nil {
return errors.Wrapf(err, "unable to write the configuration of %s operation to preference file", operation)
}
return nil
}
func handleWithoutRegistryExist(registryList []Registry, operation string, registryName string, registryURL string) ([]Registry, error) {
switch operation {
case "add":
registry := Registry{
Name: registryName,
URL: registryURL,
}
registryList = append(registryList, registry)
case "update":
return nil, errors.Errorf("failed to update registry: registry %s doesn't exist", registryName)
case "delete":
return nil, errors.Errorf("failed to delete registry: registry %s doesn't exist", registryName)
}
return registryList, nil
}
func handleWithRegistryExist(index int, registryList []Registry, operation string, registryName string, registryURL string, forceFlag bool) ([]Registry, error) {
switch operation {
case "add":
return nil, errors.Errorf("failed to add registry: registry %s already exists", registryName)
case "update":
if !forceFlag {
if !ui.Proceed(fmt.Sprintf("Are you sure you want to update registry %s", registryName)) {
log.Info("Aborted by the user")
return registryList, nil
}
}
registryList[index].URL = registryURL
log.Info("Successfully updated registry")
case "delete":
if !forceFlag {
if !ui.Proceed(fmt.Sprintf("Are you sure you want to delete registry %s", registryName)) {
log.Info("Aborted by the user")
return registryList, nil
}
}
copy(registryList[index:], registryList[index+1:])
registryList[len(registryList)-1] = Registry{}
registryList = registryList[:len(registryList)-1]
log.Info("Successfully deleted registry")
}
return registryList, nil
}
// SetConfiguration modifies Odo configurations in the config file
// as of now being used for nameprefix, timeout, updatenotification
// TODO: Use reflect to set parameters

View File

@@ -703,3 +703,132 @@ func TestMetaTypePopulatedInPreference(t *testing.T) {
t.Error("the api version and kind in preference are incorrect")
}
}
func TestHandleWithoutRegistryExist(t *testing.T) {
tests := []struct {
name string
registryList []Registry
operation string
registryName string
registryURL string
want []Registry
}{
{
name: "Case 1: Add registry",
registryList: []Registry{},
operation: "add",
registryName: "testName",
registryURL: "testURL",
want: []Registry{
{
Name: "testName",
URL: "testURL",
},
},
},
{
name: "Case 2: Update registry",
registryList: []Registry{},
operation: "update",
registryName: "testName",
registryURL: "testURL",
want: nil,
},
{
name: "Case 3: Delete registry",
registryList: []Registry{},
operation: "delete",
registryName: "testName",
registryURL: "testURL",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := handleWithoutRegistryExist(tt.registryList, tt.operation, tt.registryName, tt.registryURL)
if err != nil {
t.Logf("Error message is %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Got: %v, want %v", got, tt.want)
}
})
}
}
func TestHandleWithRegistryExist(t *testing.T) {
tests := []struct {
name string
index int
registryList []Registry
operation string
registryName string
registryURL string
forceFlag bool
want []Registry
}{
{
name: "Case 1: Add registry",
index: 0,
registryList: []Registry{
{
Name: "testName",
URL: "testURL",
},
},
operation: "add",
registryName: "testName",
registryURL: "addURL",
forceFlag: false,
want: nil,
},
{
name: "Case 2: update registry",
index: 0,
registryList: []Registry{
{
Name: "testName",
URL: "testURL",
},
},
operation: "update",
registryName: "testName",
registryURL: "updateURL",
forceFlag: true,
want: []Registry{
{
Name: "testName",
URL: "updateURL",
},
},
},
{
name: "Case 3: Delete registry",
index: 0,
registryList: []Registry{
{
Name: "testName",
URL: "testURL",
},
},
operation: "delete",
registryName: "testName",
registryURL: "",
forceFlag: true,
want: []Registry{},
},
}
for _, tt := range tests {
got, err := handleWithRegistryExist(tt.index, tt.registryList, tt.operation, tt.registryName, tt.registryURL, tt.forceFlag)
if err != nil {
t.Logf("Error message is %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Got: %v, want: %v", got, tt.want)
}
}
}

View File

@@ -982,3 +982,17 @@ func CheckKubeConfigExist() bool {
return false
}
// ValidateURL validates the URL
func ValidateURL(sourceURL string) error {
u, err := url.Parse(sourceURL)
if err != nil {
return err
}
if len(u.Host) == 0 || len(u.Scheme) == 0 {
return errors.New("URL is invalid")
}
return nil
}

View File

@@ -1614,3 +1614,41 @@ func TestGetGitHubZipURL(t *testing.T) {
}
}
*/
func TestValidateURL(t *testing.T) {
tests := []struct {
name string
url string
wantErr bool
}{
{
name: "Case 1: Valid URL",
url: "http://www.example.com/",
wantErr: false,
},
{
name: "Case 2: Invalid URL - No host",
url: "http://",
wantErr: true,
},
{
name: "Case 3: Invalid URL - No scheme",
url: "://www.example.com/",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotErr := false
got := ValidateURL(tt.url)
if got != nil {
gotErr = true
}
if !reflect.DeepEqual(gotErr, tt.wantErr) {
t.Errorf("Got %v, want %v", got, tt.wantErr)
}
})
}
}

View File

@@ -48,14 +48,33 @@ var _ = Describe("odo devfile catalog command tests", func() {
Context("When executing catalog list components", func() {
It("should list all supported devfile components", func() {
output := helper.CmdShouldPass("odo", "catalog", "list", "components")
helper.MatchAllInOutput(output, []string{"Odo Devfile Components", "java-spring-boot", "openLiberty"})
wantOutput := []string{
"Odo Devfile Components",
"NAME",
"java-spring-boot",
"openLiberty",
"DESCRIPTION",
"REGISTRY",
"SUPPORTED",
}
helper.MatchAllInOutput(output, wantOutput)
})
})
Context("When executing catalog list components with -a flag", func() {
It("should list all supported and unsupported devfile components", func() {
output := helper.CmdShouldPass("odo", "catalog", "list", "components", "-a")
helper.MatchAllInOutput(output, []string{"Odo Devfile Components", "java-spring-boot", "java-maven", "php-mysql"})
wantOutput := []string{
"Odo Devfile Components",
"NAME",
"java-spring-boot",
"java-maven",
"php-mysql",
"DESCRIPTION",
"REGISTRY",
"SUPPORTED",
}
helper.MatchAllInOutput(output, wantOutput)
})
})
})

View File

@@ -79,6 +79,13 @@ var _ = Describe("odo devfile create command tests", func() {
})
})
Context("When executing odo create with devfile component type argument and --registry flag", func() {
It("should successfully create the devfile component", func() {
componentRegistry := "DefaultDevfileRegistry"
helper.CmdShouldPass("odo", "create", "openLiberty", "--registry", componentRegistry)
})
})
Context("When executing odo create with devfile component type argument and --context flag", func() {
It("should successfully create the devfile component in the context", func() {
newContext := path.Join(context, "newContext")

View File

@@ -0,0 +1,93 @@
package devfile
import (
"os"
"path/filepath"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/openshift/odo/tests/helper"
)
var _ = Describe("odo devfile registry command tests", func() {
var project string
var context string
var currentWorkingDirectory string
const registryName string = "RegistryName"
const addRegistryURL string = "https://raw.githubusercontent.com/GeekArthur/registry/master"
const updateRegistryURL string = "http://www.example.com/update"
// This is run after every Spec (It)
var _ = BeforeEach(func() {
SetDefaultEventuallyTimeout(10 * time.Minute)
context = helper.CreateNewContext()
os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml"))
helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true")
if os.Getenv("KUBERNETES") == "true" {
project = helper.CreateRandNamespace(context)
} else {
project = helper.CreateRandProject()
}
currentWorkingDirectory = helper.Getwd()
helper.Chdir(context)
})
// This is run after every Spec (It)
var _ = AfterEach(func() {
if os.Getenv("KUBERNETES") == "true" {
helper.DeleteNamespace(project)
} else {
helper.DeleteProject(project)
}
helper.Chdir(currentWorkingDirectory)
helper.DeleteDir(context)
})
Context("When executing registry list", func() {
It("Should list all default registries", func() {
output := helper.CmdShouldPass("odo", "registry", "list")
helper.MatchAllInOutput(output, []string{"CheDevfileRegistry", "DefaultDevfileRegistry"})
})
})
Context("When executing registry commands with the registry is not present", func() {
It("Should successfully add the registry", func() {
helper.CmdShouldPass("odo", "registry", "add", registryName, addRegistryURL)
output := helper.CmdShouldPass("odo", "registry", "list")
helper.MatchAllInOutput(output, []string{registryName, addRegistryURL})
helper.CmdShouldPass("odo", "create", "nodejs", "--registry", registryName)
helper.CmdShouldPass("odo", "registry", "delete", registryName, "-f")
})
It("Should fail to update the registry", func() {
helper.CmdShouldFail("odo", "registry", "update", registryName, updateRegistryURL, "-f")
})
It("Should fail to delete the registry", func() {
helper.CmdShouldFail("odo", "registry", "delete", registryName, "-f")
})
})
Context("When executing registry commands with the registry is present", func() {
It("Should fail to add the registry", func() {
helper.CmdShouldPass("odo", "registry", "add", registryName, addRegistryURL)
helper.CmdShouldFail("odo", "registry", "add", registryName, addRegistryURL)
helper.CmdShouldPass("odo", "registry", "delete", registryName, "-f")
})
It("Should successfully update the registry", func() {
helper.CmdShouldPass("odo", "registry", "add", registryName, addRegistryURL)
helper.CmdShouldPass("odo", "registry", "update", registryName, updateRegistryURL, "-f")
output := helper.CmdShouldPass("odo", "registry", "list")
helper.MatchAllInOutput(output, []string{registryName, updateRegistryURL})
helper.CmdShouldPass("odo", "registry", "delete", registryName, "-f")
})
It("Should successfully delete the registry", func() {
helper.CmdShouldPass("odo", "registry", "add", registryName, addRegistryURL)
helper.CmdShouldPass("odo", "registry", "delete", registryName, "-f")
helper.CmdShouldFail("odo", "create", "maven", "--registry", registryName)
})
})
})