Skip to content

Commit

Permalink
Add higher-level function in 'libdevfile' allowing to load component …
Browse files Browse the repository at this point in the history
…resource manifest and substitute variables

This works with both Inlined or Uri resources.

Notes:
- Ideally, it would make more sense to rely on the same logic used in Devfile to
  substitute variables (defined in the 'variables' package),
  but this function is not exported;
  and the exported ones substitute variables only in the URI name,
  not in the content itself (it is not fetched),
  which is actually the issue we are trying to solve here.
  • Loading branch information
rm3l committed May 2, 2022
1 parent aaf2f71 commit fc5aeb2
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 43 deletions.
4 changes: 2 additions & 2 deletions pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (o *deployHandler) ApplyImage(img v1alpha2.Component) error {
func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error {

// Validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster
_, err := service.ValidateResourceExist(o.kubeClient, kubernetes, o.path)
_, err := service.ValidateResourceExist(o.kubeClient, o.devfileObj, kubernetes, o.path)
if err != nil {
return err
}
Expand All @@ -75,7 +75,7 @@ func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error {
odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(o.devfileObj.Data.GetMetadata()))

// Get the Kubernetes component
u, err := libdevfile.GetK8sComponentAsUnstructured(kubernetes.Kubernetes, o.path, devfilefs.DefaultFs{})
u, err := libdevfile.GetK8sComponentAsUnstructured(o.devfileObj, kubernetes.Kubernetes, o.path, devfilefs.DefaultFs{})
if err != nil {
return err
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/devfile/adapters/kubernetes/component/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,13 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {
}

// validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster
err = service.ValidateResourcesExist(a.Client, k8sComponents, a.Context)
err = service.ValidateResourcesExist(a.Client, a.Devfile, k8sComponents, a.Context)
if err != nil {
return err
}

// create the Kubernetes objects from the manifest and delete the ones not in the devfile
err = service.PushKubernetesResources(a.Client, k8sComponents, labels, annotations, a.Context)
err = service.PushKubernetesResources(a.Client, a.Devfile, k8sComponents, labels, annotations, a.Context)
if err != nil {
return fmt.Errorf("failed to create service(s) associated with the component: %w", err)
}
Expand Down Expand Up @@ -246,14 +246,14 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) {

// Update all services with owner references
err = a.Client.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error {
return service.UpdateServicesWithOwnerReferences(a.Client, k8sComponents, ownerRef, a.Context)
return service.UpdateServicesWithOwnerReferences(a.Client, a.Devfile, k8sComponents, ownerRef, a.Context)
})
if err != nil {
return err
}

// create the Kubernetes objects from the manifest and delete the ones not in the devfile
needRestart, err := service.PushLinks(a.Client, k8sComponents, labels, a.deployment, a.Context)
needRestart, err := service.PushLinks(a.Client, a.Devfile, k8sComponents, labels, a.deployment, a.Context)
if err != nil {
return fmt.Errorf("failed to create service(s) associated with the component: %w", err)
}
Expand Down
17 changes: 7 additions & 10 deletions pkg/libdevfile/component_kubernetes_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@ import (
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
devfilefs "github.com/devfile/library/pkg/testingutil/filesystem"
"github.com/ghodss/yaml"
"github.com/redhat-developer/odo/pkg/util"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// GetK8sComponentAsUnstructured parses the Inlined/URI K8s of the devfile K8s component
func GetK8sComponentAsUnstructured(component *v1alpha2.KubernetesComponent, context string, fs devfilefs.Filesystem) (unstructured.Unstructured, error) {
strCRD := component.Inlined
var err error
if component.Uri != "" {
strCRD, err = util.GetDataFromURI(component.Uri, context, fs)
if err != nil {
return unstructured.Unstructured{}, err
}
func GetK8sComponentAsUnstructured(devfileObj parser.DevfileObj, component *v1alpha2.KubernetesComponent,
context string, fs devfilefs.Filesystem) (unstructured.Unstructured, error) {

strCRD, err := GetComponentResourceManifestContentWithVariablesResolved(devfileObj, component, context, fs)
if err != nil {
return unstructured.Unstructured{}, err
}

// convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource
Expand All @@ -40,7 +37,7 @@ func ListKubernetesComponents(devfileObj parser.DevfileObj, path string) (list [
var u unstructured.Unstructured
for _, kComponent := range components {
if kComponent.Kubernetes != nil {
u, err = GetK8sComponentAsUnstructured(kComponent.Kubernetes, path, devfilefs.DefaultFs{})
u, err = GetK8sComponentAsUnstructured(devfileObj, kComponent.Kubernetes, path, devfilefs.DefaultFs{})
if err != nil {
return
}
Expand Down
86 changes: 86 additions & 0 deletions pkg/libdevfile/libdevfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package libdevfile

import (
"fmt"
"regexp"
"strings"

"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/api/v2/pkg/validation/variables"
"github.com/devfile/library/pkg/devfile/parser"
"github.com/devfile/library/pkg/devfile/parser/data"
"github.com/devfile/library/pkg/devfile/parser/data/v2/common"
devfilefs "github.com/devfile/library/pkg/testingutil/filesystem"

"github.com/redhat-developer/odo/pkg/util"
)

type Handler interface {
Expand Down Expand Up @@ -193,3 +199,83 @@ func GetEndpointsFromDevfile(devfileObj parser.DevfileObj, ignoreExposures []v1a
}
return endpoints, nil
}

// GetComponentResourceManifestContentWithVariablesResolved returns the full content of either a Kubernetes or an Openshift
// Devfile component, either Inlined or referenced via a URI.
// No matter how the component is defined, it returns
// the content with all variables substituted using the global variables map defined in `devfileObj`.
// An error is returned if the content references an invalid variable key not defined in the Devfile object.
func GetComponentResourceManifestContentWithVariablesResolved(devfileObj parser.DevfileObj, devfileCmp interface{},
context string, fs devfilefs.Filesystem) (string, error) {

var content, uri string
switch devfileCmp := devfileCmp.(type) {
case v1alpha2.Component:
componentType, err := common.GetComponentType(devfileCmp)
if err != nil {
return "", err
}
switch componentType {
case v1alpha2.KubernetesComponentType:
return GetComponentResourceManifestContentWithVariablesResolved(devfileObj, devfileCmp.Kubernetes, context, fs)

case v1alpha2.OpenshiftComponentType:
return GetComponentResourceManifestContentWithVariablesResolved(devfileObj, devfileCmp.Openshift, context, fs)

default:
return "", fmt.Errorf("unexpected component type %s", componentType)
}
case *v1alpha2.KubernetesComponent:
content = devfileCmp.Inlined
if devfileCmp.Uri != "" {
uri = devfileCmp.Uri
}

case *v1alpha2.OpenshiftComponent:
content = devfileCmp.Inlined
if devfileCmp.Uri != "" {
uri = devfileCmp.Uri
}
default:
return "", fmt.Errorf("unexpected type for %v", devfileCmp)
}

if uri == "" {
return substituteVariables(devfileObj.Data.GetDevfileWorkspaceSpec().Variables, content)
}

return loadResourceManifestFromUriAndResolveVariables(devfileObj, uri, context, fs)
}

func loadResourceManifestFromUriAndResolveVariables(devfileObj parser.DevfileObj, uri string,
context string, fs devfilefs.Filesystem) (string, error) {
content, err := util.GetDataFromURI(uri, context, fs)
if err != nil {
return content, err
}
return substituteVariables(devfileObj.Data.GetDevfileWorkspaceSpec().Variables, content)
}

// substituteVariables validates the string for a global variable in the given `devfileObj` and replaces it.
// An error is returned if the string references an invalid variable key not defined in the Devfile object.
//
//Inspired from variables.validateAndReplaceDataWithVariable, which is unfortunately not exported
func substituteVariables(devfileVars map[string]string, val string) (string, error) {
// example of the regex: {{variable}} / {{ variable }}
matches := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`).FindAllStringSubmatch(val, -1)
var invalidKeys []string
for _, match := range matches {
varValue, ok := devfileVars[match[1]]
if !ok {
invalidKeys = append(invalidKeys, match[1])
} else {
val = strings.Replace(val, match[0], varValue, -1)
}
}

if len(invalidKeys) > 0 {
return val, &variables.InvalidKeysError{Keys: invalidKeys}
}

return val, nil
}
Loading

0 comments on commit fc5aeb2

Please sign in to comment.