github.com/oam-dev/kubevela@v1.9.11/references/cli/velaql.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 cli
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"regexp"
    25  	"strings"
    26  
    27  	"github.com/spf13/cobra"
    28  
    29  	"github.com/kubevela/workflow/pkg/cue/model/value"
    30  
    31  	"github.com/oam-dev/kubevela/apis/types"
    32  	"github.com/oam-dev/kubevela/pkg/utils"
    33  	"github.com/oam-dev/kubevela/pkg/utils/common"
    34  	"github.com/oam-dev/kubevela/pkg/utils/util"
    35  	"github.com/oam-dev/kubevela/pkg/velaql"
    36  	querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types"
    37  )
    38  
    39  // Filter filter options
    40  type Filter struct {
    41  	Component        string
    42  	Cluster          string
    43  	ClusterNamespace string
    44  }
    45  
    46  const (
    47  	// ViewNamingRegex is a regex for names of view, essentially allowing something like `some-name-123`
    48  	ViewNamingRegex = `^[a-z\d]+(-[a-z\d]+)*$`
    49  )
    50  
    51  // NewQlCommand creates `ql` command for executing velaQL
    52  func NewQlCommand(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command {
    53  	var cueFile, querySts string
    54  	ctx := context.Background()
    55  	cmd := &cobra.Command{
    56  		Use:   "ql",
    57  		Short: "Show result of executing velaQL.",
    58  		Long: `Show result of executing velaQL, use it like:
    59  		vela ql --query "inner-view-name{param1=value1,param2=value2}"
    60  		vela ql --file ./ql.cue`,
    61  		Example: `  Users can query with a query statement:
    62  		vela ql --query "inner-view-name{param1=value1,param2=value2}"
    63  
    64    Query by a ql file:
    65  		vela ql --file ./ql.cue
    66    Query by a ql file from remote url:
    67  		vela ql --file https://my.host.to.cue/ql.cue
    68    Query by a ql file from stdin:
    69  		cat ./ql.cue | vela ql --file -
    70  
    71  Example content of ql.cue:
    72  ---
    73  import (
    74  	"vela/ql"
    75  )
    76  configmap: ql.#Read & {
    77     value: {
    78        kind: "ConfigMap"
    79        apiVersion: "v1"
    80        metadata: {
    81          name: "mycm"
    82        }
    83     }
    84  }
    85  status: configmap.value.data.key
    86  
    87  export: "status"
    88  ---
    89  `,
    90  		RunE: func(cmd *cobra.Command, args []string) error {
    91  			if cueFile == "" && querySts == "" && len(args) == 0 {
    92  				return fmt.Errorf("please specify at least one VelaQL statement or VelaQL file path")
    93  			}
    94  
    95  			if cueFile != "" {
    96  				return queryFromView(ctx, c, cueFile, cmd)
    97  			}
    98  			if querySts == "" {
    99  				// for compatibility
   100  				querySts = args[0]
   101  			}
   102  			return queryFromStatement(ctx, c, querySts, cmd)
   103  		},
   104  		Annotations: map[string]string{
   105  			types.TagCommandOrder: order,
   106  			types.TagCommandType:  types.TypeAuxiliary,
   107  		},
   108  	}
   109  	cmd.Flags().StringVarP(&cueFile, "file", "f", "", "The CUE file path for VelaQL, it could be a remote url.")
   110  	cmd.Flags().StringVarP(&querySts, "query", "q", "", "The query statement for VelaQL.")
   111  	cmd.SetOut(ioStreams.Out)
   112  
   113  	// Add subcommands like `create`, to `vela ql`
   114  	cmd.AddCommand(NewQLApplyCommand(c))
   115  	// TODO(charlie0129): add `vela ql delete` command to delete created views (ConfigMaps)
   116  	// TODO(charlie0129): add `vela ql list` command to list user-created views (and views installed from addons, if that's feasible)
   117  
   118  	return cmd
   119  }
   120  
   121  // NewQLApplyCommand creates a VelaQL view
   122  func NewQLApplyCommand(c common.Args) *cobra.Command {
   123  	var (
   124  		viewFile string
   125  	)
   126  	cmd := &cobra.Command{
   127  		Use:   "apply [view-name]",
   128  		Short: "Create and store a VelaQL view",
   129  		Long: `Create and store a VelaQL view to reuse it later.
   130  
   131  You can specify your view file from:
   132  	- a file (-f my-view.cue)
   133  	- a URL (-f https://example.com/view.cue)
   134  	- stdin (-f -)
   135  
   136  View name can be automatically inferred from file/URL.
   137  If we cannot infer a name from it, you must explicitly specify the view name (see examples).
   138  
   139  If a view with the same name already exists, it will be updated.`,
   140  		Example: `Assume your VelaQL view is stored in <my-view.cue>.
   141  
   142  View name will be implicitly inferred from file name or URL (my-view):
   143  	vela ql create -f my-view.cue
   144  
   145  You can also explicitly specify view name (custom-name):
   146  	vela ql create custom-name -f my-view.cue
   147  
   148  If view name cannot be inferred, or you are reading from stdin (-f -), you must explicitly specify view name:
   149  	cat my-view.cue | vela ql create custom-name -f -`,
   150  		RunE: func(cmd *cobra.Command, args []string) error {
   151  			var viewName string
   152  
   153  			if viewFile == "" {
   154  				return fmt.Errorf("no cue file provided")
   155  			}
   156  
   157  			// If a view name is provided by the user,
   158  			// we will use it instead of inferring from file name.
   159  			if len(args) == 1 {
   160  				viewName = args[0]
   161  			} else if viewFile != "-" {
   162  				// If the user doesn't provide a name, but a file/URL is provided,
   163  				// try to get the file name of .cue file/URL provided.
   164  				n, err := utils.GetFilenameFromLocalOrRemote(viewFile)
   165  				if err != nil {
   166  					return fmt.Errorf("cannot get filename from %s: %w", viewFile, err)
   167  				}
   168  				viewName = n
   169  			}
   170  
   171  			// In case we can't infer a view name from file/URL,
   172  			// and the user didn't provide a view name,
   173  			// we can't continue.
   174  			if viewName == "" {
   175  				return fmt.Errorf("no view name provided or cannot inferr view name from file")
   176  			}
   177  
   178  			// Just do some name checks, following a typical convention.
   179  			// In case the inferred/user-provided name have some problems.
   180  			re := regexp.MustCompile(ViewNamingRegex)
   181  			if !re.MatchString(viewName) {
   182  				return fmt.Errorf("view name should only cocntain lowercase letters, dashes, and numbers, but received: %s", viewName)
   183  			}
   184  
   185  			k8sClient, err := c.GetClient()
   186  			if err != nil {
   187  				return err
   188  			}
   189  
   190  			return velaql.StoreViewFromFile(context.Background(), k8sClient, viewFile, viewName)
   191  		},
   192  	}
   193  
   194  	flag := cmd.Flags()
   195  	flag.StringVarP(&viewFile, "file", "f", "", "CUE file that stores the view, can be local path, URL, or stdin (-)")
   196  
   197  	return cmd
   198  }
   199  
   200  // queryFromStatement print velaQL result from query statement with inner query view
   201  func queryFromStatement(ctx context.Context, velaC common.Args, velaQLStatement string, cmd *cobra.Command) error {
   202  	queryView, err := velaql.ParseVelaQL(velaQLStatement)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	queryValue, err := QueryValue(ctx, velaC, &queryView)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	return printValue(queryValue, cmd)
   211  }
   212  
   213  // queryFromView print velaQL result from query view
   214  func queryFromView(ctx context.Context, velaC common.Args, velaQLViewPath string, cmd *cobra.Command) error {
   215  	queryView, err := velaql.ParseVelaQLFromPath(velaQLViewPath)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	queryValue, err := QueryValue(ctx, velaC, queryView)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	return printValue(queryValue, cmd)
   224  }
   225  
   226  func printValue(queryValue *value.Value, cmd *cobra.Command) error {
   227  	response, err := queryValue.CueValue().MarshalJSON()
   228  	if err != nil {
   229  		return err
   230  	}
   231  	var out bytes.Buffer
   232  	err = json.Indent(&out, response, "", "  ")
   233  	if err != nil {
   234  		return err
   235  	}
   236  	cmd.Println(strings.Trim(strings.TrimSpace(out.String()), "\""))
   237  	return nil
   238  }
   239  
   240  // MakeVelaQL build velaQL
   241  func MakeVelaQL(view string, params map[string]string, action string) string {
   242  	var paramString string
   243  	for k, v := range params {
   244  		if paramString != "" {
   245  			paramString = fmt.Sprintf("%s, %s=%s", paramString, k, v)
   246  		} else {
   247  			paramString = fmt.Sprintf("%s=%s", k, v)
   248  		}
   249  	}
   250  	return fmt.Sprintf("%s{%s}.%s", view, paramString, action)
   251  }
   252  
   253  // GetServiceEndpoints get service endpoints by velaQL
   254  func GetServiceEndpoints(ctx context.Context, appName string, namespace string, velaC common.Args, f Filter) ([]querytypes.ServiceEndpoint, error) {
   255  	params := map[string]string{
   256  		"appName": appName,
   257  		"appNs":   namespace,
   258  	}
   259  	setFilterParams(f, params)
   260  
   261  	velaQL := MakeVelaQL("service-endpoints-view", params, "status")
   262  	queryView, err := velaql.ParseVelaQL(velaQL)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	queryValue, err := QueryValue(ctx, velaC, &queryView)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	var response = struct {
   271  		Endpoints []querytypes.ServiceEndpoint `json:"endpoints"`
   272  		Error     string                       `json:"error"`
   273  	}{}
   274  	if err := queryValue.UnmarshalTo(&response); err != nil {
   275  		return nil, err
   276  	}
   277  	if response.Error != "" {
   278  		return nil, fmt.Errorf(response.Error)
   279  	}
   280  	return response.Endpoints, nil
   281  }
   282  
   283  // GetApplicationPods get the pods by velaQL
   284  func GetApplicationPods(ctx context.Context, appName string, namespace string, velaC common.Args, f Filter) ([]querytypes.PodBase, error) {
   285  	params := map[string]string{
   286  		"appName": appName,
   287  		"appNs":   namespace,
   288  	}
   289  	setFilterParams(f, params)
   290  
   291  	velaQL := MakeVelaQL("component-pod-view", params, "status")
   292  	queryView, err := velaql.ParseVelaQL(velaQL)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	queryValue, err := QueryValue(ctx, velaC, &queryView)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	var response = struct {
   301  		Pods  []querytypes.PodBase `json:"podList"`
   302  		Error string               `json:"error"`
   303  	}{}
   304  	if err := queryValue.UnmarshalTo(&response); err != nil {
   305  		return nil, err
   306  	}
   307  	if response.Error != "" {
   308  		return nil, fmt.Errorf(response.Error)
   309  	}
   310  	return response.Pods, nil
   311  }
   312  
   313  // GetApplicationServices get the services by velaQL
   314  func GetApplicationServices(ctx context.Context, appName string, namespace string, velaC common.Args, f Filter) ([]querytypes.ResourceItem, error) {
   315  	params := map[string]string{
   316  		"appName": appName,
   317  		"appNs":   namespace,
   318  	}
   319  	setFilterParams(f, params)
   320  	velaQL := MakeVelaQL("component-service-view", params, "status")
   321  	queryView, err := velaql.ParseVelaQL(velaQL)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	queryValue, err := QueryValue(ctx, velaC, &queryView)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	var response = struct {
   330  		Services []querytypes.ResourceItem `json:"services"`
   331  		Error    string                    `json:"error"`
   332  	}{}
   333  	if err := queryValue.UnmarshalTo(&response); err != nil {
   334  		return nil, err
   335  	}
   336  	if response.Error != "" {
   337  		return nil, fmt.Errorf(response.Error)
   338  	}
   339  	return response.Services, nil
   340  }
   341  
   342  // setFilterParams will convert Filter fields to velaQL params
   343  func setFilterParams(f Filter, params map[string]string) {
   344  	if f.Component != "" {
   345  		params["name"] = f.Component
   346  	}
   347  	if f.Cluster != "" {
   348  		params["cluster"] = f.Cluster
   349  	}
   350  	if f.ClusterNamespace != "" {
   351  		params["clusterNs"] = f.ClusterNamespace
   352  	}
   353  
   354  }
   355  
   356  // QueryValue get queryValue from velaQL
   357  func QueryValue(ctx context.Context, velaC common.Args, queryView *velaql.QueryView) (*value.Value, error) {
   358  	pd, err := velaC.GetPackageDiscover()
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	config, err := velaC.GetConfig()
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	client, err := velaC.GetClient()
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	queryValue, err := velaql.NewViewHandler(client, config, pd).QueryView(ctx, *queryView)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	return queryValue, nil
   375  }