github.com/oam-dev/kubevela@v1.9.11/pkg/velaql/view.go (about)

     1  /*
     2   Copyright 2021. The KubeVela Authors.
     3  
     4   Licensed under the Apache License, Version 2.0 (the "License");
     5   you may not use this file except in compliance with the License.
     6   You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10   Unless required by applicable law or agreed to in writing, software
    11   distributed under the License is distributed on an "AS IS" BASIS,
    12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   See the License for the specific language governing permissions and
    14   limitations under the License.
    15  */
    16  
    17  package velaql
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"strings"
    25  
    26  	"github.com/pkg/errors"
    27  	v1 "k8s.io/api/core/v1"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	pkgtypes "k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/client-go/rest"
    33  	"k8s.io/klog/v2"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  
    36  	monitorContext "github.com/kubevela/pkg/monitor/context"
    37  	workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
    38  	"github.com/kubevela/workflow/pkg/cue/model/value"
    39  	"github.com/kubevela/workflow/pkg/cue/packages"
    40  	"github.com/kubevela/workflow/pkg/executor"
    41  	"github.com/kubevela/workflow/pkg/generator"
    42  	"github.com/kubevela/workflow/pkg/providers"
    43  	"github.com/kubevela/workflow/pkg/providers/kube"
    44  	wfTypes "github.com/kubevela/workflow/pkg/types"
    45  
    46  	"github.com/oam-dev/kubevela/apis/types"
    47  	"github.com/oam-dev/kubevela/pkg/cue/process"
    48  	"github.com/oam-dev/kubevela/pkg/multicluster"
    49  	oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
    50  	"github.com/oam-dev/kubevela/pkg/stdlib"
    51  	"github.com/oam-dev/kubevela/pkg/utils"
    52  	"github.com/oam-dev/kubevela/pkg/utils/apply"
    53  	"github.com/oam-dev/kubevela/pkg/velaql/providers/query"
    54  	"github.com/oam-dev/kubevela/pkg/workflow/template"
    55  )
    56  
    57  func init() {
    58  	if err := stdlib.SetupBuiltinImports(); err != nil {
    59  		klog.ErrorS(err, "Unable to set up builtin imports on package initialization")
    60  		os.Exit(1)
    61  	}
    62  }
    63  
    64  const (
    65  	qlNs = "vela-system"
    66  
    67  	// ViewTaskPhaseSucceeded means view task run succeeded.
    68  	ViewTaskPhaseSucceeded = "succeeded"
    69  )
    70  
    71  // ViewHandler view handler
    72  type ViewHandler struct {
    73  	cli       client.Client
    74  	cfg       *rest.Config
    75  	viewTask  workflowv1alpha1.WorkflowStep
    76  	pd        *packages.PackageDiscover
    77  	namespace string
    78  }
    79  
    80  // NewViewHandler new view handler
    81  func NewViewHandler(cli client.Client, cfg *rest.Config, pd *packages.PackageDiscover) *ViewHandler {
    82  	return &ViewHandler{
    83  		cli:       cli,
    84  		cfg:       cfg,
    85  		pd:        pd,
    86  		namespace: qlNs,
    87  	}
    88  }
    89  
    90  // QueryView generate view step
    91  func (handler *ViewHandler) QueryView(ctx context.Context, qv QueryView) (*value.Value, error) {
    92  	outputsTemplate := fmt.Sprintf(OutputsTemplate, qv.Export, qv.Export)
    93  	queryKey := QueryParameterKey{}
    94  	if err := json.Unmarshal([]byte(outputsTemplate), &queryKey); err != nil {
    95  		return nil, errors.Errorf("unmarhsal query template: %v", err)
    96  	}
    97  
    98  	handler.viewTask = workflowv1alpha1.WorkflowStep{
    99  		WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
   100  			Name:       fmt.Sprintf("%s-%s", qv.View, qv.Export),
   101  			Type:       qv.View,
   102  			Properties: oamutil.Object2RawExtension(qv.Parameter),
   103  			Outputs:    queryKey.Outputs,
   104  		},
   105  	}
   106  
   107  	instance := &wfTypes.WorkflowInstance{
   108  		WorkflowMeta: wfTypes.WorkflowMeta{
   109  			Name: fmt.Sprintf("%s-%s", qv.View, qv.Export),
   110  		},
   111  		Steps: []workflowv1alpha1.WorkflowStep{
   112  			{
   113  				WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
   114  					Name:       fmt.Sprintf("%s-%s", qv.View, qv.Export),
   115  					Type:       qv.View,
   116  					Properties: oamutil.Object2RawExtension(qv.Parameter),
   117  					Outputs:    queryKey.Outputs,
   118  				},
   119  			},
   120  		},
   121  	}
   122  	executor.InitializeWorkflowInstance(instance)
   123  	handlerProviders := providers.NewProviders()
   124  	kube.Install(handlerProviders, handler.cli, nil, &kube.Handlers{
   125  		Apply:  handler.dispatch,
   126  		Delete: handler.delete,
   127  	})
   128  	query.Install(handlerProviders, handler.cli, handler.cfg)
   129  	loader := template.NewViewTemplateLoader(handler.cli, handler.namespace)
   130  	if len(strings.Split(qv.View, "\n")) > 2 {
   131  		loader = &template.EchoLoader{}
   132  	}
   133  	logCtx := monitorContext.NewTraceContext(ctx, "").AddTag("velaql")
   134  	runners, err := generator.GenerateRunners(logCtx, instance, wfTypes.StepGeneratorOptions{
   135  		Providers:       handlerProviders,
   136  		PackageDiscover: handler.pd,
   137  		ProcessCtx:      process.NewContext(process.ContextData{}),
   138  		TemplateLoader:  loader,
   139  		Client:          handler.cli,
   140  		LogLevel:        3,
   141  	})
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	viewCtx, err := NewViewContext()
   147  	if err != nil {
   148  		return nil, errors.Errorf("new view context: %v", err)
   149  	}
   150  	for _, runner := range runners {
   151  		status, _, err := runner.Run(viewCtx, &wfTypes.TaskRunOptions{})
   152  		if err != nil {
   153  			return nil, errors.Errorf("run query view: %v", err)
   154  		}
   155  		if string(status.Phase) != ViewTaskPhaseSucceeded {
   156  			return nil, errors.Errorf("failed to query the view %s %s", status.Message, status.Reason)
   157  		}
   158  	}
   159  	return viewCtx.GetVar(qv.Export)
   160  }
   161  
   162  func (handler *ViewHandler) dispatch(ctx context.Context, cluster string, _ string, manifests ...*unstructured.Unstructured) error {
   163  	ctx = multicluster.ContextWithClusterName(ctx, cluster)
   164  	applicator := apply.NewAPIApplicator(handler.cli)
   165  	for _, manifest := range manifests {
   166  		if err := applicator.Apply(ctx, manifest); err != nil {
   167  			return err
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func (handler *ViewHandler) delete(ctx context.Context, _ string, _ string, manifest *unstructured.Unstructured) error {
   174  	return handler.cli.Delete(ctx, manifest)
   175  }
   176  
   177  // ValidateView makes sure the cue provided can use as view.
   178  //
   179  // For now, we only check 1. cue is valid 2. `status` or `view` field exists
   180  func ValidateView(viewStr string) error {
   181  	val, err := value.NewValue(viewStr, nil, "")
   182  	if err != nil {
   183  		return errors.Errorf("error when parsing view: %v", err)
   184  	}
   185  
   186  	// Make sure `status` or `export` field exists
   187  	vStatus, errStatus := val.LookupValue(DefaultExportValue)
   188  	vExport, errExport := val.LookupValue(KeyWordExport)
   189  	if errStatus != nil && errExport != nil {
   190  		return errors.Errorf("no `status` or `export` field found in view: %v, %v", errStatus, errExport)
   191  	}
   192  	if errStatus == nil {
   193  		_, errStatus = vStatus.String()
   194  	}
   195  	if errExport == nil {
   196  		_, errExport = vExport.String()
   197  	}
   198  	if errStatus != nil && errExport != nil {
   199  		return errors.Errorf("connot get string from` status` or `export`: %v, %v", errStatus, errExport)
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  // ParseViewIntoConfigMap parses a CUE string (representing a view) into a ConfigMap
   206  // ready to be stored into etcd.
   207  func ParseViewIntoConfigMap(viewStr, name string) (*v1.ConfigMap, error) {
   208  	err := ValidateView(viewStr)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	cm := &v1.ConfigMap{
   214  		TypeMeta: metav1.TypeMeta{
   215  			APIVersion: "v1",
   216  			Kind:       "ConfigMap",
   217  		},
   218  		ObjectMeta: metav1.ObjectMeta{
   219  			Name:      name,
   220  			Namespace: types.DefaultKubeVelaNS,
   221  			// TODO(charlie0129): add a label to ConfigMap to identify itself as a view
   222  			// It is useful when searching for views through all other ConfigMaps (when listing views).
   223  		},
   224  		Data: map[string]string{
   225  			types.VelaQLConfigmapKey: viewStr,
   226  		},
   227  	}
   228  
   229  	return cm, nil
   230  }
   231  
   232  // StoreViewFromFile reads a view from the specified CUE file, and stores into a ConfigMap in vela-system namespace.
   233  // So the user can use the view in VelaQL later.
   234  //
   235  // By saying file, it can actually be a file, URL, or stdin (-).
   236  func StoreViewFromFile(ctx context.Context, c client.Client, path, viewName string) error {
   237  	content, err := utils.ReadRemoteOrLocalPath(path, false)
   238  	if err != nil {
   239  		return errors.Errorf("cannot load cue file: %v", err)
   240  	}
   241  
   242  	cm, err := ParseViewIntoConfigMap(string(content), viewName)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	// Create or Update ConfigMap
   248  	oldCm := cm.DeepCopy()
   249  	err = c.Get(ctx, pkgtypes.NamespacedName{
   250  		Namespace: oldCm.GetNamespace(),
   251  		Name:      oldCm.GetName(),
   252  	}, oldCm)
   253  
   254  	if err != nil {
   255  		// No previous ConfigMap found, create one.
   256  		if apierrors.IsNotFound(err) {
   257  			err = c.Create(ctx, cm)
   258  			if err != nil {
   259  				return errors.Errorf("cannot create ConfigMap %s: %v", viewName, err)
   260  			}
   261  			return nil
   262  		}
   263  		return err
   264  	}
   265  
   266  	// Previous ConfigMap found, update it.
   267  	if err = c.Update(ctx, cm); err != nil {
   268  		return errors.Errorf("cannot update ConfigMap %s: %v", viewName, err)
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  // QueryParameterKey query parameter key
   275  type QueryParameterKey struct {
   276  	Outputs workflowv1alpha1.StepOutputs `json:"outputs"`
   277  }
   278  
   279  // OutputsTemplate output template
   280  var OutputsTemplate = `
   281  {
   282      "outputs": [
   283          {
   284              "valueFrom": "%s",
   285              "name": "%s"
   286          }
   287      ]
   288  }
   289  `