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 `