github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/thirdparty/kyaml/runfn/runfn.go (about) 1 // Copyright 2019 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package runfn 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "os" 11 "os/user" 12 "path/filepath" 13 "strings" 14 15 "github.com/GoogleContainerTools/kpt/internal/pkg" 16 "github.com/GoogleContainerTools/kpt/pkg/printer" 17 "sigs.k8s.io/kustomize/kyaml/errors" 18 "sigs.k8s.io/kustomize/kyaml/filesys" 19 "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" 20 "sigs.k8s.io/kustomize/kyaml/kio" 21 "sigs.k8s.io/kustomize/kyaml/yaml" 22 23 "github.com/GoogleContainerTools/kpt/internal/fnruntime" 24 "github.com/GoogleContainerTools/kpt/internal/types" 25 "github.com/GoogleContainerTools/kpt/internal/util/printerutil" 26 fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1" 27 kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 28 ) 29 30 // RunFns runs the set of configuration functions in a local directory against 31 // the Resources in that directory 32 type RunFns struct { 33 Ctx context.Context 34 35 StorageMounts []runtimeutil.StorageMount 36 37 // Path is the path to the directory containing functions 38 Path string 39 40 // uniquePath is the absolute version of Path 41 uniquePath types.UniquePath 42 43 // FnConfigPath specifies a config file which contains the configs used in 44 // function input. It can be absolute or relative to kpt working directory. 45 // The exact format depends on the OS. 46 FnConfigPath string 47 48 // Function is an function to run against the input. 49 Function *runtimeutil.FunctionSpec 50 51 // FnConfig is the configurations passed from command line 52 FnConfig *yaml.RNode 53 54 // Input can be set to read the Resources from Input rather than from a directory 55 Input io.Reader 56 57 // Network enables network access for functions that declare it 58 Network bool 59 60 // Output can be set to write the result to Output rather than back to the directory 61 Output io.Writer 62 63 // ResultsDir is where to write each functions results 64 ResultsDir string 65 66 fnResults *fnresult.ResultList 67 68 // functionFilterProvider provides a filter to perform the function. 69 // this is a variable so it can be mocked in tests 70 functionFilterProvider func( 71 filter runtimeutil.FunctionSpec, fnConfig *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) 72 73 // AsCurrentUser is a boolean to indicate whether docker container should use 74 // the uid and gid that run the command 75 AsCurrentUser bool 76 77 // Env contains environment variables that will be exported to container 78 Env []string 79 80 // ContinueOnEmptyResult configures what happens when the underlying pipeline 81 // returns an empty result. 82 // If it is false (default), subsequent functions will be skipped and the 83 // result will be returned immediately. 84 // If it is true, the empty result will be provided as input to the next 85 // function in the list. 86 ContinueOnEmptyResult bool 87 88 RunnerOptions fnruntime.RunnerOptions 89 90 // ExecArgs are the arguments for exec commands 91 ExecArgs []string 92 93 // OriginalExec is the original exec commands 94 OriginalExec string 95 96 Selector kptfile.Selector 97 98 Exclusion kptfile.Selector 99 } 100 101 // Execute runs the command 102 func (r RunFns) Execute() error { 103 // default the containerFilterProvider if it hasn't been override. Split out for testing. 104 err := (&r).init() 105 if err != nil { 106 return err 107 } 108 nodes, fltrs, output, err := r.getNodesAndFilters() 109 if err != nil { 110 return err 111 } 112 return r.runFunctions(nodes, output, fltrs) 113 } 114 115 func (r RunFns) getNodesAndFilters() ( 116 *kio.PackageBuffer, []kio.Filter, *kio.LocalPackageReadWriter, error) { 117 // Read Resources from Directory or Input 118 buff := &kio.PackageBuffer{} 119 p := kio.Pipeline{Outputs: []kio.Writer{buff}} 120 // save the output dir because we will need it to write back 121 // the same one for reading must be used for writing if deleting Resources 122 var outputPkg *kio.LocalPackageReadWriter 123 124 if r.Path != "" { 125 outputPkg = &kio.LocalPackageReadWriter{ 126 PackagePath: string(r.uniquePath), 127 MatchFilesGlob: pkg.MatchAllKRM, 128 PreserveSeqIndent: true, 129 PackageFileName: kptfile.KptFileName, 130 IncludeSubpackages: true, 131 WrapBareSeqNode: true, 132 } 133 } 134 135 if r.Input == nil { 136 p.Inputs = []kio.Reader{outputPkg} 137 } else { 138 p.Inputs = []kio.Reader{&kio.ByteReader{Reader: r.Input, PreserveSeqIndent: true, WrapBareSeqNode: true}} 139 } 140 if err := p.Execute(); err != nil { 141 return nil, nil, outputPkg, err 142 } 143 144 fltrs, err := r.getFilters() 145 if err != nil { 146 return nil, nil, outputPkg, err 147 } 148 return buff, fltrs, outputPkg, nil 149 } 150 151 func (r RunFns) getFilters() ([]kio.Filter, error) { 152 spec := r.Function 153 if spec == nil { 154 return nil, nil 155 } 156 // merge envs from imperative and declarative 157 spec.Container.Env = r.mergeContainerEnv(spec.Container.Env) 158 159 c, err := r.functionFilterProvider(*spec, r.FnConfig, user.Current) 160 if err != nil { 161 return nil, err 162 } 163 164 if c == nil { 165 return nil, nil 166 } 167 return []kio.Filter{c}, nil 168 } 169 170 // runFunctions runs the fltrs against the input and writes to either r.Output or output 171 func (r RunFns) runFunctions(input kio.Reader, output kio.Writer, fltrs []kio.Filter) error { 172 // use the previously read Resources as input 173 var outputs []kio.Writer 174 if r.Output == nil { 175 // write back to the package 176 outputs = append(outputs, output) 177 } else { 178 // write to the output instead of the directory if r.Output is specified or 179 // the output is nil (reading from Input) 180 outputs = append(outputs, kio.ByteWriter{ 181 Writer: r.Output, 182 KeepReaderAnnotations: true, 183 WrappingKind: kio.ResourceListKind, 184 WrappingAPIVersion: kio.ResourceListAPIVersion, 185 }) 186 } 187 188 inputResources, err := input.Read() 189 if err != nil { 190 return err 191 } 192 193 selectedInput := inputResources 194 195 if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() { 196 err = fnruntime.SetResourceIds(inputResources) 197 if err != nil { 198 return err 199 } 200 201 // select the resources on which function should be applied 202 selectedInput, err = fnruntime.SelectInput( 203 inputResources, 204 []kptfile.Selector{r.Selector}, 205 []kptfile.Selector{r.Exclusion}, 206 &fnruntime.SelectionContext{RootPackagePath: r.uniquePath}) 207 if err != nil { 208 return err 209 } 210 } 211 212 pb := &kio.PackageBuffer{} 213 pipeline := kio.Pipeline{ 214 Inputs: []kio.Reader{&kio.PackageBuffer{Nodes: selectedInput}}, 215 Filters: fltrs, 216 Outputs: []kio.Writer{pb}, 217 ContinueOnEmptyResult: r.ContinueOnEmptyResult, 218 } 219 err = pipeline.Execute() 220 outputResources := pb.Nodes 221 222 if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() { 223 outputResources = fnruntime.MergeWithInput(pb.Nodes, selectedInput, inputResources) 224 deleteAnnoErr := fnruntime.DeleteResourceIds(outputResources) 225 if deleteAnnoErr != nil { 226 return deleteAnnoErr 227 } 228 } 229 230 if err == nil { 231 writeErr := outputs[0].Write(outputResources) 232 if writeErr != nil { 233 return writeErr 234 } 235 } 236 resultsFile, resultErr := fnruntime.SaveResults(filesys.FileSystemOrOnDisk{}, r.ResultsDir, r.fnResults) 237 if err != nil { 238 // function fails 239 if resultErr == nil { 240 r.printFnResultsStatus(resultsFile) 241 } 242 return err 243 } 244 if resultErr == nil { 245 r.printFnResultsStatus(resultsFile) 246 } 247 return nil 248 } 249 250 func (r RunFns) printFnResultsStatus(resultsFile string) { 251 printerutil.PrintFnResultInfo(r.Ctx, resultsFile, true) 252 } 253 254 // mergeContainerEnv will merge the envs specified by command line (imperative) and config 255 // file (declarative). If they have same key, the imperative value will be respected. 256 func (r RunFns) mergeContainerEnv(envs []string) []string { 257 imperative := fnruntime.NewContainerEnvFromStringSlice(r.Env) 258 declarative := fnruntime.NewContainerEnvFromStringSlice(envs) 259 for key, value := range imperative.EnvVars { 260 declarative.AddKeyValue(key, value) 261 } 262 263 for _, key := range imperative.VarsToExport { 264 declarative.AddKey(key) 265 } 266 267 return declarative.Raw() 268 } 269 270 // init initializes the RunFns with a containerFilterProvider. 271 func (r *RunFns) init() error { 272 // if no path is specified, default reading from stdin and writing to stdout 273 if r.Path == "" { 274 if r.Output == nil { 275 r.Output = printer.FromContextOrDie(r.Ctx).OutStream() 276 } 277 if r.Input == nil { 278 r.Input = os.Stdin 279 } 280 } else { 281 // make the path absolute so it works on mac 282 var err error 283 absPath, err := filepath.Abs(r.Path) 284 if err != nil { 285 return errors.Wrap(err) 286 } 287 r.uniquePath = types.UniquePath(absPath) 288 } 289 290 r.fnResults = fnresult.NewResultList() 291 292 // functionFilterProvider set the filter provider 293 if r.functionFilterProvider == nil { 294 r.functionFilterProvider = r.defaultFnFilterProvider 295 } 296 297 // fn config path should be absolute 298 if r.FnConfigPath != "" && !filepath.IsAbs(r.FnConfigPath) { 299 // if the FnConfigPath is relative, we should use the 300 // current directory to construct full path. 301 path, err := os.Getwd() 302 if err != nil { 303 return fmt.Errorf("failed to get working directory: %w", err) 304 } 305 r.FnConfigPath = filepath.Join(path, r.FnConfigPath) 306 } 307 return nil 308 } 309 310 type currentUserFunc func() (*user.User, error) 311 312 // getUIDGID will return "nobody" if asCurrentUser is false. Otherwise 313 // return "uid:gid" according to the return from currentUser function. 314 func getUIDGID(asCurrentUser bool, currentUser currentUserFunc) (string, error) { 315 if !asCurrentUser { 316 return "nobody", nil 317 } 318 319 u, err := currentUser() 320 if err != nil { 321 return "", err 322 } 323 return fmt.Sprintf("%s:%s", u.Uid, u.Gid), nil 324 } 325 326 // getFunctionConfig returns yaml representation of functionConfig that can 327 // be provided to a function as input. 328 func (r *RunFns) getFunctionConfig() (*yaml.RNode, error) { 329 return kptfile.GetValidatedFnConfigFromPath(filesys.FileSystemOrOnDisk{}, "", r.FnConfigPath) 330 } 331 332 // defaultFnFilterProvider provides function filters 333 func (r *RunFns) defaultFnFilterProvider(spec runtimeutil.FunctionSpec, fnConfig *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) { 334 if spec.Container.Image == "" && spec.Exec.Path == "" { 335 return nil, fmt.Errorf("either image name or executable path need to be provided") 336 } 337 338 var err error 339 if r.FnConfigPath != "" { 340 fnConfig, err = r.getFunctionConfig() 341 if err != nil { 342 return nil, err 343 } 344 } 345 fltr := &runtimeutil.FunctionFilter{ 346 FunctionConfig: fnConfig, 347 DeferFailure: spec.DeferFailure, 348 } 349 fnResult := &fnresult.Result{ 350 // TODO(droot): This is required for making structured results subpackage aware. 351 // Enable this once test harness supports filepath based assertions. 352 // Pkg: string(r.uniquePath), 353 } 354 if spec.Container.Image != "" { 355 fnResult.Image = spec.Container.Image 356 357 resolvedImage, err := r.RunnerOptions.ResolveToImage(context.TODO(), spec.Container.Image) 358 if err != nil { 359 return nil, err 360 } 361 // If AllowWasm is true, we try to use the image field as a wasm image. 362 // TODO: we can be smarter here. If the image doesn't support wasm/js platform, 363 // it should fallback to run it as container fn. 364 if r.RunnerOptions.AllowWasm { 365 wFn, err := fnruntime.NewWasmFn(fnruntime.NewOciLoader(filepath.Join(os.TempDir(), "kpt-fn-wasm"), resolvedImage)) 366 if err != nil { 367 return nil, err 368 } 369 fltr.Run = wFn.Run 370 } else { 371 // TODO: Add a test for this behavior 372 uidgid, err := getUIDGID(r.AsCurrentUser, currentUser) 373 if err != nil { 374 return nil, err 375 } 376 c := &fnruntime.ContainerFn{ 377 Image: resolvedImage, 378 ImagePullPolicy: r.RunnerOptions.ImagePullPolicy, 379 UIDGID: uidgid, 380 StorageMounts: r.StorageMounts, 381 Env: spec.Container.Env, 382 FnResult: fnResult, 383 Perm: fnruntime.ContainerFnPermission{ 384 AllowNetwork: r.Network, 385 // mounts are always from CLI flags so we allow 386 // them by default for eval 387 AllowMount: true, 388 }, 389 } 390 fltr.Run = c.Run 391 } 392 } 393 394 if spec.Exec.Path != "" { 395 fnResult.ExecPath = r.OriginalExec 396 397 if r.RunnerOptions.AllowWasm && strings.HasSuffix(spec.Exec.Path, ".wasm") { 398 wFn, err := fnruntime.NewWasmFn(&fnruntime.FsLoader{Filename: spec.Exec.Path}) 399 if err != nil { 400 return nil, err 401 } 402 fltr.Run = wFn.Run 403 } else { 404 e := &fnruntime.ExecFn{ 405 Path: spec.Exec.Path, 406 Args: r.ExecArgs, 407 FnResult: fnResult, 408 } 409 fltr.Run = e.Run 410 } 411 } 412 413 opts := r.RunnerOptions 414 if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() { 415 opts.DisplayResourceCount = true 416 } 417 418 return fnruntime.NewFunctionRunner(r.Ctx, fltr, "", fnResult, r.fnResults, opts) 419 }