diff --git a/docs/proposals/odo-deploy.md b/docs/proposals/odo-deploy.md index 04841cdf255..2e61a6ba488 100644 --- a/docs/proposals/odo-deploy.md +++ b/docs/proposals/odo-deploy.md @@ -3,10 +3,10 @@ ## Abstract Add a new command (verb) to build a production-like/slim container image for the project and deploy the built image on the target platform. -There is an existing proposal for adding on outer-loop information (including support for multiple strategies for building and deploying the projects) to Devfile 2.1.0: +There is an existing proposal for adding on outer-loop information (including support for multiple strategies for building and deploying the projects) to Devfile v2.0.0/v2.1.0: https://github.com/devfile/kubernetes-api/issues/49 -It would be useful to start the design/development of a simpler version of `odo deploy` with devfile 2.0.0 that covers: +It would be useful to start the design/development of a simpler version of `odo deploy` with devfile v2.0.0/v2.1.0 that covers: - single build strategy - Dockerfile built using `BuildConfig` or `Kaniko`. - single deployment strategy - Kubernetes manifest deployed using `kubectl`. @@ -49,7 +49,7 @@ This deployment is equivalent to a development version of your production and wi #### Pre-requisites: - The implementation would be under the experimental flag. -- Only supported for Devfile v2.0.0 components. +- Only supported for Devfile v2.0.0/v2.1.0 components. - Only supported for Kubernetes/OpenShift targets. #### odo deploy @@ -71,13 +71,15 @@ This command will delete any resources created by odo deploy. ### Devfile -For the initial implementation, we could use devfile v2.0.0 and capture basic outer-loop information as `metadata` on the devfile. `odo` could look for specific keys, while other tools like Che could ignore them. +For the initial implementation, we could use devfile v2.0.0/v2.1.0 and capture basic outer-loop information as `metadata` on the devfile. `odo` could look for specific keys, while other tools like Che could ignore them. For example: ``` metadata: - alpha.build-dockerfile: alpha.deployment-manifest: +component: + - dockerfile: + dockerfilePath: ``` ### Dockerfile @@ -100,7 +102,7 @@ Examples: This command will perform the following actions: #### Input validation -- Check if the devfile is v2.0.0 and that it specifies the expected outer-loop metadata. +- Check if the devfile is v2.0.0/v2.1.0 and that it specifies the expected outer-loop metadata. - If not provided, display a meaningful error message. - Validate all arguments passed by the user. - If argument values are invalid, display a meaningful error message. @@ -145,4 +147,4 @@ This command will perform the following actions: - If a devfile does not provide deployment manifest, odo can perhaps create a manifest in the way it does for inner-loop. This will mean devfile creators do not need to provide a deployment manifest if they do not care so much about the deployment aspect. -- Once `odo link` and service binding is supported by odo and devfiles v2, we could use the same service binding information for `odo deploy`. \ No newline at end of file +- Once `odo link` and service binding is supported by odo and devfiles v2.0.0/v2.1.0, we could use the same service binding information for `odo deploy`. \ No newline at end of file diff --git a/pkg/devfile/adapters/common/types.go b/pkg/devfile/adapters/common/types.go index ff5db948f79..f608d70beac 100644 --- a/pkg/devfile/adapters/common/types.go +++ b/pkg/devfile/adapters/common/types.go @@ -40,6 +40,7 @@ type DeployParameters struct { EnvSpecificInfo envinfo.EnvSpecificInfo // EnvSpecificInfo contains infomation of env.yaml file Tag string // Tag refers to the image tag of the image being built ManifestSource []byte // Source of the manifest file + DeploymentPort int // Port to be used in deployment manifest } // PushParameters is a struct containing the parameters to be used when pushing to a devfile component diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 9cf676fb60f..dea5fecd5f5 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -6,10 +6,12 @@ import ( "fmt" "io" "os" + "os/signal" "path/filepath" "reflect" "strconv" "strings" + "syscall" "time" "github.com/openshift/odo/pkg/exec" @@ -31,7 +33,6 @@ import ( "github.com/openshift/odo/pkg/devfile/adapters/kubernetes/storage" "github.com/openshift/odo/pkg/devfile/adapters/kubernetes/utils" versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common" - "github.com/openshift/odo/pkg/envinfo" "github.com/openshift/odo/pkg/kclient" "github.com/openshift/odo/pkg/log" "github.com/openshift/odo/pkg/machineoutput" @@ -84,6 +85,10 @@ func (a Adapter) runBuildConfig(client *occlient.Client, parameters common.Build Name: buildName, } + controlC := make(chan os.Signal) + signal.Notify(controlC, os.Interrupt, syscall.SIGTERM) + go a.terminateBuild(controlC, client, commonObjectMeta) + _, err = client.CreateDockerBuildConfigWithBinaryInput(commonObjectMeta, dockerfilePath, parameters.Tag, []corev1.EnvVar{}) if err != nil { return err @@ -114,21 +119,30 @@ func (a Adapter) runBuildConfig(client *occlient.Client, parameters common.Build var cmdOutput string // This Go routine will automatically pipe the output from WaitForBuildToFinish to // our logger. - go func() { - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - line := scanner.Text() - - if log.IsDebug() { - _, err := fmt.Fprintln(os.Stdout, line) - if err != nil { - log.Errorf("Unable to print to stdout: %v", err) + // We pass the controlC os.Signal in order to output the logs within the terminateBuild + // function if the process is interrupted by the user performing a ^C. If we didn't pass it + // The Scanner would consume the log, and only output it if there was an err within this + // func. + go func(controlC chan os.Signal) { + select { + case <-controlC: + return + default: + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + + if log.IsDebug() { + _, err := fmt.Fprintln(os.Stdout, line) + if err != nil { + log.Errorf("Unable to print to stdout: %v", err) + } } - } - cmdOutput += fmt.Sprintln(line) + cmdOutput += fmt.Sprintln(line) + } } - }() + }(controlC) s := log.Spinner("Waiting for build to complete") if err := client.WaitForBuildToFinish(bc.Name, writer); err != nil { @@ -137,9 +151,26 @@ func (a Adapter) runBuildConfig(client *occlient.Client, parameters common.Build } s.End(true) + // Stop listening for a ^C so it doesnt perform terminateBuild during any later stages + signal.Stop(controlC) return } +// terminateBuild is triggered if the user performs a ^C action within the terminal during the build phase +// of the deploy. +// It cleans up the resources created for the build, as the defer function would not be reached. +// The subsequent deploy would fail if these resources are not cleaned up. +func (a Adapter) terminateBuild(c chan os.Signal, client *occlient.Client, commonObjectMeta metav1.ObjectMeta) { + _ = <-c + + log.Info("\nBuild process interrupted, terminating build, this might take a few seconds") + err := client.DeleteBuildConfig(commonObjectMeta) + if err != nil { + log.Info("\n", err.Error()) + } + os.Exit(0) +} + // Build image for devfile project func (a Adapter) Build(parameters common.BuildParameters) (err error) { client, err := occlient.New() @@ -159,18 +190,6 @@ func (a Adapter) Build(parameters common.BuildParameters) (err error) { return errors.New("unable to build image, only Openshift BuildConfig build is supported") } -func determinePort(envSpecificInfo envinfo.EnvSpecificInfo) string { - // Determine port to use from first non-Docker route in env.yaml) - deploymentPort := "" - for _, localURL := range envSpecificInfo.GetURL() { - if localURL.Kind != envinfo.DOCKER { - deploymentPort = strconv.Itoa(localURL.Port) - break - } - } - return deploymentPort -} - func substitueYamlVariables(baseYaml []byte, yamlSubstitutions map[string]string) []byte { // TODO: Provide a better way to do the substitution in the manifest file(s) for key, value := range yamlSubstitutions { @@ -254,7 +273,7 @@ func (a Adapter) Deploy(parameters common.DeployParameters) (err error) { yamlSubstitutions := map[string]string{ "CONTAINER_IMAGE": parameters.Tag, "COMPONENT_NAME": applicationName, - "PORT": determinePort(parameters.EnvSpecificInfo), + "PORT": strconv.Itoa(parameters.DeploymentPort), } // Build a yaml decoder with the unstructured Scheme diff --git a/pkg/devfile/parser/data/2.1.0/components_test.go b/pkg/devfile/parser/data/2.0.0/components_test.go similarity index 88% rename from pkg/devfile/parser/data/2.1.0/components_test.go rename to pkg/devfile/parser/data/2.0.0/components_test.go index 6716ff6698f..fb6c0b4662a 100644 --- a/pkg/devfile/parser/data/2.1.0/components_test.go +++ b/pkg/devfile/parser/data/2.0.0/components_test.go @@ -1,4 +1,4 @@ -package version210 +package version200 import ( "testing" @@ -21,7 +21,7 @@ func TestGetCommands(t *testing.T) { } -func getTestDevfileData() (testDevfile Devfile210, commands []common.DevfileCommand) { +func getTestDevfileData() (testDevfile Devfile200, commands []common.DevfileCommand) { command := "ls -la" component := "alias1" @@ -46,7 +46,7 @@ func getTestDevfileData() (testDevfile Devfile210, commands []common.DevfileComm }, } - testDevfileobj := Devfile210{ + testDevfileobj := Devfile200{ Commands: execCommands, } diff --git a/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go b/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go index bf008943969..6e80fb037a9 100644 --- a/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go +++ b/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go @@ -614,7 +614,8 @@ const JsonSchema200 = `{ "CheEditor", "Volume", "ChePlugin", - "Custom" + "Custom", + "Dockerfile" ], "type": "string" }, @@ -634,6 +635,40 @@ const JsonSchema200 = `{ "name" ], "type": "object" + }, + "Dockerfile":{ + "description":"Dockerfile component", + "properties":{ + "name":{ + "description":"Mandatory name that allows referencing the dockerfile component", + "type":"string" + }, + "source":{ + "sourceDir":{ + "description":"path of source directory to establish build context", + "type":"string" + }, + "location":{ + "description":"location of the source code repostory", + "type":"string" + }, + "type":"object" + }, + "dockerfilePath":{ + "description":"path to dockerfile", + "type":"string" + }, + "destination":{ + "description":"path to registry where the build image is to be pushed", + "type":"string" + } + }, + "required":[ + "name", + "dockerfilePath", + "source" + ], + "type":"object" } }, "type": "object" @@ -849,10 +884,6 @@ const JsonSchema200 = `{ "type": "string", "description": "Optional devfile name" }, - "alpha.build-dockerfile": { - "type":"string", - "description": "Optional URL to remote Dockerfile" - }, "alpha.deployment-manifest": { "type":"string", "description": "Optional URL to remote Deployment Manifest" diff --git a/pkg/devfile/parser/data/2.0.0/types.go b/pkg/devfile/parser/data/2.0.0/types.go index 72d5177c462..ffbca203844 100644 --- a/pkg/devfile/parser/data/2.0.0/types.go +++ b/pkg/devfile/parser/data/2.0.0/types.go @@ -71,6 +71,9 @@ type Component struct { // Allows specifying the definition of a volume shared by several other components Volume *Volume `json:"volume,omitempty"` + + // Allows specifying a dockerfile to initiate build + Dockerfile *Dockerfile `json:"dockerfile,omitempty"` } // Composite Composite command that allows executing several sub-commands either sequentially or concurrently @@ -408,3 +411,28 @@ type Zip struct { // Part of project to populate in the working directory. SparseCheckoutDir string `json:"sparseCheckoutDir,omitempty"` } + +// Dockerfile Component is for dockerfile image build +type Dockerfile struct { + // Mandatory name that allows referencing the Volume component in Container volume mounts or inside a parent + Name string `json:"name"` + + // Mandatory path to source code + Source *Source `json:"source"` + + //Mandatory path to dockerfile + DockerfilePath string `json:"dockerfilePath"` + + //Mandatory destination to registry to push built image + Destination string `json:"destination,omitempty"` +} + +// Source represents source code for Dockerfile Component +type Source struct { + //Mandatory path to local source directory folder + SourceDir string `json:"sourceDir"` + + //Mandatory path to source repository hosted locally or on cloud + Location string `json:"location"` +} + diff --git a/pkg/devfile/parser/data/2.1.0/components.go b/pkg/devfile/parser/data/2.1.0/components.go deleted file mode 100644 index 7ee31e7504c..00000000000 --- a/pkg/devfile/parser/data/2.1.0/components.go +++ /dev/null @@ -1,53 +0,0 @@ -package version210 - -import ( - "strings" - - "github.com/openshift/odo/pkg/devfile/parser/data/common" -) - -// GetComponents returns the slice of DevfileComponent objects parsed from the Devfile -func (d *Devfile210) GetComponents() []common.DevfileComponent { - return d.Components -} - -// GetCommands returns the slice of DevfileCommand objects parsed from the Devfile -func (d *Devfile210) GetCommands() []common.DevfileCommand { - var commands []common.DevfileCommand - - for _, command := range d.Commands { - // we convert devfile command id to lowercase so that we can handle - // cases efficiently without being error prone - // we also convert the odo push commands from build-command and run-command flags - command.Exec.Id = strings.ToLower(command.Exec.Id) - commands = append(commands, command) - } - - return commands -} - -// GetParent returns the DevfileParent object parsed from devfile -func (d *Devfile210) GetParent() common.DevfileParent { - return d.Parent -} - -// GetProjects returns the DevfileProject Object parsed from devfile -func (d *Devfile210) GetProjects() []common.DevfileProject { - return d.Projects -} - -// GetMetadata returns the DevfileMetadata Object parsed from devfile -func (d *Devfile210) GetMetadata() common.DevfileMetadata { - return d.Metadata -} - -// GetEvents returns the Events Object parsed from devfile -func (d *Devfile210) GetEvents() common.DevfileEvents { - return d.Events -} - -// GetAliasedComponents returns the slice of DevfileComponent objects that each have an alias -func (d *Devfile210) GetAliasedComponents() []common.DevfileComponent { - // V2.1.0 has name required in jsonSchema - return d.Components -} diff --git a/pkg/devfile/parser/data/2.1.0/devfileJsonSchema210.go b/pkg/devfile/parser/data/2.1.0/devfileJsonSchema210.go deleted file mode 100644 index 9f98d858204..00000000000 --- a/pkg/devfile/parser/data/2.1.0/devfileJsonSchema210.go +++ /dev/null @@ -1,903 +0,0 @@ -package version210 - -const JsonSchema210 = `{ - "description":"Devfile schema", - "properties":{ - "commands":{ - "description":"Predefined, ready-to-use, workspace-related commands", - "items":{ - "properties":{ - "composite":{ - "description":"Composite command", - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "description":"Optional map of free-form additional command attributes", - "type":"object" - }, - "commands":{ - "description":"The commands that comprise this composite command", - "items":{ - "type":"string" - }, - "type":"array" - }, - "group":{ - "description":"Defines the group this command is part of", - "properties":{ - "isDefault":{ - "description":"Identifies the default command for a given group kind", - "type":"boolean" - }, - "kind":{ - "description":"Kind of group the command is part of", - "enum":[ - "build", - "run", - "test", - "debug" - ], - "type":"string" - } - }, - "required":[ - "kind" - ], - "type":"object" - }, - "id":{ - "description":"Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events.", - "type":"string" - }, - "label":{ - "description":"Optional label that provides a label for this command to be used in Editor UI menus for example", - "type":"string" - }, - "parallel":{ - "type":"boolean" - } - }, - "required":[ - "id" - ], - "type":"object" - }, - "custom":{ - "description":"Custom command", - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "description":"Optional map of free-form additional command attributes", - "type":"object" - }, - "commandClass":{ - "type":"string" - }, - "embeddedResource":{ - "type":"object" - }, - "group":{ - "description":"Defines the group this command is part of", - "properties":{ - "isDefault":{ - "description":"Identifies the default command for a given group kind", - "type":"boolean" - }, - "kind":{ - "description":"Kind of group the command is part of", - "enum":[ - "build", - "run", - "test", - "debug" - ], - "type":"string" - } - }, - "required":[ - "kind" - ], - "type":"object" - }, - "id":{ - "description":"Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events.", - "type":"string" - }, - "label":{ - "description":"Optional label that provides a label for this command to be used in Editor UI menus for example", - "type":"string" - } - }, - "required":[ - "commandClass", - "embeddedResource", - "id" - ], - "type":"object" - }, - "exec":{ - "description":"Exec command", - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "description":"Optional map of free-form additional command attributes", - "type":"object" - }, - "commandLine":{ - "description":"The actual command-line string", - "type":"string" - }, - "component":{ - "description":"Describes component to which given action relates", - "type":"string" - }, - "env":{ - "description":"Optional list of environment variables that have to be set before running the command", - "items":{ - "properties":{ - "name":{ - "type":"string" - }, - "value":{ - "type":"string" - } - }, - "required":[ - "name", - "value" - ], - "type":"object" - }, - "type":"array" - }, - "group":{ - "description":"Defines the group this command is part of", - "properties":{ - "isDefault":{ - "description":"Identifies the default command for a given group kind", - "type":"boolean" - }, - "kind":{ - "description":"Kind of group the command is part of", - "enum":[ - "build", - "run", - "test", - "debug" - ], - "type":"string" - } - }, - "required":[ - "kind" - ], - "type":"object" - }, - "id":{ - "description":"Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events.", - "type":"string" - }, - "label":{ - "description":"Optional label that provides a label for this command to be used in Editor UI menus for example", - "type":"string" - }, - "workingDir":{ - "description":"Working directory where the command should be executed", - "type":"string" - } - }, - "required":[ - "commandLine", - "id" - ], - "type":"object" - }, - "type":{ - "description":"Type of workspace command", - "enum":[ - "Exec", - "VscodeTask", - "VscodeLaunch", - "Custom" - ], - "type":"string" - }, - "vscodeLaunch":{ - "description":"VscodeLaunch command", - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "description":"Optional map of free-form additional command attributes", - "type":"object" - }, - "group":{ - "description":"Defines the group this command is part of", - "properties":{ - "isDefault":{ - "description":"Identifies the default command for a given group kind", - "type":"boolean" - }, - "kind":{ - "description":"Kind of group the command is part of", - "enum":[ - "build", - "run", - "test", - "debug" - ], - "type":"string" - } - }, - "required":[ - "kind" - ], - "type":"object" - }, - "id":{ - "description":"Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events.", - "type":"string" - }, - "inlined":{ - "description":"Embedded content of the vscode configuration file", - "type":"string" - }, - "locationType":{ - "description":"Type of Vscode configuration command location", - "type":"string" - }, - "url":{ - "description":"Location as an absolute of relative URL", - "type":"string" - } - }, - "required":[ - "id" - ], - "type":"object" - }, - "vscodeTask":{ - "description":"VscodeTask command", - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "description":"Optional map of free-form additional command attributes", - "type":"object" - }, - "group":{ - "description":"Defines the group this command is part of", - "properties":{ - "isDefault":{ - "description":"Identifies the default command for a given group kind", - "type":"boolean" - }, - "kind":{ - "description":"Kind of group the command is part of", - "enum":[ - "build", - "run", - "test", - "debug" - ], - "type":"string" - } - }, - "required":[ - "kind" - ], - "type":"object" - }, - "id":{ - "description":"Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events.", - "type":"string" - }, - "inlined":{ - "description":"Embedded content of the vscode configuration file", - "type":"string" - }, - "locationType":{ - "description":"Type of Vscode configuration command location", - "type":"string" - }, - "url":{ - "description":"Location as an absolute of relative URL", - "type":"string" - } - }, - "required":[ - "id" - ], - "type":"object" - } - }, - "type":"object" - }, - "type":"array" - }, - "components":{ - "description":"List of the workspace components, such as editor and plugins, user-provided containers, or other types of components", - "items":{ - "properties":{ - "cheEditor":{ - "description":"CheEditor component", - "properties":{ - "locationType":{ - "description":"Type of plugin location", - "enum":[ - "RegistryEntry", - "Uri" - ], - "type":"string" - }, - "memoryLimit":{ - "type":"string" - }, - "name":{ - "description":"Optional name that allows referencing the component in commands, or inside a parent If omitted it will be infered from the location (uri or registryEntry)", - "type":"string" - }, - "registryEntry":{ - "description":"Location of an entry inside a plugin registry", - "properties":{ - "baseUrl":{ - "type":"string" - }, - "id":{ - "type":"string" - } - }, - "required":[ - "id" - ], - "type":"object" - }, - "uri":{ - "description":"Location defined as an URI", - "type":"string" - } - }, - "type":"object" - }, - "chePlugin":{ - "description":"ChePlugin component", - "properties":{ - "locationType":{ - "description":"Type of plugin location", - "enum":[ - "RegistryEntry", - "Uri" - ], - "type":"string" - }, - "memoryLimit":{ - "type":"string" - }, - "name":{ - "description":"Optional name that allows referencing the component in commands, or inside a parent If omitted it will be infered from the location (uri or registryEntry)", - "type":"string" - }, - "registryEntry":{ - "description":"Location of an entry inside a plugin registry", - "properties":{ - "baseUrl":{ - "type":"string" - }, - "id":{ - "type":"string" - } - }, - "required":[ - "id" - ], - "type":"object" - }, - "uri":{ - "description":"Location defined as an URI", - "type":"string" - } - }, - "type":"object" - }, - "container":{ - "description":"Container component", - "properties":{ - "endpoints":{ - "items":{ - "properties":{ - "attributes":{ - "additionalProperties":{ - "type":"string" - }, - "type":"object" - }, - "configuration":{ - "properties":{ - "cookiesAuthEnabled":{ - "type":"boolean" - }, - "discoverable":{ - "type":"boolean" - }, - "path":{ - "type":"string" - }, - "protocol":{ - "description":"The is the low-level protocol of traffic coming through this endpoint. Default value is \"tcp\"", - "type":"string" - }, - "public":{ - "type":"boolean" - }, - "scheme":{ - "description":"The is the URL scheme to use when accessing the endpoint. Default value is \"http\"", - "type":"string" - }, - "secure":{ - "type":"boolean" - }, - "type":{ - "enum":[ - "ide", - "terminal", - "ide-dev" - ], - "type":"string" - } - }, - "type":"object" - }, - "name":{ - "type":"string" - }, - "targetPort":{ - "type":"integer" - } - }, - "required":[ - "configuration", - "name", - "targetPort" - ], - "type":"object" - }, - "type":"array" - }, - "env":{ - "description":"Environment variables used in this container", - "items":{ - "properties":{ - "name":{ - "type":"string" - }, - "value":{ - "type":"string" - } - }, - "required":[ - "name", - "value" - ], - "type":"object" - }, - "type":"array" - }, - "image":{ - "type":"string" - }, - "memoryLimit":{ - "type":"string" - }, - "mountSources":{ - "type":"boolean" - }, - "name":{ - "type":"string" - }, - "sourceMapping":{ - "description":"Optional specification of the path in the container where project sources should be transferred/mounted when mountSources is true. When omitted, the value of the PROJECTS_ROOT environment variable is used.", - "type":"string" - }, - "volumeMounts":{ - "description":"List of volumes mounts that should be mounted is this container.", - "items":{ - "description":"Volume that should be mounted to a component container", - "properties":{ - "name":{ - "description":"The volume mount name is the name of an existing Volume component. If no corresponding Volume component exist it is implicitly added. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files.", - "type":"string" - }, - "path":{ - "description":"The path in the component container where the volume should be mounted", - "type":"string" - } - }, - "required":[ - "name", - "path" - ], - "type":"object" - }, - "type":"array" - } - }, - "required":[ - "image", - "name" - ], - "type":"object" - }, - "custom":{ - "description":"Custom component", - "properties":{ - "componentClass":{ - "type":"string" - }, - "embeddedResource":{ - "type":"object" - }, - "name":{ - "type":"string" - } - }, - "required":[ - "componentClass", - "embeddedResource", - "name" - ], - "type":"object" - }, - "kubernetes":{ - "description":"Kubernetes component", - "properties":{ - "inlined":{ - "description":"Reference to the plugin definition", - "type":"string" - }, - "locationType":{ - "description":"Type of Kubernetes-like location", - "type":"string" - }, - "name":{ - "description":"Mandatory name that allows referencing the component in commands, or inside a parent", - "type":"string" - }, - "url":{ - "description":"Location in a plugin registry", - "type":"string" - } - }, - "required":[ - "name" - ], - "type":"object" - }, - "openshift":{ - "description":"Openshift component", - "properties":{ - "inlined":{ - "description":"Reference to the plugin definition", - "type":"string" - }, - "locationType":{ - "description":"Type of Kubernetes-like location", - "type":"string" - }, - "name":{ - "description":"Mandatory name that allows referencing the component in commands, or inside a parent", - "type":"string" - }, - "url":{ - "description":"Location in a plugin registry", - "type":"string" - } - }, - "required":[ - "name" - ], - "type":"object" - }, - "type":{ - "description":"Type of project source", - "enum":[ - "Container", - "Kubernetes", - "Openshift", - "CheEditor", - "Volume", - "ChePlugin", - "Custom", - "Dockerfile" - ], - "type":"string" - }, - "volume":{ - "description":"Volume component", - "properties":{ - "name":{ - "description":"Mandatory name that allows referencing the Volume component in Container volume mounts or inside a parent", - "type":"string" - }, - "size":{ - "description":"Size of the volume", - "type":"string" - } - }, - "required":[ - "name" - ], - "type":"object" - }, - "Dockerfile":{ - "description":"Dockerfile component", - "properties":{ - "name":{ - "description":"Mandatory name that allows referencing the dockerfile component", - "type":"string" - }, - "source":{ - "sourceDir":{ - "description":"path of source directory to establish build context", - "type":"string" - }, - "location":{ - "description":"location of the source code repostory", - "type":"string" - }, - "type":"object" - }, - "dockerfilePath":{ - "description":"path to dockerfile", - "type":"string" - }, - "destination":{ - "description":"path to registry where the build image is to be pushed", - "type":"string" - } - }, - "required":[ - "name", - "dockerfilePath", - "source" - ], - "type":"object" - } - }, - "type":"object" - }, - "type":"array" - }, - "events":{ - "description":"Bindings of commands to events. Each command is referred-to by its name.", - "properties":{ - "postStart":{ - "description":"Names of commands that should be executed after the workspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser.", - "items":{ - "type":"string" - }, - "type":"array" - }, - "postStop":{ - "description":"Names of commands that should be executed after stopping the workspace.", - "items":{ - "type":"string" - }, - "type":"array" - }, - "preStart":{ - "description":"Names of commands that should be executed before the workspace start. Kubernetes-wise, these commands would typically be executed in init containers of the workspace POD.", - "items":{ - "type":"string" - }, - "type":"array" - }, - "preStop":{ - "description":"Names of commands that should be executed before stopping the workspace.", - "items":{ - "type":"string" - }, - "type":"array" - } - }, - "type":"object" - }, - "parent":{ - "description":"Parent workspace template", - "properties":{ - "kubernetes":{ - "description":"Reference to a Kubernetes CRD of type DevWorkspaceTemplate", - "properties":{ - "name":{ - "type":"string" - }, - "namespace":{ - "type":"string" - } - }, - "required":[ - "name" - ], - "type":"object" - }, - "locationType":{ - "description":"Type of parent location", - "enum":[ - "Uri", - "RegistryEntry", - "Kubernetes" - ], - "type":"string" - }, - "registryEntry":{ - "description":"Entry in a registry (base URL + ID) that contains a Devfile yaml file", - "properties":{ - "baseUrl":{ - "type":"string" - }, - "id":{ - "type":"string" - } - }, - "required":[ - "id" - ], - "type":"object" - }, - "uri":{ - "description":"Uri of a Devfile yaml file", - "type":"string" - } - }, - "type":"object" - }, - "projects":{ - "description":"Projects worked on in the workspace, containing names and sources locations", - "items":{ - "properties":{ - "clonePath":{ - "description":"Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name.", - "type":"string" - }, - "custom":{ - "description":"Project's Custom source", - "properties":{ - "embeddedResource":{ - "type":"object" - }, - "projectSourceClass":{ - "type":"string" - } - }, - "required":[ - "embeddedResource", - "projectSourceClass" - ], - "type":"object" - }, - "git":{ - "description":"Project's Git source", - "properties":{ - "branch":{ - "description":"The branch to check", - "type":"string" - }, - "location":{ - "description":"Project's source location address. Should be URL for git and github located projects, or; file:// for zip", - "type":"string" - }, - "sparseCheckoutDir":{ - "description":"Part of project to populate in the working directory.", - "type":"string" - }, - "startPoint":{ - "description":"The tag or commit id to reset the checked out branch to", - "type":"string" - } - }, - "required":[ - "location" - ], - "type":"object" - }, - "github":{ - "description":"Project's GitHub source", - "properties":{ - "branch":{ - "description":"The branch to check", - "type":"string" - }, - "location":{ - "description":"Project's source location address. Should be URL for git and github located projects, or; file:// for zip", - "type":"string" - }, - "sparseCheckoutDir":{ - "description":"Part of project to populate in the working directory.", - "type":"string" - }, - "startPoint":{ - "description":"The tag or commit id to reset the checked out branch to", - "type":"string" - } - }, - "required":[ - "location" - ], - "type":"object" - }, - "name":{ - "description":"Project name", - "type":"string" - }, - "sourceType":{ - "description":"Type of project source", - "enum":[ - "Git", - "Github", - "Zip", - "Custom" - ], - "type":"string" - }, - "zip":{ - "description":"Project's Zip source", - "properties":{ - "location":{ - "description":"Project's source location address. Should be URL for git and github located projects, or; file:// for zip", - "type":"string" - }, - "sparseCheckoutDir":{ - "description":"Part of project to populate in the working directory.", - "type":"string" - } - }, - "required":[ - "location" - ], - "type":"object" - } - }, - "required":[ - "name" - ], - "type":"object" - }, - "type":"array" - }, - "metadata":{ - "type":"object", - "description":"Optional metadata", - "properties":{ - "version":{ - "type":"string", - "description":"Optional semver-compatible version", - "pattern":"^([0-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" - }, - "name":{ - "type":"string", - "description":"Optional devfile name" - }, - "alpha.deployment-manifest":{ - "type":"string", - "description":"Optional URL to remote Deployment Manifest" - } - } - }, - "schemaVersion":{ - "type":"string", - "description":"Devfile schema version", - "pattern":"^([2-9]+)\\.([0-9]+)\\.([0-9]+)(\\-[0-9a-z-]+(\\.[0-9a-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" - } - }, - "type":"object", - "required":[ - "schemaVersion" - ] - }` diff --git a/pkg/devfile/parser/data/2.1.0/types.go b/pkg/devfile/parser/data/2.1.0/types.go deleted file mode 100644 index bbffd3c12c3..00000000000 --- a/pkg/devfile/parser/data/2.1.0/types.go +++ /dev/null @@ -1,435 +0,0 @@ -package version210 - -import "github.com/openshift/odo/pkg/devfile/parser/data/common" - -// CommandGroupType describes the kind of command group. -// +kubebuilder:validation:Enum=build;run;test;debug -type CommandGroupType string - -const ( - BuildCommandGroupType CommandGroupType = "build" - RunCommandGroupType CommandGroupType = "run" - TestCommandGroupType CommandGroupType = "test" - DebugCommandGroupType CommandGroupType = "debug" -) - -// Devfile210 Devfile schema. -type Devfile210 struct { - - // Predefined, ready-to-use, workspace-related commands - Commands []common.DevfileCommand `json:"commands,omitempty"` - - // List of the workspace components, such as editor and plugins, user-provided containers, or other types of components - Components []common.DevfileComponent `json:"components,omitempty"` - - // Bindings of commands to events. Each command is referred-to by its name. - Events common.DevfileEvents `json:"events,omitempty"` - - // Optional metadata - Metadata common.DevfileMetadata `json:"metadata,omitempty"` - - // Parent workspace template - Parent common.DevfileParent `json:"parent,omitempty"` - - // Projects worked on in the workspace, containing names and sources locations - Projects []common.DevfileProject `json:"projects,omitempty"` - - // Devfile schema version - SchemaVersion string `json:"schemaVersion"` -} - -// CommandsItems -type Command struct { - - // Composite command that allows executing several sub-commands either sequentially or concurrently - Composite *Composite `json:"composite,omitempty"` - - // CLI Command executed in a component container - Exec *Exec `json:"exec,omitempty"` - - // Command providing the definition of a VsCode launch action - VscodeLaunch *VscodeLaunch `json:"vscodeLaunch,omitempty"` - - // Command providing the definition of a VsCode Task - VscodeTask *VscodeTask `json:"vscodeTask,omitempty"` -} - -// ComponentsItems -type Component struct { - - // Allows adding and configuring workspace-related containers - Container *Container `json:"container,omitempty"` - - // Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production. - Kubernetes *Kubernetes `json:"kubernetes,omitempty"` - - // Allows importing into the workspace the OpenShift resources defined in a given manifest. For example this allows reusing the OpenShift definitions used to deploy some runtime components in production. - Openshift *Openshift `json:"openshift,omitempty"` - - // Allows importing a plugin. Plugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as `DevWorkspaceTemplate` Kubernetes Custom Resources - Plugin *Plugin `json:"plugin,omitempty"` - - // Allows specifying the definition of a volume shared by several other components - Volume *Volume `json:"volume,omitempty"` - - // Allows specifying a dockerfile to initiate build - Dockerfile *Dockerfile `json:"dockerfile,omitempty"` -} - -// Composite Composite command that allows executing several sub-commands either sequentially or concurrently -type Composite struct { - - // Optional map of free-form additional command attributes - Attributes map[string]string `json:"attributes,omitempty"` - - // The commands that comprise this composite command - Commands []string `json:"commands,omitempty"` - - // Defines the group this command is part of - Group *Group `json:"group,omitempty"` - - // Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events. - Id string `json:"id"` - - // Optional label that provides a label for this command to be used in Editor UI menus for example - Label string `json:"label,omitempty"` - - // Indicates if the sub-commands should be executed concurrently - Parallel bool `json:"parallel,omitempty"` -} - -// Configuration -type Configuration struct { - CookiesAuthEnabled bool `json:"cookiesAuthEnabled,omitempty"` - Discoverable bool `json:"discoverable,omitempty"` - Path string `json:"path,omitempty"` - - // The is the low-level protocol of traffic coming through this endpoint. Default value is "tcp" - Protocol string `json:"protocol,omitempty"` - Public bool `json:"public,omitempty"` - - // The is the URL scheme to use when accessing the endpoint. Default value is "http" - Scheme string `json:"scheme,omitempty"` - Secure bool `json:"secure,omitempty"` - Type string `json:"type,omitempty"` -} - -// Container Allows adding and configuring workspace-related containers -type Container struct { - - // The arguments to supply to the command running the dockerimage component. The arguments are supplied either to the default command provided in the image or to the overridden command. Defaults to an empty array, meaning use whatever is defined in the image. - Args []string `json:"args,omitempty"` - - // The command to run in the dockerimage component instead of the default one provided in the image. Defaults to an empty array, meaning use whatever is defined in the image. - Command []string `json:"command,omitempty"` - - Endpoints []*Endpoint `json:"endpoints,omitempty"` - - // Environment variables used in this container - Env []*Env `json:"env,omitempty"` - Image string `json:"image,omitempty"` - MemoryLimit string `json:"memoryLimit,omitempty"` - MountSources bool `json:"mountSources,omitempty"` - Name string `json:"name"` - - // Optional specification of the path in the container where project sources should be transferred/mounted when `mountSources` is `true`. When omitted, the value of the `PROJECTS_ROOT` environment variable is used. - SourceMapping string `json:"sourceMapping,omitempty"` - - // List of volumes mounts that should be mounted is this container. - VolumeMounts []*VolumeMount `json:"volumeMounts,omitempty"` -} - -// Endpoint -type Endpoint struct { - Attributes map[string]string `json:"attributes,omitempty"` - Configuration *Configuration `json:"configuration,omitempty"` - Name string `json:"name"` - TargetPort int32 `json:"targetPort"` -} - -// Env -type Env struct { - Name string `json:"name"` - Value string `json:"value"` -} - -// Events Bindings of commands to events. Each command is referred-to by its name. -type Events struct { - - // Names of commands that should be executed after the workspace is completely started. In the case of Che-Theia, these commands should be executed after all plugins and extensions have started, including project cloning. This means that those commands are not triggered until the user opens the IDE in his browser. - PostStart []string `json:"postStart,omitempty"` - - // Names of commands that should be executed after stopping the workspace. - PostStop []string `json:"postStop,omitempty"` - - // Names of commands that should be executed before the workspace start. Kubernetes-wise, these commands would typically be executed in init containers of the workspace POD. - PreStart []string `json:"preStart,omitempty"` - - // Names of commands that should be executed before stopping the workspace. - PreStop []string `json:"preStop,omitempty"` -} - -// Exec CLI Command executed in a component container -type Exec struct { - - // Optional map of free-form additional command attributes - Attributes map[string]string `json:"attributes,omitempty"` - - // The actual command-line string - CommandLine string `json:"commandLine,omitempty"` - - // Describes component to which given action relates - Component string `json:"component,omitempty"` - - // Optional list of environment variables that have to be set before running the command - Env []*Env `json:"env,omitempty"` - - // Defines the group this command is part of - Group *Group `json:"group,omitempty"` - - // Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events. - Id string `json:"id"` - - // Optional label that provides a label for this command to be used in Editor UI menus for example - Label string `json:"label,omitempty"` - - // Working directory where the command should be executed - WorkingDir string `json:"workingDir,omitempty"` -} - -// Git Project's Git source -type Git struct { - - // The branch to check - Branch string `json:"branch,omitempty"` - - // Project's source location address. Should be URL for git and github located projects, or; file:// for zip - Location string `json:"location,omitempty"` - - // Part of project to populate in the working directory. - SparseCheckoutDir string `json:"sparseCheckoutDir,omitempty"` - - // The tag or commit id to reset the checked out branch to - StartPoint string `json:"startPoint,omitempty"` -} - -// Github Project's GitHub source -type Github struct { - - // The branch to check - Branch string `json:"branch,omitempty"` - - // Project's source location address. Should be URL for git and github located projects, or; file:// for zip - Location string `json:"location,omitempty"` - - // Part of project to populate in the working directory. - SparseCheckoutDir string `json:"sparseCheckoutDir,omitempty"` - - // The tag or commit id to reset the checked out branch to - StartPoint string `json:"startPoint,omitempty"` -} - -// Group Defines the group this command is part of -type Group struct { - - // Identifies the default command for a given group kind - IsDefault bool `json:"isDefault,omitempty"` - - // Kind of group the command is part of - Kind CommandGroupType `json:"kind"` -} - -// Kubernetes Allows importing into the workspace the Kubernetes resources defined in a given manifest. For example this allows reusing the Kubernetes definitions used to deploy some runtime components in production. -type Kubernetes struct { - - // Inlined manifest - Inlined string `json:"inlined,omitempty"` - - // Mandatory name that allows referencing the component in commands, or inside a parent - Name string `json:"name"` - - // Location in a file fetched from a uri. - Uri string `json:"uri,omitempty"` -} - -// Metadata Optional metadata -type Metadata struct { - - // Optional devfile name - Name string `json:"name,omitempty"` - - // Optional semver-compatible version - Version string `json:"version,omitempty"` -} - -// Openshift Configuration overriding for an OpenShift component -type Openshift struct { - - // Inlined manifest - Inlined string `json:"inlined,omitempty"` - - // Mandatory name that allows referencing the component in commands, or inside a parent - Name string `json:"name"` - - // Location in a file fetched from a uri. - Uri string `json:"uri,omitempty"` -} - -// Parent Parent workspace template -type Parent struct { - - // Predefined, ready-to-use, workspace-related commands - Commands []*Command `json:"commands,omitempty"` - - // List of the workspace components, such as editor and plugins, user-provided containers, or other types of components - Components []*Component `json:"components,omitempty"` - - // Bindings of commands to events. Each command is referred-to by its name. - Events *Events `json:"events,omitempty"` - - // Id in a registry that contains a Devfile yaml file - Id string `json:"id,omitempty"` - - // Reference to a Kubernetes CRD of type DevWorkspaceTemplate - Kubernetes *Kubernetes `json:"kubernetes,omitempty"` - - // Projects worked on in the workspace, containing names and sources locations - Projects []*Project `json:"projects,omitempty"` - - RegistryUrl string `json:"registryUrl,omitempty"` - - // Uri of a Devfile yaml file - Uri string `json:"uri,omitempty"` -} - -// Plugin Allows importing a plugin. Plugins are mainly imported devfiles that contribute components, commands and events as a consistent single unit. They are defined in either YAML files following the devfile syntax, or as `DevWorkspaceTemplate` Kubernetes Custom Resources -type Plugin struct { - - // Overrides of commands encapsulated in a plugin. Overriding is done using a strategic merge - Commands []*Command `json:"commands,omitempty"` - - // Overrides of components encapsulated in a plugin. Overriding is done using a strategic merge - Components []*Component `json:"components,omitempty"` - - // Id in a registry that contains a Devfile yaml file - Id string `json:"id,omitempty"` - - // Reference to a Kubernetes CRD of type DevWorkspaceTemplate - Kubernetes *Kubernetes `json:"kubernetes,omitempty"` - - // Optional name that allows referencing the component in commands, or inside a parent If omitted it will be infered from the location (uri or registryEntry) - Name string `json:"name,omitempty"` - RegistryUrl string `json:"registryUrl,omitempty"` - - // Uri of a Devfile yaml file - Uri string `json:"uri,omitempty"` -} - -// ProjectsItems -type Project struct { - - // Path relative to the root of the projects to which this project should be cloned into. This is a unix-style relative path (i.e. uses forward slashes). The path is invalid if it is absolute or tries to escape the project root through the usage of '..'. If not specified, defaults to the project name. - ClonePath string `json:"clonePath,omitempty"` - - // Project's Git source - Git *Git `json:"git,omitempty"` - - // Project's GitHub source - Github *Github `json:"github,omitempty"` - - // Project name - Name string `json:"name"` - - // Project's Zip source - Zip *Zip `json:"zip,omitempty"` -} - -// Volume Allows specifying the definition of a volume shared by several other components -type Volume struct { - - // Mandatory name that allows referencing the Volume component in Container volume mounts or inside a parent - Name string `json:"name"` - - // Size of the volume - Size string `json:"size,omitempty"` -} - -// VolumeMountsItems Volume that should be mounted to a component container -type VolumeMount struct { - - // The volume mount name is the name of an existing `Volume` component. If no corresponding `Volume` component exist it is implicitly added. If several containers mount the same volume name then they will reuse the same volume and will be able to access to the same files. - Name string `json:"name"` - - // The path in the component container where the volume should be mounted. If not path is mentioned, default path is the is `/`. - Path string `json:"path,omitempty"` -} - -// VscodeLaunch Command providing the definition of a VsCode launch action -type VscodeLaunch struct { - - // Optional map of free-form additional command attributes - Attributes map[string]string `json:"attributes,omitempty"` - - // Defines the group this command is part of - Group *Group `json:"group,omitempty"` - - // Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events. - Id string `json:"id"` - - // Inlined content of the VsCode configuration - Inlined string `json:"inlined,omitempty"` - - // Location as an absolute of relative URI the VsCode configuration will be fetched from - Uri string `json:"uri,omitempty"` -} - -// VscodeTask Command providing the definition of a VsCode Task -type VscodeTask struct { - - // Optional map of free-form additional command attributes - Attributes map[string]string `json:"attributes,omitempty"` - - // Defines the group this command is part of - Group *Group `json:"group,omitempty"` - - // Mandatory identifier that allows referencing this command in composite commands, or from a parent, or in events. - Id string `json:"id"` - - // Inlined content of the VsCode configuration - Inlined string `json:"inlined,omitempty"` - - // Location as an absolute of relative URI the VsCode configuration will be fetched from - Uri string `json:"uri,omitempty"` -} - -// Zip Project's Zip source -type Zip struct { - - // Project's source location address. Should be URL for git and github located projects, or; file:// for zip - Location string `json:"location,omitempty"` - - // Part of project to populate in the working directory. - SparseCheckoutDir string `json:"sparseCheckoutDir,omitempty"` -} - -type Dockerfile struct { - // Mandatory name that allows referencing the Volume component in Container volume mounts or inside a parent - Name string `json:"name"` - - // Mandatory path to source code - Source *Source `json:"source"` - - //Mandatory path to dockerfile - DockerfilePath string `json:"dockerfilePath"` - - //Mandatory destination to registry to push built image - Destination string `json:"destination,omitempty"` -} - -type Source struct { - //Mandatory path to local source directory folder - SourceDir string `json:"sourceDir"` - - //Mandatory path to source repository hosted locally or on cloud - Location string `json:"location"` -} diff --git a/pkg/devfile/parser/data/versions.go b/pkg/devfile/parser/data/versions.go index f0dde350afe..bdee911e8c7 100644 --- a/pkg/devfile/parser/data/versions.go +++ b/pkg/devfile/parser/data/versions.go @@ -5,7 +5,6 @@ import ( v100 "github.com/openshift/odo/pkg/devfile/parser/data/1.0.0" v200 "github.com/openshift/odo/pkg/devfile/parser/data/2.0.0" - v210 "github.com/openshift/odo/pkg/devfile/parser/data/2.1.0" ) // SupportedApiVersions stores the supported devfile API versions @@ -30,8 +29,9 @@ var apiVersionToDevfileStruct map[supportedApiVersion]reflect.Type func init() { apiVersionToDevfileStruct = make(map[supportedApiVersion]reflect.Type) apiVersionToDevfileStruct[apiVersion100] = reflect.TypeOf(v100.Devfile100{}) + // 2.1.0 is backward compatible with 2.0.0. v200.Devfile200 is for 2.x apiVersionToDevfileStruct[apiVersion200] = reflect.TypeOf(v200.Devfile200{}) - apiVersionToDevfileStruct[apiVersion210] = reflect.TypeOf(v210.Devfile210{}) + apiVersionToDevfileStruct[apiVersion210] = reflect.TypeOf(v200.Devfile200{}) } @@ -42,6 +42,7 @@ var devfileApiVersionToJSONSchema map[supportedApiVersion]string func init() { devfileApiVersionToJSONSchema = make(map[supportedApiVersion]string) devfileApiVersionToJSONSchema[apiVersion100] = v100.JsonSchema100 + // 2.1.0 is backward compatible with 2.0.0. v200.JsonSchema200 is for 2.x devfileApiVersionToJSONSchema[apiVersion200] = v200.JsonSchema200 - devfileApiVersionToJSONSchema[apiVersion210] = v210.JsonSchema210 + devfileApiVersionToJSONSchema[apiVersion210] = v200.JsonSchema200 } diff --git a/pkg/devfile/validate/validate.go b/pkg/devfile/validate/validate.go index a205c9a0af7..e976bdd3199 100644 --- a/pkg/devfile/validate/validate.go +++ b/pkg/devfile/validate/validate.go @@ -8,7 +8,6 @@ import ( v100 "github.com/openshift/odo/pkg/devfile/parser/data/1.0.0" v200 "github.com/openshift/odo/pkg/devfile/parser/data/2.0.0" - v210 "github.com/openshift/odo/pkg/devfile/parser/data/2.1.0" ) // ValidateDevfileData validates whether sections of devfile are odo compatible @@ -27,11 +26,6 @@ func ValidateDevfileData(data interface{}) error { components = d.GetComponents() } - if typeData == reflect.TypeOf(&v210.Devfile210{}) { - d := data.(*v210.Devfile210) - components = d.GetComponents() - } - // Validate Components if err := ValidateComponents(components); err != nil { return err diff --git a/pkg/envinfo/envinfo.go b/pkg/envinfo/envinfo.go index 04770448dbd..ccbdbd305f4 100644 --- a/pkg/envinfo/envinfo.go +++ b/pkg/envinfo/envinfo.go @@ -1,6 +1,7 @@ package envinfo import ( + "fmt" "io" "os" "path/filepath" @@ -297,6 +298,16 @@ func (ei *EnvInfo) GetDebugPort() int { return *ei.componentSettings.DebugPort } +// GetPortByURLKind returns the Port of a specific URL type, returns 0 if nil +func (ei *EnvInfo) GetPortByURLKind(urlKind URLKind) (int, error) { + for _, localURL := range ei.GetURL() { + if localURL.Kind == urlKind { + return localURL.Port, nil + } + } + return 0, errors.New(fmt.Sprintf("unable to find port for URL of kind: '%s'", urlKind)) +} + // GetNamespace returns component namespace func (ei *EnvInfo) GetNamespace() string { return ei.componentSettings.Namespace diff --git a/pkg/odo/cli/component/deploy.go b/pkg/odo/cli/component/deploy.go index f93285501c0..b2cb115ffb5 100644 --- a/pkg/odo/cli/component/deploy.go +++ b/pkg/odo/cli/component/deploy.go @@ -1,6 +1,7 @@ package component import ( + "bytes" "fmt" "path/filepath" @@ -43,6 +44,7 @@ type DeployOptions struct { namespace string tag string ManifestSource []byte + DeploymentPort int *genericclioptions.Context } @@ -79,6 +81,7 @@ func (do *DeployOptions) Complete(name string, cmd *cobra.Command, args []string func (do *DeployOptions) Validate() (err error) { log.Infof("\nValidation") + // Validate the --tag if do.tag == "" { return errors.New("odo deploy requires a tag, in the format /namespace>/") @@ -118,7 +121,7 @@ func (do *DeployOptions) Validate() (err error) { } if dockerfileURL != "" { - dockerfileBytes, err := util.DownloadFileInMemory(dockerfileURL) + dockerfileBytes, err := util.LoadFileIntoMemory(dockerfileURL) if err != nil { s.End(false) return errors.New("unable to download Dockerfile from URL specified in devfile") @@ -143,22 +146,31 @@ func (do *DeployOptions) Validate() (err error) { s = log.Spinner("Validating deployment information") metadata := do.devObj.Data.GetMetadata() manifestURL := metadata.Manifest + if manifestURL == "" { s.End(false) return errors.New("Unable to deploy as alpha.deployment-manifest is not defined in devfile.yaml") } - err = util.ValidateURL(manifestURL) + manifestBytes, err := util.LoadFileIntoMemory(manifestURL) if err != nil { s.End(false) - return errors.New(fmt.Sprintf("Invalid manifest url: %s, %s", manifestURL, err)) + return errors.Wrap(err, "unable to download manifest from URL specified in devfile") } + do.ManifestSource = manifestBytes - do.ManifestSource, err = util.DownloadFileInMemory(manifestURL) - if err != nil { - s.End(false) - return errors.New(fmt.Sprintf("Unable to download manifest: %s, %s", manifestURL, err)) + // check if manifestSource contains PORT template variable + // if it does, then check we have an port setup in env.yaml + do.DeploymentPort = 0 + if bytes.Contains(manifestBytes, []byte("PORT")) { + deploymentPort, err := do.EnvSpecificInfo.GetPortByURLKind(envinfo.ROUTE) + if err != nil { + s.End(false) + return errors.Wrap(err, "unable to find `port` for deployment. `odo url create` must be run prior to `odo deploy`") + } + do.DeploymentPort = deploymentPort } + s.End(true) return diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go index c824369afb1..01c0931ee25 100644 --- a/pkg/odo/cli/component/devfile.go +++ b/pkg/odo/cli/component/devfile.go @@ -175,6 +175,7 @@ func (do *DeployOptions) DevfileDeploy() (err error) { EnvSpecificInfo: *do.EnvSpecificInfo, Tag: do.tag, ManifestSource: do.ManifestSource, + DeploymentPort: do.DeploymentPort, } warnIfURLSInvalid(do.EnvSpecificInfo.GetURL()) diff --git a/pkg/util/util.go b/pkg/util/util.go index 86c458ee735..2a7bf6d3fd0 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -991,6 +991,54 @@ func DownloadFile(url string, filepath string) error { return nil } +// Load a file into memory (http(s):// or file://) +func LoadFileIntoMemory(URL string) (fileBytes []byte, err error) { + // check if we have a file url + if strings.HasPrefix(strings.ToLower(URL), "file://") { + // strip off the "file://" to get a local filepath + filepath := strings.Replace(URL, "file://", "", -1) + + // if filepath doesn't start with a "/"" then we have a relative + // filepath and will need to prepend the current working directory + if !strings.HasPrefix(filepath, "/") { + // get the current working directory + cwd, err := os.Getwd() + if err != nil { + return nil, errors.New("unable to determine current working directory") + } + // prepend the current working directory to the relatove filepath + filepath = fmt.Sprintf("%s/%s", cwd, filepath) + } + + // check to see if filepath exists + info, err := os.Stat(filepath) + if os.IsNotExist(err) || info.IsDir() { + return nil, errors.New(fmt.Sprintf("unable to read file: %s, %s", URL, err)) + } + + // read the bytes from the filepath + fileBytes, err = ioutil.ReadFile(filepath) + if err != nil { + return nil, errors.New(fmt.Sprintf("unable to read file: %s, %s", URL, err)) + } + + return fileBytes, nil + } else { + // assume we have an http:// or https:// url and validate it + err = ValidateURL(URL) + if err != nil { + return nil, errors.New(fmt.Sprintf("invalid url: %s, %s", URL, err)) + } + + // download the file and store the bytes + fileBytes, err = DownloadFileInMemory(URL) + if err != nil { + return nil, errors.New(fmt.Sprintf("unable to download url: %s, %s", URL, err)) + } + return fileBytes, nil + } +} + // DownloadFileInMemory uses the url to download the file and return bytes func DownloadFileInMemory(url string) ([]byte, error) { var httpClient = &http.Client{Timeout: HTTPRequestTimeout} @@ -1069,7 +1117,7 @@ func ValidateURL(sourceURL string) error { // This function could be expanded to be a more viable linter func ValidateDockerfile(contents []byte) error { if len(contents) == 0 { - return errors.New("aplha.build-dockerfile URL provided in the Devfile is referencing an empty file") + return errors.New("Dockerfile URL provided in the Devfile is referencing an empty file") } // Split the file downloaded line-by-line splitContents := strings.Split(string(contents), "\n") diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 09b078d3d6b..27e6c53de52 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "fmt" "io/ioutil" "net" @@ -1615,6 +1616,7 @@ func TestDownloadFileInMemory(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := DownloadFileInMemory(tt.url) + if tt.url != "invalid" && err != nil { t.Errorf("Failed to download file with error %s", err) } @@ -1626,6 +1628,72 @@ func TestDownloadFileInMemory(t *testing.T) { } } +func TestLoadFileIntoMemory(t *testing.T) { + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Send response to be tested + _, err := rw.Write([]byte("OK")) + if err != nil { + t.Error(err) + } + })) + + // Close the server when test finishes + defer server.Close() + + tests := []struct { + name string + url string + contains []byte + expectedError string + }{ + { + name: "Case 1: Input url is valid", + url: server.URL, + contains: []byte{79, 75}, + expectedError: "", + }, + { + name: "Case 2: Input url is invalid", + url: "invalid", + contains: []byte(nil), + expectedError: "invalid url:", + }, + { + name: "Case 3: Input http:// url doesnt exist", + url: "http://test.it.doesnt/exist/", + contains: []byte(nil), + expectedError: "unable to download url", + }, + { + name: "Case 4: Input file:// url doesnt exist", + url: "file://./notexists.txt", + contains: []byte(nil), + expectedError: "unable to read file", + }, + { + name: "Case 5: Input file://./util.go exists", + url: "file://./util.go", + contains: []byte("Load a file into memory ("), + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := LoadFileIntoMemory(tt.url) + + if err != nil && !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Got err: %s, expected err %s", err.Error(), tt.expectedError) + } + + if tt.expectedError == "" && !bytes.Contains(data, tt.contains) { + t.Errorf("Got: %w, should contain: %v", data, tt.contains) + } + }) + } +} + /* func TestGetGitHubZipURL(t *testing.T) { tests := []struct { diff --git a/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml b/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml index 22467e00533..dfd50887722 100644 --- a/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml +++ b/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml @@ -1,8 +1,7 @@ -schemaVersion: 2.0.0 +schemaVersion: 2.1.0 metadata: name: nodejs version: 1.0.0 - alpha.build-dockerfile: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/Dockerfile" alpha.deployment-manifest: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/deploy/k8s-deploy.yaml" projects: - name: express @@ -27,6 +26,13 @@ components: protocol: tcp scheme: http type: terminal + - dockerfile: + name: dockerfile-build + source: + sourceDir: "src" + location: "https://github.com/ranakan19/golang-ex.git" + dockerfilePath: "https://raw.githubusercontent.com/wtam2018/test/master/nodejs-dockerfiile" + destination: commands: - exec: id: download app dependencies diff --git a/tests/examples/source/devfilesV2.1.0/nodejs/devfile.yaml b/tests/examples/source/devfilesV2.1.0/nodejs/devfile.yaml deleted file mode 100644 index 45d64d09513..00000000000 --- a/tests/examples/source/devfilesV2.1.0/nodejs/devfile.yaml +++ /dev/null @@ -1,64 +0,0 @@ -schemaVersion: 2.1.0 -metadata: - name: nodejs - version: 1.0.0 - alpha.deployment-manifest: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml" -projects: - - name: nodejs-starter - git: - location: "https://github.com/odo-devfiles/nodejs-ex.git" -components: - - container: - name: runtime - image: registry.access.redhat.com/ubi8/nodejs-12:1-36 - memoryLimit: 1024Mi - mountSources: true - endpoints: - - name: http-3000 - targetPort: 3000 - configuration: - protocol: tcp - scheme: http - type: terminal - - - dockerfile: - name: dockerfile-build - source: - sourceDir: "src" - location: "https://github.com/ranakan19/golang-ex.git" - dockerfilePath: "https://raw.githubusercontent.com/wtam2018/test/master/nodejs-dockerfiile" - destination: - -commands: - - exec: - id: install - component: runtime - commandLine: npm install - workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter - group: - kind: build - isDefault: true - - exec: - id: run - component: runtime - commandLine: npm start - workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter - group: - kind: run - isDefault: true - - exec: - id: debug - component: runtime - commandLine: npm run debug - workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter - group: - kind: debug - isDefault: true - - exec: - id: test - component: runtime - commandLine: npm test - workingDir: ${CHE_PROJECTS_ROOT}/nodejs-starter - group: - kind: test - isDefault: true diff --git a/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml b/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml index 2e513b1bc13..fba185211ad 100644 --- a/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml +++ b/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml @@ -1,7 +1,7 @@ -schemaVersion: "2.0.0" +schemaVersion: "2.1.0" metadata: name: test-devfile - alpha.build-dockerfile: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/build/Dockerfile" + alpha.deployment-manifest: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml" projects: - name: nodejs-web-app git: @@ -23,6 +23,13 @@ components: protocol: tcp scheme: http type: terminal + - dockerfile: + name: dockerfile-build + source: + sourceDir: "src" + location: "https://github.com/ranakan19/golang-ex.git" + dockerfilePath: "https://raw.githubusercontent.com/wtam2018/test/master/nodejs-dockerfiile" + destination: commands: - exec: id: download dependencies diff --git a/tests/examples/source/devfilesV2/nodejs/devfile.yaml b/tests/examples/source/devfilesV2/nodejs/devfile.yaml index 69014f025cc..fba185211ad 100644 --- a/tests/examples/source/devfilesV2/nodejs/devfile.yaml +++ b/tests/examples/source/devfilesV2/nodejs/devfile.yaml @@ -1,7 +1,6 @@ -schemaVersion: "2.0.0" +schemaVersion: "2.1.0" metadata: name: test-devfile - alpha.build-dockerfile: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/build/Dockerfile" alpha.deployment-manifest: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml" projects: - name: nodejs-web-app @@ -24,6 +23,13 @@ components: protocol: tcp scheme: http type: terminal + - dockerfile: + name: dockerfile-build + source: + sourceDir: "src" + location: "https://github.com/ranakan19/golang-ex.git" + dockerfilePath: "https://raw.githubusercontent.com/wtam2018/test/master/nodejs-dockerfiile" + destination: commands: - exec: id: download dependencies diff --git a/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml b/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml new file mode 100644 index 00000000000..35a91c5d367 --- /dev/null +++ b/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml @@ -0,0 +1,60 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: COMPONENT_NAME + labels: + app.kubernetes.io/instance: COMPONENT_NAME + app.kubernetes.io/name: COMPONENT_NAME + app.kubernetes.io/part-of: COMPONENT_NAME +spec: + replicas: 1 + selector: + matchLabels: + app: COMPONENT_NAME + template: + metadata: + creationTimestamp: null + labels: + app: COMPONENT_NAME + spec: + containers: + - name: COMPONENT_NAME + image: CONTAINER_IMAGE + ports: + - name: http + containerPort: 3000 + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: COMPONENT_NAME +spec: + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + selector: + app: COMPONENT_NAME + type: ClusterIP + sessionAffinity: None +--- +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: COMPONENT_NAME + labels: + app.kubernetes.io/instance: COMPONENT_NAME + app.kubernetes.io/name: COMPONENT_NAME + app.kubernetes.io/part-of: COMPONENT_NAME + annotations: + openshift.io/host.generated: 'true' +spec: + to: + kind: Service + name: COMPONENT_NAME + weight: 100 + port: + targetPort: 3000 + wildcardPolicy: None diff --git a/tests/integration/devfile/cmd_devfile_deploy_test.go b/tests/integration/devfile/cmd_devfile_deploy_test.go index 56b2c512145..1a20ce843c6 100644 --- a/tests/integration/devfile/cmd_devfile_deploy_test.go +++ b/tests/integration/devfile/cmd_devfile_deploy_test.go @@ -48,7 +48,7 @@ var _ = Describe("odo devfile deploy command tests", func() { helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) helper.CmdShouldPass("odo", "url", "create", "--port", "3000") - helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2.1.0", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) output := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) cliRunner.WaitAndCheckForExistence("buildconfig", namespace, 1) Expect(output).NotTo(ContainSubstring("does not point to a valid Dockerfile")) @@ -61,7 +61,7 @@ var _ = Describe("odo devfile deploy command tests", func() { It("Should error out with 'URL does not point to a valid Dockerfile'", func() { helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) helper.CmdShouldPass("odo", "url", "create", "--port", "3000") - helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2.1.0", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) err := helper.ReplaceDevfileField("devfile.yaml", "dockerfilePath", "https://google.com") Expect(err).To(BeNil()) @@ -102,7 +102,7 @@ var _ = Describe("odo devfile deploy command tests", func() { Expect(err).To(BeNil()) cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) - Expect(cmdOutput).To(ContainSubstring("Invalid manifest url")) + Expect(cmdOutput).To(ContainSubstring("invalid url")) }) }) @@ -115,7 +115,33 @@ var _ = Describe("odo devfile deploy command tests", func() { Expect(err).To(BeNil()) cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) - Expect(cmdOutput).To(ContainSubstring("Unable to download manifest")) + Expect(cmdOutput).To(ContainSubstring("unable to download url")) + }) + }) + + Context("Verify error if no port is found in env.yaml", func() { + It("Should error out with 'Unable to find `port` for deployment.'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("Unable to find `port` for deployment.")) + }) + }) + + Context("Verify deploy completes when no port in env.yaml or PORT in manifest", func() { + It("Should successfully deploy the application and return a URL", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_no_port_substitution.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring(fmt.Sprintf("Successfully deployed component: http://%s-deploy-%s", cmpName, namespace))) }) }) @@ -125,7 +151,7 @@ var _ = Describe("odo devfile deploy command tests", func() { helper.CmdShouldPass("odo", "url", "create", "--port", "3000") helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", - "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml") + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) Expect(err).To(BeNil()) cmdOutput := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag)