diff --git a/commands/commands.go b/commands/commands.go index cc9cb0e53a..85d4cb5f8e 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -35,8 +35,8 @@ func GetKptCommands(ctx context.Context, name, version string) []*cobra.Command fnCmd := GetFnCommand(ctx, name) pkgCmd := GetPkgCommand(ctx, name) liveCmd := GetLiveCommand(ctx, name, version) - - c = append(c, pkgCmd, fnCmd, liveCmd) + editorCmd := EditorCommand(ctx, name) + c = append(c, pkgCmd, fnCmd, liveCmd, editorCmd) // apply cross-cutting issues to commands NormalizeCommand(c...) diff --git a/commands/editor.go b/commands/editor.go new file mode 100644 index 0000000000..bfd11d2b19 --- /dev/null +++ b/commands/editor.go @@ -0,0 +1,35 @@ +package commands + +import ( + "context" + + "github.com/GoogleContainerTools/kpt/internal/cad/function" + "github.com/GoogleContainerTools/kpt/internal/cad/resource" + "github.com/spf13/cobra" +) + +func EditorCommand(ctx context.Context, name string) *cobra.Command { + editor := &cobra.Command{ + Use: "editor", + Short: `Edit local package resources`, + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + } + editor.AddCommand(addCommand(ctx)) + return editor +} + +func addCommand(ctx context.Context) *cobra.Command { + adder := &cobra.Command{ + Use: "add", + Short: `Add a resource or a function to your package`, + Aliases: []string{"set"}, + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + } + adder.AddCommand(resource.NewAdd(ctx).Command) + adder.AddCommand(function.NewAdd(ctx).Command) + return adder +} diff --git a/internal/cad/consts.go b/internal/cad/consts.go new file mode 100644 index 0000000000..c058b3f6be --- /dev/null +++ b/internal/cad/consts.go @@ -0,0 +1,42 @@ +package cad + +const PlaceHolder = "example" + +var K8sResources = map[string][]string{ + "clusterrole": nil, + "clusterrolebinding": nil, + "configmap": nil, + "cronjob": nil, + "deployment": nil, + "ingress": nil, + "job": nil, + "namespace": nil, + "poddisruptionbudget": nil, + "priorityclass": nil, + "quota": []string{"--hard=cpu=1,memory=1G"}, + "role": nil, + "rolebinding": []string{"--clusterrole=admin", "--group=example-admins@example.com"}, + "secret": nil, + "service": nil, + "serviceaccount": nil, +} + +func ResourceKinds() []string { + var kinds []string + for k, _ := range K8sResources { + kinds = append(kinds, k) + } + return kinds +} + +var ResourceContextMap = map[string][]string{ + "namespace": []string{"rolebinding", "quota"}, +} + +func ResourceKindArgs(kind string) []string { + flags, ok := K8sResources[kind] + if !ok { + return nil + } + return flags +} diff --git a/internal/cad/function/cmdadd.go b/internal/cad/function/cmdadd.go new file mode 100644 index 0000000000..41c6dfc5d9 --- /dev/null +++ b/internal/cad/function/cmdadd.go @@ -0,0 +1,151 @@ +package function + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/GoogleContainerTools/kpt/internal/pkg" + "github.com/GoogleContainerTools/kpt/internal/printer" + "github.com/GoogleContainerTools/kpt/internal/util/argutil" + "github.com/GoogleContainerTools/kpt/internal/util/cmdutil" + fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1" + kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" + "github.com/google/shlex" + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" +) + +const gcloudName = "./gcloud-config.yaml" + +// This will be replaced by variant constructor +func (r *Setter) GetGcloudFnConfigPath() string { + return filepath.Join(r.Dest, gcloudName) +} + +// THis will be supported by variant constructor +var IncludeMetaResourcesFlag = true + +func NewAdd(ctx context.Context) *Setter { + r := &Setter{ctx: ctx} + c := &cobra.Command{ + Use: "fn [--validator=kubeval] [--mutator=set-namespace]", + Short: `Add KRM function mutators or validators to kpt hydration pipeline`, + Example: ` + # validate all resources by running kubeval as a container runtime. + $ kpt editor add fn --validator=kubeval +`, + PreRunE: r.preRunE, + RunE: r.runE, + } + + c.Flags().StringVarP(&r.validator, "validator", "v", "", "KRM validator function") + c.RegisterFlagCompletionFunc("validator", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cmdutil.FetchFunctionImages(), cobra.ShellCompDirectiveDefault + }) + c.Flags().StringVarP(&r.mutator, "mutator", "m", "", "KRM mutator function") + c.RegisterFlagCompletionFunc("mutator", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cmdutil.FetchFunctionImages(), cobra.ShellCompDirectiveDefault + }) + r.Command = c + return r +} + +type Setter struct { + validator string + mutator string + + // The kpt package directory + Dest string + kf *kptfile.KptFile + Command *cobra.Command + ctx context.Context + fnResults *fnresult.ResultList +} + +func (r *Setter) preRunE(c *cobra.Command, args []string) error { + if r.validator == "" && r.mutator == "" { + return fmt.Errorf("must specify a flag `mutator` or a `validator`") + } + if r.validator != "" && r.mutator != "" { + return fmt.Errorf("only accept one of `mutator` or `validator`") + } + if len(args) == 0 { + // no pkg path specified, default to current working dir + wd, err := os.Getwd() + if err != nil { + return err + } + r.Dest = wd + } else { + // resolve and validate the provided path + r.Dest = args[0] + } + var err error + r.Dest, err = argutil.ResolveSymlink(r.ctx, r.Dest) + if err != nil { + return err + } + r.kf, err = pkg.ReadKptfile(r.Dest) + if err != nil { + return err + } + return nil +} + +func (r *Setter) getFunctionSpec(execPath string) (*runtimeutil.FunctionSpec, []string, error) { + fn := &runtimeutil.FunctionSpec{} + var execArgs []string + s, err := shlex.Split(execPath) + if err != nil { + return nil, nil, fmt.Errorf("exec command %q must be valid: %w", execPath, err) + } + if len(s) > 0 { + fn.Exec.Path = s[0] + execArgs = s[1:] + } + return fn, execArgs, nil +} + +func (r *Setter) runE(c *cobra.Command, _ []string) error { + kptFile, err := pkg.ReadKptfile(r.Dest) + if err != nil { + return err + } + if kptFile.Pipeline == nil { + kptFile.Pipeline = &kptfile.Pipeline{} + } + if r.mutator != "" { + if kptFile.Pipeline.Mutators == nil { + kptFile.Pipeline.Mutators = []kptfile.Function{} + } else { + for _, m := range kptFile.Pipeline.Mutators { + if m.Name == r.mutator || m.Image == r.mutator { + return fmt.Errorf("mutator function already exists in %v/Kptfile", r.Dest) + } + } + } + newMutator := kptfile.Function{Image: r.mutator, ConfigPath: gcloudName} + kptFile.Pipeline.Mutators = append(kptFile.Pipeline.Mutators, newMutator) + } else { + if kptFile.Pipeline.Validators == nil { + kptFile.Pipeline.Validators = []kptfile.Function{} + } else { + for _, m := range kptFile.Pipeline.Validators { + if m.Name == r.validator || m.Image == r.validator { + return fmt.Errorf("validator function already exists in %v/Kptfile", r.Dest) + } + } + } + newValidator := kptfile.Function{Image: r.validator, ConfigPath: gcloudName} + kptFile.Pipeline.Validators = append(kptFile.Pipeline.Validators, newValidator) + } + if err = kptfileutil.WriteFile(r.Dest, kptFile); err != nil { + return err + } + pr := printer.FromContextOrDie(r.ctx) + pr.Printf("Kptfile is updated.\n") + return nil +} diff --git a/internal/cad/resource/cmdadd.go b/internal/cad/resource/cmdadd.go new file mode 100644 index 0000000000..7fb81c0d76 --- /dev/null +++ b/internal/cad/resource/cmdadd.go @@ -0,0 +1,323 @@ +package resource + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/GoogleContainerTools/kpt/internal/cad" + "github.com/GoogleContainerTools/kpt/internal/fnruntime" + "github.com/GoogleContainerTools/kpt/internal/pkg" + "github.com/GoogleContainerTools/kpt/internal/printer" + "github.com/GoogleContainerTools/kpt/internal/types" + "github.com/GoogleContainerTools/kpt/internal/util/argutil" + "github.com/GoogleContainerTools/kpt/internal/util/cmdutil" + fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1" + kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" + "github.com/GoogleContainerTools/kpt/thirdparty/cmdconfig/commands/runner" + "github.com/GoogleContainerTools/kpt/thirdparty/kyaml/runfn" + "github.com/google/shlex" + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const gcloudName = "./gcloud-config.yaml" + +// This will be replaced by variant constructor +func (r *Setter) GetGcloudFnConfigPath() string { + return filepath.Join(r.Dest, gcloudName) +} + +// THis will be supported by variant constructor +var IncludeMetaResourcesFlag = true + +func NewAdd(ctx context.Context) *Setter { + r := &Setter{ctx: ctx} + c := &cobra.Command{ + Use: "resource [--kind=namespace] [--context=false]", + Short: `Add the KRM resource(s) in the local package`, + Example: ` + # Set the package resources to the same namespace + $ kpt editor add resource --kind=namespace +`, + PreRunE: r.preRunE, + RunE: r.runE, + } + + c.Flags().StringVarP(&r.kind, "kind", "k", "", "Kubernetes core resource `Kind`") + c.RegisterFlagCompletionFunc("kind", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cad.ResourceKinds(), cobra.ShellCompDirectiveDefault + }) + + c.Flags().StringVarP(&r.context, "context", "c", "", "KRM resources correlated to existing `kind`") + c.RegisterFlagCompletionFunc("context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return r.GetContextFromSourceLocal(args), cobra.ShellCompDirectiveDefault + }) + r.Command = c + return r +} + +func (s *Setter) GetContextFromSourceLocal(args []string) []string { + var inputs []kio.Reader + matchFilesGlob := kio.MatchAll + if IncludeMetaResourcesFlag { + matchFilesGlob = append(matchFilesGlob, kptfile.KptFileName) + } + if len(args) == 0 { + // no pkg path specified, default to current working dir + wd, err := os.Getwd() + if err != nil { + // return err + } + s.Dest = wd + } else { + // resolve and validate the provided path + s.Dest = args[0] + } + var err error + s.Dest, err = argutil.ResolveSymlink(s.ctx, s.Dest) + if err != nil { + return nil + } + resolvedPath, err := argutil.ResolveSymlink(s.ctx, s.Dest) + if err != nil { + return nil + } + functionConfigFilter, err := pkg.FunctionConfigFilterFunc(types.UniquePath(resolvedPath), IncludeMetaResourcesFlag) + if err != nil { + return nil + } + inputs = append(inputs, kio.LocalPackageReader{ + PackagePath: resolvedPath, + MatchFilesGlob: matchFilesGlob, + FileSkipFunc: functionConfigFilter, + PreserveSeqIndent: true, + PackageFileName: kptfile.KptFileName, + IncludeSubpackages: true, + WrapBareSeqNode: true, + }) + var outputs []kio.Writer + var writer bytes.Buffer + outputs = append(outputs, kio.ByteWriter{ + Writer: &writer, + FunctionConfig: nil, + ClearAnnotations: []string{kioutil.IndexAnnotation, kioutil.PathAnnotation}, // nolint:staticcheck + }) + + resourceReader := &ResoureReader{} + err = kio.Pipeline{Inputs: inputs, Filters: []kio.Filter{resourceReader}, Outputs: outputs}.Execute() + if e := runner.HandleError(s.ctx, err); e != nil { + return nil + } + resourceFromCtx := []string{} + for _, r := range resourceReader.KindLists { + if rs, ok := cad.ResourceContextMap[r]; ok { + resourceFromCtx = append(resourceFromCtx, rs...) + } + } + return resourceFromCtx +} + +type ResoureReader struct { + KindLists []string +} + +func (r *ResoureReader) Filter(o []*yaml.RNode) ([]*yaml.RNode, error) { + for _, rn := range o { + r.KindLists = append(r.KindLists, strings.ToLower(rn.GetKind())) + } + return o, nil +} + +type Setter struct { + kind string + context string + + // The kpt package directory + Dest string + kf *kptfile.KptFile + Command *cobra.Command + ctx context.Context + fnResults *fnresult.ResultList +} + +func (r *Setter) preRunE(c *cobra.Command, args []string) error { + if r.kind == "" && r.context == "" { + return fmt.Errorf("must specify either `kind` or `context` flag") + } + if len(args) == 0 { + // no pkg path specified, default to current working dir + wd, err := os.Getwd() + if err != nil { + return err + } + r.Dest = wd + } else { + // resolve and validate the provided path + r.Dest = args[0] + } + var err error + r.Dest, err = argutil.ResolveSymlink(r.ctx, r.Dest) + if err != nil { + return err + } + r.kf, err = pkg.ReadKptfile(r.Dest) + if err != nil { + return err + } + return nil +} + +func (r *Setter) fromKubectlCreate(kind string) (string, error) { + var out, errout bytes.Buffer + args := []string{"create", kind, cad.PlaceHolder, "--dry-run=client", "-oyaml"} + flagArgs := cad.ResourceKindArgs(kind) + if flagArgs != nil { + args = append(args, flagArgs...) + } + cmd := exec.Command("kubectl", args...) + cmd.Stdout = &out + cmd.Stderr = &errout + err := cmd.Run() + if err != nil { + return "", err + } + if errout.String() != "" { + return "", fmt.Errorf(errout.String()) + } + return out.String(), nil +} + +func (r *Setter) getFunctionSpec(execPath string) (*runtimeutil.FunctionSpec, []string, error) { + fn := &runtimeutil.FunctionSpec{} + var execArgs []string + s, err := shlex.Split(execPath) + if err != nil { + return nil, nil, fmt.Errorf("exec command %q must be valid: %w", execPath, err) + } + if len(s) > 0 { + fn.Exec.Path = s[0] + execArgs = s[1:] + } + return fn, execArgs, nil +} + +/* +// GetExeFnsPath choose which kpt fn executable(s) to run for the given `Kind`. +// The mapping between fn and resource `kind` will be done by function/pkg discovery mechanism. +// TODO: run multiple fn execs in series (like render pipeline) +func GetExeFnsPath(kind string) string { + dir := os.Getenv(gitutil.RepoCacheDirEnv) + if dir != "" { + return dir + } + // cache location unspecified, use UserHomeDir/.kpt/repos + dir, _ = os.UserHomeDir() + execName, ok := cad.BuiltinTransformers[kind] + if !ok { + return "" + } + execPath := filepath.Join(dir, ".kpt", "fns", execName) + if _, err := os.Stat(execPath); errors.Is(err, os.ErrNotExist) { + return "" + } + return execPath +} +*/ + +func (r *Setter) runE(c *cobra.Command, args []string) error { + var kind string + if r.kind != "" { + kind = r.kind + } else { + kind = r.context + } + krmResources, err := r.fromKubectlCreate(kind) + if err != nil { + return err + } + var inputs []kio.Reader + if krmResources != "" { + reader := strings.NewReader(krmResources) + inputs = append(inputs, &kio.ByteReader{Reader: reader}) + } + matchFilesGlob := kio.MatchAll + if IncludeMetaResourcesFlag { + matchFilesGlob = append(matchFilesGlob, kptfile.KptFileName) + } + resolvedPath, err := argutil.ResolveSymlink(r.ctx, r.Dest) + if err != nil { + return err + } + functionConfigFilter, err := pkg.FunctionConfigFilterFunc(types.UniquePath(resolvedPath), IncludeMetaResourcesFlag) + if err != nil { + return err + } + + inputs = append(inputs, kio.LocalPackageReader{ + PackagePath: resolvedPath, + MatchFilesGlob: matchFilesGlob, + FileSkipFunc: functionConfigFilter, + PreserveSeqIndent: true, + PackageFileName: kptfile.KptFileName, + IncludeSubpackages: true, + WrapBareSeqNode: true, + }) + var outputs []kio.Writer + configs, err := kio.LocalPackageReader{PackagePath: r.GetGcloudFnConfigPath(), PreserveSeqIndent: true, WrapBareSeqNode: true}.Read() + if err != nil { + return err + } + if len(configs) != 1 { + return fmt.Errorf("expected exactly 1 functionConfig, found %d", len(configs)) + } + functionConfig := configs[0] + + outputs = append(outputs, kio.ByteWriter{ + Writer: printer.FromContextOrDie(r.ctx).OutStream(), + FunctionConfig: functionConfig, + ClearAnnotations: []string{kioutil.IndexAnnotation, kioutil.PathAnnotation}, // nolint:staticcheck + }) + var output io.Writer + OutContent := bytes.Buffer{} + output = &OutContent + + runFns := runfn.RunFns{ + Ctx: r.ctx, + /* + Function: fnSpec, + ExecArgs: execArgs, + OriginalExec: execPath, + + */ + Output: output, + Input: nil, + KIOReaders: inputs, + Path: r.Dest, + Network: false, + StorageMounts: nil, + ResultsDir: "", + Env: nil, + AsCurrentUser: false, + FnConfig: nil, + FnConfigPath: r.GetGcloudFnConfigPath(), + IncludeMetaResources: IncludeMetaResourcesFlag, + ImagePullPolicy: fnruntime.IfNotPresentPull, + ContinueOnEmptyResult: true, + Selector: kptfile.Selector{}, + } + + err = runner.HandleError(r.ctx, runFns.Execute()) + if err != nil { + return err + } + return cmdutil.WriteFnOutput(r.Dest, OutContent.String(), false, printer.FromContextOrDie(r.ctx).OutStream()) +} diff --git a/thirdparty/kyaml/runfn/runfn.go b/thirdparty/kyaml/runfn/runfn.go index 93e9d01c15..00731c12a1 100644 --- a/thirdparty/kyaml/runfn/runfn.go +++ b/thirdparty/kyaml/runfn/runfn.go @@ -50,7 +50,8 @@ type RunFns struct { FnConfig *yaml.RNode // Input can be set to read the Resources from Input rather than from a directory - Input io.Reader + Input io.Reader + KIOReaders []kio.Reader // Network enables network access for functions that declare it Network bool @@ -140,7 +141,9 @@ func (r RunFns) getNodesAndFilters() ( } } - if r.Input == nil { + if r.KIOReaders != nil { + p.Inputs = r.KIOReaders + } else if r.Input == nil { p.Inputs = []kio.Reader{outputPkg} } else { p.Inputs = []kio.Reader{&kio.ByteReader{Reader: r.Input, PreserveSeqIndent: true, WrapBareSeqNode: true}}