Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DevWorkspaces with parents #346

Merged
merged 12 commits into from
Mar 27, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,13 @@ func (r *DevWorkspaceReconciler) Reconcile(req ctrl.Request) (reconcileResult ct
timing.SetTime(timingInfo, timing.ComponentsCreated)
// TODO#185 : Temporarily do devfile flattening in main reconcile loop; this should be moved to a subcontroller.
flattenHelpers := flatten.ResolverTools{
InstanceNamespace: workspace.Namespace,
Context: ctx,
K8sClient: r.Client,
InternalRegistry: &registry.InternalRegistryImpl{},
HttpClient: http.DefaultClient,
DefaultNamespace: workspace.Namespace,
Context: ctx,
K8sClient: r.Client,
InternalRegistry: &registry.InternalRegistryImpl{},
HttpClient: http.DefaultClient,
}
flattenedWorkspace, err := flatten.ResolveDevWorkspace(workspace.Spec.Template, flattenHelpers)
flattenedWorkspace, err := flatten.ResolveDevWorkspace(&workspace.Spec.Template, flattenHelpers)
if err != nil {
reqLogger.Info("DevWorkspace start failed")
reconcileStatus.Phase = devworkspace.WorkspaceStatusFailed
Expand All @@ -186,7 +186,7 @@ func (r *DevWorkspaceReconciler) Reconcile(req ctrl.Request) (reconcileResult ct
}
}

devfilePodAdditions, err := containerlib.GetKubeContainersFromDevfile(workspace.Spec.Template)
devfilePodAdditions, err := containerlib.GetKubeContainersFromDevfile(&workspace.Spec.Template)
if err != nil {
reqLogger.Info("DevWorkspace start failed")
reconcileStatus.Phase = devworkspace.WorkspaceStatusFailed
Expand Down
28 changes: 14 additions & 14 deletions pkg/library/annotate/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,31 @@ import (
"github.com/devfile/api/v2/pkg/attributes"
)

// AddSourceAttributesForPlugin adds an attribute 'controller.devfile.io/imported-by=sourceID' to all elements of
// AddSourceAttributesForTemplate adds an attribute 'controller.devfile.io/imported-by=sourceID' to all elements of
// a plugin that support attributes.
func AddSourceAttributesForPlugin(sourceID string, plugin *dw.DevWorkspaceTemplateSpec) {
for idx, component := range plugin.Components {
func AddSourceAttributesForTemplate(sourceID string, template *dw.DevWorkspaceTemplateSpec) {
for idx, component := range template.Components {
if component.Attributes == nil {
plugin.Components[idx].Attributes = attributes.Attributes{}
template.Components[idx].Attributes = attributes.Attributes{}
}
plugin.Components[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
template.Components[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
}
for idx, command := range plugin.Commands {
for idx, command := range template.Commands {
if command.Attributes == nil {
plugin.Commands[idx].Attributes = attributes.Attributes{}
template.Commands[idx].Attributes = attributes.Attributes{}
}
plugin.Commands[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
template.Commands[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
}
for idx, project := range plugin.Projects {
for idx, project := range template.Projects {
if project.Attributes == nil {
plugin.Projects[idx].Attributes = attributes.Attributes{}
template.Projects[idx].Attributes = attributes.Attributes{}
}
plugin.Projects[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
template.Projects[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
}
for idx, project := range plugin.StarterProjects {
for idx, project := range template.StarterProjects {
if project.Attributes == nil {
plugin.Projects[idx].Attributes = attributes.Attributes{}
template.StarterProjects[idx].Attributes = attributes.Attributes{}
}
plugin.Projects[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
template.StarterProjects[idx].Attributes.PutString(PluginSourceAttribute, sourceID)
}
}
2 changes: 1 addition & 1 deletion pkg/library/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import (
// rewritten as Volumes are added to PodAdditions, in order to support e.g. using one PVC to hold all volumes
//
// Note: Requires DevWorkspace to be flattened (i.e. the DevWorkspace contains no Parent or Components of type Plugin)
func GetKubeContainersFromDevfile(workspace devworkspace.DevWorkspaceTemplateSpec) (*v1alpha1.PodAdditions, error) {
func GetKubeContainersFromDevfile(workspace *devworkspace.DevWorkspaceTemplateSpec) (*v1alpha1.PodAdditions, error) {
if !flatten.DevWorkspaceIsFlattened(workspace) {
return nil, fmt.Errorf("devfile is not flattened")
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/library/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import (
)

type testCase struct {
Name string `json:"name,omitempty"`
Input devworkspace.DevWorkspaceTemplateSpec `json:"input,omitempty"`
Output testOutput `json:"output,omitempty"`
Name string `json:"name,omitempty"`
Input *devworkspace.DevWorkspaceTemplateSpec `json:"input,omitempty"`
Output testOutput `json:"output,omitempty"`
}

type testOutput struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/library/flatten/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ package flatten

import devworkspace "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"

func DevWorkspaceIsFlattened(devworkspace devworkspace.DevWorkspaceTemplateSpec) bool {
func DevWorkspaceIsFlattened(devworkspace *devworkspace.DevWorkspaceTemplateSpec) bool {
if devworkspace.Parent != nil {
return false
}
Expand Down
153 changes: 108 additions & 45 deletions pkg/library/flatten/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,18 @@ import (
)

type ResolverTools struct {
InstanceNamespace string
Context context.Context
K8sClient client.Client
InternalRegistry registry.InternalRegistry
HttpClient network.HTTPGetter
DefaultNamespace string
Context context.Context
K8sClient client.Client
InternalRegistry registry.InternalRegistry
HttpClient network.HTTPGetter
}

// ResolveDevWorkspace takes a devworkspace and returns a "resolved" version of it -- i.e. one where all plugins and parents
// are inlined as components.
// TODO:
// - Implement flattening for DevWorkspace parents
// - Implement plugin references by ID and URI
func ResolveDevWorkspace(workspace devworkspace.DevWorkspaceTemplateSpec, tooling ResolverTools) (*devworkspace.DevWorkspaceTemplateSpec, error) {
func ResolveDevWorkspace(workspace *devworkspace.DevWorkspaceTemplateSpec, tooling ResolverTools) (*devworkspace.DevWorkspaceTemplateSpec, error) {
// Web terminals get default container components if they do not specify one
if err := web_terminal.AddDefaultContainerIfNeeded(&workspace); err != nil {
if err := web_terminal.AddDefaultContainerIfNeeded(workspace); err != nil {
return nil, err
}

Expand All @@ -56,15 +53,24 @@ func ResolveDevWorkspace(workspace devworkspace.DevWorkspaceTemplateSpec, toolin
return resolvedDW, nil
}

func recursiveResolve(workspace devworkspace.DevWorkspaceTemplateSpec, tooling ResolverTools, resolveCtx *resolutionContextTree) (*devworkspace.DevWorkspaceTemplateSpec, error) {
func recursiveResolve(workspace *devworkspace.DevWorkspaceTemplateSpec, tooling ResolverTools, resolveCtx *resolutionContextTree) (*devworkspace.DevWorkspaceTemplateSpec, error) {
if DevWorkspaceIsFlattened(workspace) {
return workspace.DeepCopy(), nil
}

resolvedParent := &devworkspace.DevWorkspaceTemplateSpecContent{}
if workspace.Parent != nil {
// TODO: Add support for flattening DevWorkspace parents
return nil, fmt.Errorf("DevWorkspace parent is unsupported")
resolvedParentSpec, err := resolveParentComponent(workspace.Parent, tooling)
if err != nil {
return nil, err
}
if !DevWorkspaceIsFlattened(resolvedParentSpec) {
// TODO: implemenent this
return nil, fmt.Errorf("parents containing plugins or parents are not supported")
}
annotate.AddSourceAttributesForTemplate("parent", resolvedParentSpec)
resolvedParent = &resolvedParentSpec.DevWorkspaceTemplateSpecContent
}

resolvedContent := &devworkspace.DevWorkspaceTemplateSpecContent{}
resolvedContent.Projects = workspace.Projects
resolvedContent.StarterProjects = workspace.StarterProjects
Expand All @@ -86,17 +92,17 @@ func recursiveResolve(workspace devworkspace.DevWorkspaceTemplateSpec, tooling R
return nil, err
}

resolvedPlugin, err := recursiveResolve(*pluginComponent, tooling, newCtx)
resolvedPlugin, err := recursiveResolve(pluginComponent, tooling, newCtx)
if err != nil {
return nil, err
}

annotate.AddSourceAttributesForPlugin(component.Name, resolvedPlugin)
annotate.AddSourceAttributesForTemplate(component.Name, resolvedPlugin)
pluginSpecContents = append(pluginSpecContents, &resolvedPlugin.DevWorkspaceTemplateSpecContent)
}
}

resolvedContent, err := overriding.MergeDevWorkspaceTemplateSpec(resolvedContent, nil, pluginSpecContents...)
resolvedContent, err := overriding.MergeDevWorkspaceTemplateSpec(resolvedContent, resolvedParent, pluginSpecContents...)
if err != nil {
return nil, fmt.Errorf("failed to merge DevWorkspace parents/plugins: %w", err)
}
Expand All @@ -106,22 +112,49 @@ func recursiveResolve(workspace devworkspace.DevWorkspaceTemplateSpec, tooling R
}, nil
}

// resolveParentComponent resolves the parent DevWorkspaceTemplateSpec that a parent reference refers to.
func resolveParentComponent(parent *devworkspace.Parent, tools ResolverTools) (resolvedParent *devworkspace.DevWorkspaceTemplateSpec, err error) {
switch {
case parent.Kubernetes != nil:
// Search in default namespace if namespace ref is unset
if parent.Kubernetes.Namespace == "" {
parent.Kubernetes.Namespace = tools.DefaultNamespace
}
resolvedParent, err = resolveElementByKubernetesImport("parent", parent.Kubernetes, tools)
case parent.Uri != "":
resolvedParent, err = resolveElementByURI("parent", parent.Uri, tools)
case parent.Id != "":
resolvedParent, err = resolveElementById("parent", parent.Id, parent.RegistryUrl, tools)
default:
err = fmt.Errorf("devfile parent does not define any resources")
}
if err != nil {
return nil, err
}
if parent.Components != nil || parent.Commands != nil || parent.Projects != nil || parent.StarterProjects != nil {
overrideSpec, err := overriding.OverrideDevWorkspaceTemplateSpec(&resolvedParent.DevWorkspaceTemplateSpecContent, parent.ParentOverrides)

if err != nil {
return nil, err
}
resolvedParent.DevWorkspaceTemplateSpecContent = *overrideSpec
}
return resolvedParent, nil
}

// resolvePluginComponent resolves the DevWorkspaceTemplateSpec that a plugin component refers to. The name parameter is
// used to construct meaningful error messages (e.g. issue resolving plugin 'name')
func resolvePluginComponent(
name string,
plugin *devworkspace.PluginComponent,
tooling ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {
tools ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {
switch {
// TODO: Add support for plugin ID and URI
case plugin.Kubernetes != nil:
// Search in devworkspace's namespace if namespace ref is unset
if plugin.Kubernetes.Namespace == "" {
plugin.Kubernetes.Namespace = tooling.InstanceNamespace
}
resolvedPlugin, err = resolvePluginComponentByKubernetesReference(name, plugin, tooling)
resolvedPlugin, err = resolveElementByKubernetesImport(name, plugin.Kubernetes, tools)
case plugin.Uri != "":
resolvedPlugin, err = resolvePluginComponentByURI(name, plugin, tooling)
resolvedPlugin, err = resolveElementByURI(name, plugin.Uri, tools)
case plugin.Id != "":
resolvedPlugin, err = resolvePluginComponentById(name, plugin, tooling)
resolvedPlugin, err = resolveElementById(name, plugin.Id, plugin.RegistryUrl, tools)
default:
err = fmt.Errorf("plugin %s does not define any resources", name)
}
Expand All @@ -143,17 +176,32 @@ func resolvePluginComponent(
return resolvedPlugin, nil
}

func resolvePluginComponentByKubernetesReference(
// resolveElementByKubernetesImport resolves a plugin specified by a Kubernetes reference.
// The name parameter is used to construct meaningful error messages (e.g. issue resolving plugin 'name')
func resolveElementByKubernetesImport(
name string,
plugin *devworkspace.PluginComponent,
tooling ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {
kubeReference *devworkspace.KubernetesCustomResourceImportReference,
tools ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {

if tools.K8sClient == nil {
return nil, fmt.Errorf("cannot resolve resources by kubernetes reference: no kubernetes client provided")
}

// Search in default namespace if namespace ref is unset
namespace := kubeReference.Namespace
if namespace == "" {
if tools.DefaultNamespace == "" {
return nil, fmt.Errorf("'%s' specifies a kubernetes reference without namespace and a default is not provided", name)
}
namespace = tools.DefaultNamespace
}

var dwTemplate devworkspace.DevWorkspaceTemplate
namespacedName := types.NamespacedName{
Name: plugin.Kubernetes.Name,
Namespace: plugin.Kubernetes.Namespace,
Name: kubeReference.Name,
Namespace: namespace,
}
err = tooling.K8sClient.Get(tooling.Context, namespacedName, &dwTemplate)
err = tools.K8sClient.Get(tools.Context, namespacedName, &dwTemplate)
if err != nil {
if errors.IsNotFound(err) {
return nil, fmt.Errorf("plugin for component %s not found", name)
Expand All @@ -163,47 +211,62 @@ func resolvePluginComponentByKubernetesReference(
return &dwTemplate.Spec, nil
}

func resolvePluginComponentById(
// resolveElementById resolves a component specified by ID and registry URL. The name parameter is used to
// construct meaningful error messages (e.g. issue resolving plugin 'name'). When registry URL is empty,
// the DefaultRegistryURL from tools is used.
func resolveElementById(
name string,
plugin *devworkspace.PluginComponent,
id string,
registryUrl string,
tools ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {

// Check internal registry for plugins that do not specify a registry
if plugin.RegistryUrl == "" {
if registryUrl == "" {
if tools.InternalRegistry == nil {
return nil, fmt.Errorf("plugin %s does not specify a registryUrl and no internal registry is configured", name)
}
if !tools.InternalRegistry.IsInInternalRegistry(plugin.Id) {
if !tools.InternalRegistry.IsInInternalRegistry(id) {
return nil, fmt.Errorf("plugin for component %s does not specify a registry and is not present in the internal registry", name)
}
pluginDWT, err := tools.InternalRegistry.ReadPluginFromInternalRegistry(plugin.Id)
pluginDWT, err := tools.InternalRegistry.ReadPluginFromInternalRegistry(id)
if err != nil {
return nil, fmt.Errorf("failed to read plugin for component %s from internal registry: %w", name, err)
}
return &pluginDWT.Spec, nil

}

pluginURL, err := url.Parse(plugin.RegistryUrl)
if tools.HttpClient == nil {
return nil, fmt.Errorf("cannot resolve resources by id: no HTTP client provided")
}

pluginURL, err := url.Parse(registryUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse registry URL for plugin %s: %w", name, err)
return nil, fmt.Errorf("failed to parse registry URL for component %s: %w", name, err)
}
pluginURL.Path = path.Join(pluginURL.Path, "plugins", plugin.Id)
pluginURL.Path = path.Join(pluginURL.Path, id)

dwt, err := network.FetchDevWorkspaceTemplate(pluginURL.String(), tools.HttpClient)
if err != nil {
return nil, fmt.Errorf("failed to resolve plugin %s from registry %s: %w", name, plugin.RegistryUrl, err)
return nil, fmt.Errorf("failed to resolve component %s from registry %s: %w", name, registryUrl, err)
}
return dwt, nil
}

func resolvePluginComponentByURI(
// resolveElementByURI resolves a plugin defined by URI. The name parameter is used to construct meaningful
// error messages (e.g. issue resolving plugin 'name')
func resolveElementByURI(
name string,
plugin *devworkspace.PluginComponent,
uri string,
tools ResolverTools) (resolvedPlugin *devworkspace.DevWorkspaceTemplateSpec, err error) {

dwt, err := network.FetchDevWorkspaceTemplate(plugin.Uri, tools.HttpClient)
if tools.HttpClient == nil {
return nil, fmt.Errorf("cannot resolve resources by id: no HTTP client provided")
}

dwt, err := network.FetchDevWorkspaceTemplate(uri, tools.HttpClient)
if err != nil {
return nil, fmt.Errorf("failed to resolve plugin %s by URI: %w", name, err)
return nil, fmt.Errorf("failed to resolve component %s by URI: %w", name, err)
}
return dwt, nil
}
Loading