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 }