From 68ada607a98eeba46217192c088b776b5e9be3ae Mon Sep 17 00:00:00 2001 From: Kashish Chaudhary Date: Mon, 12 Aug 2024 09:00:20 +0000 Subject: [PATCH 1/4] cherry-picking diags code from master --- cmd/operator/operator.go | 29 + deploy/crds/portworx.io_portworxdiags.yaml | 131 ++ .../portworx/component/portworx_basic.go | 5 + drivers/storage/portworx/portworx.go | 4 +- drivers/storage/portworx/portworx_test.go | 4 +- .../testspec/portworxClusterRole.yaml | 3 + drivers/storage/portworx/util/util.go | 87 + go.mod | 4 + go.sum | 4 + pkg/apis/addtoscheme_portworx_v1.go | 10 + .../portworxdiag/controller_test.go | 1182 +++++++++++ pkg/controller/portworxdiag/matchers_test.go | 52 + pkg/controller/portworxdiag/pod.go | 115 ++ pkg/controller/portworxdiag/portworxdiag.go | 823 ++++++++ pkg/mock/sdkserver.go | 1 + tools.go | 2 + vendor/github.com/gobuffalo/flect/.gitignore | 29 + .../gobuffalo/flect/.gometalinter.json | 3 + vendor/github.com/gobuffalo/flect/LICENSE | 21 + vendor/github.com/gobuffalo/flect/Makefile | 61 + vendor/github.com/gobuffalo/flect/README.md | 36 + .../github.com/gobuffalo/flect/SHOULDERS.md | 10 + vendor/github.com/gobuffalo/flect/acronyms.go | 152 ++ vendor/github.com/gobuffalo/flect/camelize.go | 44 + .../github.com/gobuffalo/flect/capitalize.go | 24 + .../github.com/gobuffalo/flect/custom_data.go | 88 + .../github.com/gobuffalo/flect/dasherize.go | 34 + vendor/github.com/gobuffalo/flect/flect.go | 43 + vendor/github.com/gobuffalo/flect/humanize.go | 35 + vendor/github.com/gobuffalo/flect/ident.go | 122 ++ .../github.com/gobuffalo/flect/lower_upper.go | 13 + .../github.com/gobuffalo/flect/ordinalize.go | 43 + .../github.com/gobuffalo/flect/pascalize.go | 32 + .../gobuffalo/flect/plural_rules.go | 286 +++ .../github.com/gobuffalo/flect/pluralize.go | 62 + vendor/github.com/gobuffalo/flect/rule.go | 10 + .../gobuffalo/flect/singular_rules.go | 23 + .../github.com/gobuffalo/flect/singularize.go | 61 + vendor/github.com/gobuffalo/flect/titleize.go | 30 + .../github.com/gobuffalo/flect/underscore.go | 35 + vendor/github.com/gobuffalo/flect/version.go | 4 + .../inconshreveable/mousetrap/LICENSE | 201 ++ .../inconshreveable/mousetrap/README.md | 23 + .../inconshreveable/mousetrap/trap_others.go | 16 + .../inconshreveable/mousetrap/trap_windows.go | 42 + vendor/github.com/spf13/cobra/.gitignore | 39 + vendor/github.com/spf13/cobra/.golangci.yml | 62 + vendor/github.com/spf13/cobra/.mailmap | 3 + vendor/github.com/spf13/cobra/CONDUCT.md | 37 + vendor/github.com/spf13/cobra/CONTRIBUTING.md | 50 + vendor/github.com/spf13/cobra/LICENSE.txt | 174 ++ vendor/github.com/spf13/cobra/MAINTAINERS | 13 + vendor/github.com/spf13/cobra/Makefile | 35 + vendor/github.com/spf13/cobra/README.md | 112 + vendor/github.com/spf13/cobra/active_help.go | 63 + vendor/github.com/spf13/cobra/active_help.md | 157 ++ vendor/github.com/spf13/cobra/args.go | 131 ++ .../spf13/cobra/bash_completions.go | 712 +++++++ .../spf13/cobra/bash_completions.md | 93 + .../spf13/cobra/bash_completionsV2.go | 396 ++++ vendor/github.com/spf13/cobra/cobra.go | 239 +++ vendor/github.com/spf13/cobra/command.go | 1834 +++++++++++++++++ .../github.com/spf13/cobra/command_notwin.go | 20 + vendor/github.com/spf13/cobra/command_win.go | 41 + vendor/github.com/spf13/cobra/completions.go | 878 ++++++++ .../spf13/cobra/fish_completions.go | 292 +++ .../spf13/cobra/fish_completions.md | 4 + vendor/github.com/spf13/cobra/flag_groups.go | 224 ++ .../spf13/cobra/powershell_completions.go | 325 +++ .../spf13/cobra/powershell_completions.md | 3 + .../spf13/cobra/projects_using_cobra.md | 64 + .../spf13/cobra/shell_completions.go | 98 + .../spf13/cobra/shell_completions.md | 576 ++++++ vendor/github.com/spf13/cobra/user_guide.md | 726 +++++++ .../github.com/spf13/cobra/zsh_completions.go | 308 +++ .../github.com/spf13/cobra/zsh_completions.md | 48 + vendor/modules.txt | 25 + vendor/sigs.k8s.io/controller-tools/LICENSE | 201 ++ .../cmd/controller-gen/main.go | 263 +++ .../controller-tools/pkg/crd/conv.go | 43 + .../controller-tools/pkg/crd/desc_visitor.go | 78 + .../controller-tools/pkg/crd/doc.go | 63 + .../controller-tools/pkg/crd/flatten.go | 445 ++++ .../controller-tools/pkg/crd/gen.go | 307 +++ .../controller-tools/pkg/crd/known_types.go | 180 ++ .../controller-tools/pkg/crd/markers/crd.go | 387 ++++ .../controller-tools/pkg/crd/markers/doc.go | 46 + .../pkg/crd/markers/package.go | 40 + .../pkg/crd/markers/register.go | 83 + .../pkg/crd/markers/topology.go | 163 ++ .../pkg/crd/markers/validation.go | 520 +++++ .../crd/markers/zz_generated.markerhelp.go | 525 +++++ .../controller-tools/pkg/crd/parser.go | 243 +++ .../controller-tools/pkg/crd/schema.go | 464 +++++ .../pkg/crd/schema_visitor.go | 131 ++ .../controller-tools/pkg/crd/spec.go | 178 ++ .../pkg/crd/zz_generated.markerhelp.go | 66 + .../controller-tools/pkg/deepcopy/doc.go | 23 + .../controller-tools/pkg/deepcopy/gen.go | 305 +++ .../controller-tools/pkg/deepcopy/traverse.go | 829 ++++++++ .../pkg/deepcopy/zz_generated.markerhelp.go | 46 + .../controller-tools/pkg/genall/doc.go | 58 + .../controller-tools/pkg/genall/genall.go | 269 +++ .../controller-tools/pkg/genall/help/doc.go | 23 + .../pkg/genall/help/pretty/doc.go | 30 + .../pkg/genall/help/pretty/help.go | 171 ++ .../pkg/genall/help/pretty/print.go | 304 +++ .../pkg/genall/help/pretty/table.go | 64 + .../controller-tools/pkg/genall/help/sort.go | 106 + .../controller-tools/pkg/genall/help/types.go | 215 ++ .../controller-tools/pkg/genall/input.go | 37 + .../controller-tools/pkg/genall/options.go | 194 ++ .../controller-tools/pkg/genall/output.go | 160 ++ .../pkg/genall/zz_generated.markerhelp.go | 90 + .../controller-tools/pkg/loader/doc.go | 60 + .../controller-tools/pkg/loader/errors.go | 67 + .../controller-tools/pkg/loader/loader.go | 629 ++++++ .../controller-tools/pkg/loader/paths.go | 32 + .../controller-tools/pkg/loader/refs.go | 273 +++ .../controller-tools/pkg/loader/visit.go | 81 + .../controller-tools/pkg/markers/collect.go | 422 ++++ .../controller-tools/pkg/markers/doc.go | 113 + .../controller-tools/pkg/markers/help.go | 81 + .../controller-tools/pkg/markers/parse.go | 963 +++++++++ .../controller-tools/pkg/markers/reg.go | 154 ++ .../controller-tools/pkg/markers/regutil.go | 36 + .../controller-tools/pkg/markers/zip.go | 201 ++ .../controller-tools/pkg/rbac/parser.go | 267 +++ .../pkg/rbac/zz_generated.markerhelp.go | 86 + .../controller-tools/pkg/schemapatcher/gen.go | 433 ++++ .../schemapatcher/internal/yaml/convert.go | 61 + .../pkg/schemapatcher/internal/yaml/nested.go | 87 + .../pkg/schemapatcher/internal/yaml/set.go | 80 + .../schemapatcher/zz_generated.markerhelp.go | 50 + .../controller-tools/pkg/version/version.go | 49 + .../controller-tools/pkg/webhook/parser.go | 431 ++++ .../pkg/webhook/zz_generated.markerhelp.go | 110 + 137 files changed, 23933 insertions(+), 4 deletions(-) create mode 100644 deploy/crds/portworx.io_portworxdiags.yaml create mode 100644 pkg/apis/addtoscheme_portworx_v1.go create mode 100644 pkg/controller/portworxdiag/controller_test.go create mode 100644 pkg/controller/portworxdiag/matchers_test.go create mode 100644 pkg/controller/portworxdiag/pod.go create mode 100644 pkg/controller/portworxdiag/portworxdiag.go create mode 100644 vendor/github.com/gobuffalo/flect/.gitignore create mode 100644 vendor/github.com/gobuffalo/flect/.gometalinter.json create mode 100644 vendor/github.com/gobuffalo/flect/LICENSE create mode 100644 vendor/github.com/gobuffalo/flect/Makefile create mode 100644 vendor/github.com/gobuffalo/flect/README.md create mode 100644 vendor/github.com/gobuffalo/flect/SHOULDERS.md create mode 100644 vendor/github.com/gobuffalo/flect/acronyms.go create mode 100644 vendor/github.com/gobuffalo/flect/camelize.go create mode 100644 vendor/github.com/gobuffalo/flect/capitalize.go create mode 100644 vendor/github.com/gobuffalo/flect/custom_data.go create mode 100644 vendor/github.com/gobuffalo/flect/dasherize.go create mode 100644 vendor/github.com/gobuffalo/flect/flect.go create mode 100644 vendor/github.com/gobuffalo/flect/humanize.go create mode 100644 vendor/github.com/gobuffalo/flect/ident.go create mode 100644 vendor/github.com/gobuffalo/flect/lower_upper.go create mode 100644 vendor/github.com/gobuffalo/flect/ordinalize.go create mode 100644 vendor/github.com/gobuffalo/flect/pascalize.go create mode 100644 vendor/github.com/gobuffalo/flect/plural_rules.go create mode 100644 vendor/github.com/gobuffalo/flect/pluralize.go create mode 100644 vendor/github.com/gobuffalo/flect/rule.go create mode 100644 vendor/github.com/gobuffalo/flect/singular_rules.go create mode 100644 vendor/github.com/gobuffalo/flect/singularize.go create mode 100644 vendor/github.com/gobuffalo/flect/titleize.go create mode 100644 vendor/github.com/gobuffalo/flect/underscore.go create mode 100644 vendor/github.com/gobuffalo/flect/version.go create mode 100644 vendor/github.com/inconshreveable/mousetrap/LICENSE create mode 100644 vendor/github.com/inconshreveable/mousetrap/README.md create mode 100644 vendor/github.com/inconshreveable/mousetrap/trap_others.go create mode 100644 vendor/github.com/inconshreveable/mousetrap/trap_windows.go create mode 100644 vendor/github.com/spf13/cobra/.gitignore create mode 100644 vendor/github.com/spf13/cobra/.golangci.yml create mode 100644 vendor/github.com/spf13/cobra/.mailmap create mode 100644 vendor/github.com/spf13/cobra/CONDUCT.md create mode 100644 vendor/github.com/spf13/cobra/CONTRIBUTING.md create mode 100644 vendor/github.com/spf13/cobra/LICENSE.txt create mode 100644 vendor/github.com/spf13/cobra/MAINTAINERS create mode 100644 vendor/github.com/spf13/cobra/Makefile create mode 100644 vendor/github.com/spf13/cobra/README.md create mode 100644 vendor/github.com/spf13/cobra/active_help.go create mode 100644 vendor/github.com/spf13/cobra/active_help.md create mode 100644 vendor/github.com/spf13/cobra/args.go create mode 100644 vendor/github.com/spf13/cobra/bash_completions.go create mode 100644 vendor/github.com/spf13/cobra/bash_completions.md create mode 100644 vendor/github.com/spf13/cobra/bash_completionsV2.go create mode 100644 vendor/github.com/spf13/cobra/cobra.go create mode 100644 vendor/github.com/spf13/cobra/command.go create mode 100644 vendor/github.com/spf13/cobra/command_notwin.go create mode 100644 vendor/github.com/spf13/cobra/command_win.go create mode 100644 vendor/github.com/spf13/cobra/completions.go create mode 100644 vendor/github.com/spf13/cobra/fish_completions.go create mode 100644 vendor/github.com/spf13/cobra/fish_completions.md create mode 100644 vendor/github.com/spf13/cobra/flag_groups.go create mode 100644 vendor/github.com/spf13/cobra/powershell_completions.go create mode 100644 vendor/github.com/spf13/cobra/powershell_completions.md create mode 100644 vendor/github.com/spf13/cobra/projects_using_cobra.md create mode 100644 vendor/github.com/spf13/cobra/shell_completions.go create mode 100644 vendor/github.com/spf13/cobra/shell_completions.md create mode 100644 vendor/github.com/spf13/cobra/user_guide.md create mode 100644 vendor/github.com/spf13/cobra/zsh_completions.go create mode 100644 vendor/github.com/spf13/cobra/zsh_completions.md create mode 100644 vendor/sigs.k8s.io/controller-tools/LICENSE create mode 100644 vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/conv.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/desc_visitor.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/flatten.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/gen.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/known_types.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/crd.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/package.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/register.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/topology.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/validation.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/parser.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/schema.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/schema_visitor.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/spec.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/crd/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/gen.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/traverse.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/genall.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/help.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/print.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/table.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/sort.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/help/types.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/input.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/options.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/output.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/genall/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/errors.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/loader.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/paths.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/refs.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/loader/visit.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/collect.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/doc.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/help.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/parse.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/reg.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/regutil.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/markers/zip.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/rbac/parser.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/rbac/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/gen.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/convert.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/nested.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/set.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/zz_generated.markerhelp.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/version/version.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/webhook/parser.go create mode 100644 vendor/sigs.k8s.io/controller-tools/pkg/webhook/zz_generated.markerhelp.go diff --git a/cmd/operator/operator.go b/cmd/operator/operator.go index 0e52e3e58f..410682c7e6 100644 --- a/cmd/operator/operator.go +++ b/cmd/operator/operator.go @@ -32,6 +32,7 @@ import ( "github.com/libopenstorage/operator/drivers/storage" _ "github.com/libopenstorage/operator/drivers/storage/portworx" "github.com/libopenstorage/operator/pkg/apis" + "github.com/libopenstorage/operator/pkg/controller/portworxdiag" "github.com/libopenstorage/operator/pkg/controller/storagecluster" "github.com/libopenstorage/operator/pkg/controller/storagenode" _ "github.com/libopenstorage/operator/pkg/log" @@ -52,6 +53,7 @@ const ( flagEnableProfiling = "pprof" flagDisableCacheFor = "disable-cache-for" defaultLockObjectName = "openstorage-operator" + flagEnableDiagController = "diag-controller" defaultResyncPeriod = 30 * time.Second defaultMetricsPort = 8999 defaultPprofPort = 6060 @@ -110,6 +112,10 @@ func main() { Name: flagEnableProfiling, Usage: "Enable Portworx Operator profiling using pprof (default: false)", }, + cli.BoolFlag{ + Name: flagEnableDiagController, + Usage: "Enable Portworx Diag Controller (default: false)", + }, cli.StringFlag{ Name: flagDisableCacheFor, Usage: "Comma separated object types to disable from cache to reduce memory usage, for example \"Pod,ConfigMap,Deployment,PersistentVolume\"", @@ -146,6 +152,8 @@ func run(c *cli.Context) { }() } + diagControllerEnabled := c.Bool(flagEnableDiagController) + config, err := rest.InClusterConfig() if err != nil { log.Fatalf("Error getting cluster config: %v", err) @@ -183,6 +191,15 @@ func run(c *cli.Context) { log.Fatalf("Error registering CRD's for StorageNode controller: %v", err) } + var diagController portworxdiag.Controller + if diagControllerEnabled { + diagController = portworxdiag.Controller{Driver: d} + err = diagController.RegisterCRD() + if err != nil { + log.Fatalf("Error registering CRDs for PortworxDiag controller: %v", err) + } + } + // TODO: Don't move createManager above register CRD section. This part will be refactored because of a bug, // similar to https://github.com/kubernetes-sigs/controller-runtime/issues/321 mgr, err := createManager(c, config) @@ -256,6 +273,12 @@ func run(c *cli.Context) { log.Fatalf("Error initializing storage node controller: %v", err) } + if diagControllerEnabled { + if err := diagController.Init(mgr); err != nil { + log.Fatalf("Error initializing portworx diag controller: %v", err) + } + } + if err := storageClusterController.StartWatch(); err != nil { log.Fatalf("Error start watch on storage cluster controller: %v", err) } @@ -264,6 +287,12 @@ func run(c *cli.Context) { log.Fatalf("Error starting watch on storage node controller: %v", err) } + if diagControllerEnabled { + if err := diagController.StartWatch(); err != nil { + log.Fatalf("Error starting watch on portworx diag controller: %v", err) + } + } + if c.BoolT(flagMigration) { log.Info("Migration is enabled") migrationHandler := migration.New(&storageClusterController) diff --git a/deploy/crds/portworx.io_portworxdiags.yaml b/deploy/crds/portworx.io_portworxdiags.yaml new file mode 100644 index 0000000000..3658cd2fb7 --- /dev/null +++ b/deploy/crds/portworx.io_portworxdiags.yaml @@ -0,0 +1,131 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: portworxdiags.portworx.io +spec: + group: portworx.io + names: + kind: PortworxDiag + listKind: PortworxDiagList + plural: portworxdiags + shortNames: + - pxdiag + singular: portworxdiag + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status of the Portworx diag collection. + jsonPath: .status.phase + name: Status + type: string + - description: Age of the diag resource. + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: PortworxDiag represents a portworx diag + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PortworxDiagSpec is the spec used to define a portworx diag. + properties: + portworx: + description: Configuration for diags collection of the main Portworx + component. + properties: + generateCore: + description: Generates the core dump as well when collecting the + diags. Could be useful to analyze the current state of the system. + type: boolean + nodes: + description: Nodes for which the diags need to be collected. If + a volume selector is also specified, then both the selectors + will be honored and the selected nodes will be a union of both + selectors. + properties: + all: + description: Select all nodes in the Portworx cluster. If + set to true, other selectors are ignored. + type: boolean + ids: + description: Ids of the nodes to be selected. + items: + type: string + type: array + labels: + additionalProperties: + type: string + description: Labels of the volumes to be selected. + type: object + type: object + volumes: + description: Volumes for which the diags need to be collected. + properties: + ids: + description: Ids of the volumes to be selected. + items: + type: string + type: array + labels: + additionalProperties: + type: string + description: Labels of the volumes to be selected. + type: object + type: object + type: object + type: object + status: + description: PortworxDiagStatus is the status of a portworx diag. + properties: + clusterUuid: + description: UUID of the Portworx cluster. This is useful to find + the uploaded diags. + type: string + message: + description: Optional message used to give the reason for any failure. + type: string + nodes: + description: Status of the diags collection from all the selected + nodes. + items: + description: Status of the diags collection from a single node. + properties: + message: + description: Optional message used to give the reason for any + failure. + type: string + nodeId: + description: ID of the node for which the diag status is reported. + type: string + status: + description: One word status of the diags collection on the + node. + type: string + type: object + type: array + phase: + description: One word status of the entire diags collection job. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/drivers/storage/portworx/component/portworx_basic.go b/drivers/storage/portworx/component/portworx_basic.go index e7eebcc430..2b12ddaca9 100644 --- a/drivers/storage/portworx/component/portworx_basic.go +++ b/drivers/storage/portworx/component/portworx_basic.go @@ -399,6 +399,11 @@ func (c *portworxBasic) createClusterRole() error { Resources: []string{"*"}, Verbs: []string{"*"}, }, + { + APIGroups: []string{"portworx.io"}, + Resources: []string{"portworxdiags", "portworxdiags/status"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, { APIGroups: []string{"security.openshift.io"}, Resources: []string{"securitycontextconstraints"}, diff --git a/drivers/storage/portworx/portworx.go b/drivers/storage/portworx/portworx.go index 10e3032b44..72690757a4 100644 --- a/drivers/storage/portworx/portworx.go +++ b/drivers/storage/portworx/portworx.go @@ -35,7 +35,7 @@ import ( const ( storkDriverName = "pxd" - defaultPortworxImage = "portworx/oci-monitor" + DefaultPortworxImage = "portworx/oci-monitor" defaultSecretsProvider = "k8s" defaultTokenLifetime = "24h" defaultSelfSignedIssuer = "operator.portworx.io" @@ -375,7 +375,7 @@ func (p *portworx) SetDefaultsOnStorageCluster(toUpdate *corev1.StorageCluster) if toUpdate.Spec.Version == "" && pxEnabled { if toUpdate.Spec.Image == "" { - toUpdate.Spec.Image = defaultPortworxImage + toUpdate.Spec.Image = DefaultPortworxImage } toUpdate.Spec.Image = toUpdate.Spec.Image + ":" + release.PortworxVersion toUpdate.Spec.Version = release.PortworxVersion diff --git a/drivers/storage/portworx/portworx_test.go b/drivers/storage/portworx/portworx_test.go index fba09f93bb..9d4cf1adc3 100644 --- a/drivers/storage/portworx/portworx_test.go +++ b/drivers/storage/portworx/portworx_test.go @@ -1295,7 +1295,7 @@ func TestSetDefaultsOnStorageCluster(t *testing.T) { require.NoError(t, err) // Use default image from release manifest when spec.image is not set - require.Equal(t, defaultPortworxImage+":2.10.0", cluster.Spec.Image) + require.Equal(t, DefaultPortworxImage+":2.10.0", cluster.Spec.Image) require.Equal(t, "2.10.0", cluster.Spec.Version) require.Equal(t, "2.10.0", cluster.Status.Version) require.True(t, cluster.Spec.Kvdb.Internal) @@ -1309,7 +1309,7 @@ func TestSetDefaultsOnStorageCluster(t *testing.T) { cluster.Spec.Version = " " err = driver.SetDefaultsOnStorageCluster(cluster) require.NoError(t, err) - require.Equal(t, defaultPortworxImage+":2.10.0", cluster.Spec.Image) + require.Equal(t, DefaultPortworxImage+":2.10.0", cluster.Spec.Image) require.Equal(t, "2.10.0", cluster.Spec.Version) require.Equal(t, "2.10.0", cluster.Status.Version) diff --git a/drivers/storage/portworx/testspec/portworxClusterRole.yaml b/drivers/storage/portworx/testspec/portworxClusterRole.yaml index 9182b22c64..220415630b 100644 --- a/drivers/storage/portworx/testspec/portworxClusterRole.yaml +++ b/drivers/storage/portworx/testspec/portworxClusterRole.yaml @@ -48,6 +48,9 @@ rules: - apiGroups: ["core.libopenstorage.org"] resources: ["*"] verbs: ["*"] +- apiGroups: ["portworx.io"] + resources: ["portworxdiags", "portworxdiags/status"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["security.openshift.io"] resources: ["securitycontextconstraints"] resourceNames: ["portworx"] diff --git a/drivers/storage/portworx/util/util.go b/drivers/storage/portworx/util/util.go index afa6dc8fd9..82c03f4f0e 100644 --- a/drivers/storage/portworx/util/util.go +++ b/drivers/storage/portworx/util/util.go @@ -1251,6 +1251,93 @@ func CountStorageNodes( return storageNodesCount, nil } +func getStorageNodeMappingFromRPC( + cluster *corev1.StorageCluster, + sdkConn *grpc.ClientConn, + k8sClient client.Client, +) (map[string]string, map[string]string, error) { + nodeClient := api.NewOpenStorageNodeClient(sdkConn) + ctx, err := SetupContextWithToken(context.Background(), cluster, k8sClient, false) + if err != nil { + return nil, nil, err + } + + nodeEnumerateResponse, err := nodeClient.EnumerateWithFilters( + ctx, + &api.SdkNodeEnumerateWithFiltersRequest{}, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to enumerate nodes: %v", err) + } + + nodeNameToNodeID := map[string]string{} + nodeIDToNodeName := map[string]string{} + + // Loop through all storage nodes + for _, n := range nodeEnumerateResponse.Nodes { + if n.SchedulerNodeName == "" { + continue + } + + nodeNameToNodeID[n.SchedulerNodeName] = n.Id + nodeIDToNodeName[n.Id] = n.SchedulerNodeName + } + + return nodeNameToNodeID, nodeIDToNodeName, nil +} + +func getStorageNodeMappingFromK8s( + cluster *corev1.StorageCluster, + k8sClient client.Client, +) (map[string]string, map[string]string, error) { + nodes := &corev1.StorageNodeList{} + err := k8sClient.List(context.TODO(), nodes, &client.ListOptions{Namespace: cluster.Namespace}) + if err != nil { + return nil, nil, fmt.Errorf("failed to list StorageNodes: %v", err) + } + + nodeNameToNodeID := map[string]string{} + nodeIDToNodeName := map[string]string{} + + // Loop through all storage nodes + for _, n := range nodes.Items { + if n.Status.NodeUID == "" { + continue + } + + nodeNameToNodeID[n.Name] = n.Status.NodeUID + nodeIDToNodeName[n.Status.NodeUID] = n.Name + } + + return nodeNameToNodeID, nodeIDToNodeName, nil +} + +// GetStorageNodeMapping returns a mapping of node name to node ID, as well as the inverse mapping. +// If sdkConn is nil, it will fall back to k8s API which may not be up to date. +// If both fail then the error will be returned. +func GetStorageNodeMapping( + cluster *corev1.StorageCluster, + sdkConn *grpc.ClientConn, + k8sClient client.Client, +) (map[string]string, map[string]string, error) { + var nodeNameToNodeID, nodeIDToNodeName map[string]string + var rpcErr, k8sErr error + + if sdkConn != nil { + nodeNameToNodeID, nodeIDToNodeName, rpcErr = getStorageNodeMappingFromRPC(cluster, sdkConn, k8sClient) + if rpcErr == nil { + return nodeNameToNodeID, nodeIDToNodeName, nil + } + logrus.WithError(rpcErr).Warn("Failed to get storage node mapping from RPC, falling back to k8s API which may not be up to date") + } + + nodeNameToNodeID, nodeIDToNodeName, k8sErr = getStorageNodeMappingFromK8s(cluster, k8sClient) + if k8sErr != nil { + return nil, nil, fmt.Errorf("failed to get storage node mapping from both RPC and k8s. RPC err: %v. k8s err: %v", rpcErr, k8sErr) + } + return nodeNameToNodeID, nodeIDToNodeName, nil +} + func CleanupObject(obj client.Object) { obj.SetGenerateName("") obj.SetUID("") diff --git a/go.mod b/go.mod index 62627ee4d8..fd0e210667 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( k8s.io/utils v0.0.0-20230505201702-9f6742963106 sigs.k8s.io/cluster-api v0.2.11 sigs.k8s.io/controller-runtime v0.14.5 + sigs.k8s.io/controller-tools v0.11.3 sigs.k8s.io/yaml v1.4.0 ) @@ -62,6 +63,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/gobuffalo/flect v0.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -78,6 +80,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -102,6 +105,7 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect golang.org/x/crypto v0.18.0 // indirect diff --git a/go.sum b/go.sum index 74c9b5a027..ee0f1521ca 100644 --- a/go.sum +++ b/go.sum @@ -2567,6 +2567,7 @@ github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= github.com/gobuffalo/flect v0.2.5/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= +github.com/gobuffalo/flect v0.3.0 h1:erfPWM+K1rFNIQeRPdeEXxo8yFr/PO17lhRnS8FUrtk= github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= @@ -3206,6 +3207,7 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/flux v0.65.0/go.mod h1:BwN2XG2lMszOoquQaFdPET8FRQfrXiZsWmcMO9rkaVY= github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= @@ -4446,6 +4448,7 @@ github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -6304,6 +6307,7 @@ sigs.k8s.io/cluster-api v0.2.11/go.mod h1:BCw+Pqy1sc8mQ/3d2NZM/f5BApKFCMPsnGvKol sigs.k8s.io/container-object-storage-interface-spec v0.0.0-20220211001052-50e143052de8/go.mod h1:kafkL5l/lTUrZXhVi/9p1GzpEE/ts29BkWkL3Ao33WU= sigs.k8s.io/controller-runtime v0.13.0 h1:iqa5RNciy7ADWnIc8QxCbOX5FEKVR3uxVxKHRMc2WIQ= sigs.k8s.io/controller-runtime v0.13.0/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= +sigs.k8s.io/controller-tools v0.11.3 h1:T1xzLkog9saiyQSLz1XOImu4OcbdXWytc5cmYsBeBiE= sigs.k8s.io/controller-tools v0.11.3/go.mod h1:qcfX7jfcfYD/b7lAhvqAyTbt/px4GpvN88WKLFFv7p8= sigs.k8s.io/gcp-compute-persistent-disk-csi-driver v0.7.0/go.mod h1:aSyCjg9bNQQxY9hnnNo10vjhZsQTkLliruvRXp3N9B4= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= diff --git a/pkg/apis/addtoscheme_portworx_v1.go b/pkg/apis/addtoscheme_portworx_v1.go new file mode 100644 index 0000000000..83c06e4e3a --- /dev/null +++ b/pkg/apis/addtoscheme_portworx_v1.go @@ -0,0 +1,10 @@ +package apis + +import ( + v1 "github.com/libopenstorage/operator/pkg/apis/portworx/v1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme) +} diff --git a/pkg/controller/portworxdiag/controller_test.go b/pkg/controller/portworxdiag/controller_test.go new file mode 100644 index 0000000000..e1dc976667 --- /dev/null +++ b/pkg/controller/portworxdiag/controller_test.go @@ -0,0 +1,1182 @@ +package portworxdiag + +import ( + "context" + "fmt" + "reflect" + "strconv" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/libopenstorage/openstorage/api" + pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" + corev1 "github.com/libopenstorage/operator/pkg/apis/core/v1" + diagv1 "github.com/libopenstorage/operator/pkg/apis/portworx/v1" + "github.com/libopenstorage/operator/pkg/mock" + apiextensionsops "github.com/portworx/sched-ops/k8s/apiextensions" + coreops "github.com/portworx/sched-ops/k8s/core" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + fakeextclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + kversion "k8s.io/apimachinery/pkg/version" + fakediscovery "k8s.io/client-go/discovery/fake" + fakek8sclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + k8scontroller "k8s.io/kubernetes/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/libopenstorage/operator/pkg/client/clientset/versioned/scheme" + testutil "github.com/libopenstorage/operator/pkg/util/test" +) + +var ( + podCreateCount = 0 +) + +func createK8sNode(nodeName string, allowedPods int) *v1.Node { + return &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + Labels: make(map[string]string), + }, + Status: v1.NodeStatus{ + Allocatable: map[v1.ResourceName]resource.Quantity{ + v1.ResourcePods: resource.MustParse(strconv.Itoa(allowedPods)), + }, + }, + } +} + +func createGRPCStorageNode(name, id string) *api.StorageNode { + return &api.StorageNode{ + Id: id, + SchedulerNodeName: name, + } +} + +func setUpGRPCMocks(t *testing.T, ns string) (*mock.MockOpenStorageNodeServer, *mock.MockOpenStorageVolumeServer, *v1.Service, func()) { + var ( + sdkServerIP = "127.0.0.1" + sdkServerPort = 23888 + ) + + // Run this first to detect permission errors early + testutil.SetupEtcHosts(t, sdkServerIP, pxutil.PortworxServiceName+"."+ns) + + mockCtrl := gomock.NewController(t) + + // Create the mock servers that can be used to mock SDK calls + var ( + mockNodeServer = mock.NewMockOpenStorageNodeServer(mockCtrl) + mockVolumeServer = mock.NewMockOpenStorageVolumeServer(mockCtrl) + + // Start an sdk server that implements the mock servers + mockSdk = mock.NewSdkServer(mock.SdkServers{ + Node: mockNodeServer, + Volume: mockVolumeServer, + }) + ) + + err := mockSdk.StartOnAddress(sdkServerIP, strconv.Itoa(sdkServerPort)) + require.NoError(t, err) + + pxService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: pxutil.PortworxServiceName, + Namespace: ns, + }, + Spec: v1.ServiceSpec{ + ClusterIP: sdkServerIP, + Ports: []v1.ServicePort{ + { + Name: pxutil.PortworxSDKPortName, + Port: int32(sdkServerPort), + }, + }, + }, + } + + return mockNodeServer, mockVolumeServer, pxService, func() { + mockSdk.Stop() + mockCtrl.Finish() + testutil.RestoreEtcHosts(t) + } +} + +func applyPodControlTemplates(t *testing.T, k8sClient client.Client, podControl *k8scontroller.FakePodControl) []*v1.Pod { + createdPods := []*v1.Pod{} + for _, template := range podControl.Templates { + newPod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: template.GenerateName + strconv.Itoa(podCreateCount), + Namespace: template.Namespace, + Labels: template.Labels, + }, + Spec: template.Spec, + } + err := k8sClient.Create(context.Background(), newPod) + require.NoError(t, err) + createdPods = append(createdPods, newPod) + podCreateCount += 1 + } + podControl.Templates = []v1.PodTemplateSpec{} + return createdPods +} + +func TestInit(t *testing.T) { + mockCtrl := gomock.NewController(t) + + fakeClient := fakek8sclient.NewSimpleClientset() + k8sClient := testutil.FakeK8sClient() + coreops.SetInstance(coreops.New(fakeClient)) + recorder := record.NewFakeRecorder(10) + + mgr := mock.NewMockManager(mockCtrl) + mgr.EXPECT().GetClient().Return(k8sClient).AnyTimes() + mgr.EXPECT().GetScheme().Return(scheme.Scheme).AnyTimes() + mgr.EXPECT().GetEventRecorderFor(gomock.Any()).Return(recorder).AnyTimes() + mgr.EXPECT().SetFields(gomock.Any()).Return(nil).AnyTimes() + mgr.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes() + mgr.EXPECT().GetLogger().Return(log.Log.WithName("test")).AnyTimes() + mgr.EXPECT().GetConfig().Return(&rest.Config{ + Host: "127.0.0.1", + APIPath: "fake", + }).AnyTimes() + + controller := Controller{ + client: k8sClient, + recorder: recorder, + } + err := controller.Init(mgr) + require.NoError(t, err) + + ctrl := mock.NewMockController(mockCtrl) + controller.ctrl = ctrl + ctrl.EXPECT().Watch(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + + err = controller.StartWatch() + require.NoError(t, err) +} + +func TestRegisterCRD(t *testing.T) { + fakeClient := fakek8sclient.NewSimpleClientset() + fakeClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &kversion.Info{ + GitVersion: "v1.23.0", + } + fakeExtClient := fakeextclient.NewSimpleClientset() + coreops.SetInstance(coreops.New(fakeClient)) + apiextensionsops.SetInstance(apiextensionsops.New(fakeExtClient)) + group := diagv1.SchemeGroupVersion.Group + portworxDiagCRDName := "portworxdiags" + "." + group + + // When the CRDs are created, just updated their status so the validation + // does not get stuck until timeout. + go func() { + err := testutil.ActivateCRDWhenCreated(fakeExtClient, portworxDiagCRDName) + require.NoError(t, err) + }() + + controller := Controller{} + + // Should fail if the CRD specs are not found + err := controller.RegisterCRD() + require.Error(t, err) + + // Set the correct crd path + crdBaseDir = func() string { + return "../../../deploy/crds" + } + defer func() { + crdBaseDir = getCRDBasePath + }() + + err = controller.RegisterCRD() + require.NoError(t, err) + + crds, err := fakeExtClient.ApiextensionsV1(). + CustomResourceDefinitions(). + List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, crds.Items, 1) + + pdCRD, err := fakeExtClient.ApiextensionsV1(). + CustomResourceDefinitions(). + Get(context.TODO(), portworxDiagCRDName, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, portworxDiagCRDName, pdCRD.Name) + require.Equal(t, diagv1.SchemeGroupVersion.Group, pdCRD.Spec.Group) + require.Len(t, pdCRD.Spec.Versions, 1) + require.Equal(t, diagv1.SchemeGroupVersion.Version, pdCRD.Spec.Versions[0].Name) + require.True(t, pdCRD.Spec.Versions[0].Served) + require.True(t, pdCRD.Spec.Versions[0].Storage) + subresource := &apiextensionsv1.CustomResourceSubresources{ + Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, + } + require.Equal(t, subresource, pdCRD.Spec.Versions[0].Subresources) + require.NotEmpty(t, pdCRD.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties) + require.Equal(t, apiextensionsv1.NamespaceScoped, pdCRD.Spec.Scope) + require.Equal(t, "portworxdiag", pdCRD.Spec.Names.Singular) + require.Equal(t, "portworxdiags", pdCRD.Spec.Names.Plural) + require.Equal(t, reflect.TypeOf(diagv1.PortworxDiag{}).Name(), pdCRD.Spec.Names.Kind) + require.Equal(t, reflect.TypeOf(diagv1.PortworxDiagList{}).Name(), pdCRD.Spec.Names.ListKind) + require.Equal(t, []string{"pxdiag"}, pdCRD.Spec.Names.ShortNames) + + // If CRDs are already present, then should update it + pdCRD.ResourceVersion = "1000" + _, err = fakeExtClient.ApiextensionsV1(). + CustomResourceDefinitions(). + Update(context.TODO(), pdCRD, metav1.UpdateOptions{}) + require.NoError(t, err) + + // The fake client overwrites the status in Update call which real client + // does not. This will keep the CRD activated so validation does not get stuck. + go func() { + err := keepCRDActivated(fakeExtClient, portworxDiagCRDName) + require.NoError(t, err) + }() + + // If CRDs are already present, then should not fail + err = controller.RegisterCRD() + require.NoError(t, err) + + crds, err = fakeExtClient.ApiextensionsV1(). + CustomResourceDefinitions(). + List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, crds.Items, 1) + require.Equal(t, portworxDiagCRDName, crds.Items[0].Name) + + pdCRD, err = fakeExtClient.ApiextensionsV1(). + CustomResourceDefinitions(). + Get(context.TODO(), portworxDiagCRDName, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "1000", pdCRD.ResourceVersion) +} + +func TestReconcileOfDeletedDiag(t *testing.T) { + k8sClient := testutil.FakeK8sClient() + recorder := record.NewFakeRecorder(1) + controller := Controller{ + client: k8sClient, + recorder: recorder, + } + + request := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "does-not-exist", + Namespace: "test-ns", + }, + } + result, err := controller.Reconcile(context.TODO(), request) + require.NoError(t, err) + require.Empty(t, result) + require.Len(t, recorder.Events, 0) +} + +func keepCRDActivated(fakeClient *fakeextclient.Clientset, crdName string) error { + return wait.Poll(1*time.Second, 1*time.Minute, func() (bool, error) { + crd, err := fakeClient.ApiextensionsV1(). + CustomResourceDefinitions(). + Get(context.TODO(), crdName, metav1.GetOptions{}) + if err != nil { + return false, err + } + if len(crd.Status.Conditions) == 0 { + crd.Status.Conditions = []apiextensionsv1.CustomResourceDefinitionCondition{{ + Type: apiextensionsv1.Established, + Status: apiextensionsv1.ConditionTrue, + }} + _, err = fakeClient.ApiextensionsV1(). + CustomResourceDefinitions(). + UpdateStatus(context.TODO(), crd, metav1.UpdateOptions{}) + if err != nil { + return false, err + } + return true, nil + } + return false, nil + }) +} + +func TestShouldPodBeOnNode(t *testing.T) { + // Diag is nil: should return false + should := shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, nil) + require.False(t, should) + + // Diag.Spec.Portworx is nil: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: nil, + }, + }) + require.False(t, should) + + // NodeSelector.All is true: should return true + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + All: true, + }, + }, + }, + }) + require.True(t, should) + + // NodeSelector.All is false, NodeSelector.IDs is nil and NodeSelector.Labels is nil: should return true + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: nil, + Labels: nil, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.All is false, NodeSelector.IDs is empty and NodeSelector.Labels is empty: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{}, + Labels: map[string]string{}, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.IDs contains the given node ID: should return true + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{"some-uuid"}, + }, + }, + }, + }) + require.True(t, should) + + // NodeSelector.IDs contains some node IDs but not the given node ID: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{}, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{"another-uuid"}, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.Labels is populated, no matching node: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + }, + }, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.Labels is populated, label missing: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "baz": "nah", + }, + }, + }, + }, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.Labels is populated, label present but wrong value: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "foo": "nah", + }, + }, + }, + }, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.Labels is populated, label present and correct value: should return true + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.True(t, should) + + // NodeSelector.Labels is populated, one label present, one not: should return false + should = shouldPodBeOnNode("some-uuid", "node1", []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "foo": "bar", + "baz": "nah", + }, + }, + }, + }, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + Labels: map[string]string{ + "foo": "bar", + "baz": "bar", + }, + }, + }, + }, + }) + require.False(t, should) + + // NodeSelector.IDs and Labels is populated, disjoint set: should return true for both nodes + nodes := []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + } + // Both node1 and node2 should be valid + // node1 because it has the correct ID + // node2 because it has the correct label + should = shouldPodBeOnNode("some-uuid", "node1", nodes, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{"some-uuid"}, + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.True(t, should) + should = shouldPodBeOnNode("some-uuid", "node2", nodes, nil, &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{"some-uuid"}, + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }) + require.True(t, should) +} + +func TestGetNodeToPodMap(t *testing.T) { + res := getNodeToPodMap(nil) + require.Empty(t, res) + + res = getNodeToPodMap(&v1.PodList{}) + require.Empty(t, res) + + pod1 := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + }, + Spec: v1.PodSpec{ + NodeName: "node1", + }, + } + pod2 := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + }, + Spec: v1.PodSpec{ + NodeName: "node2", + }, + } + + res = getNodeToPodMap(&v1.PodList{ + Items: []v1.Pod{pod1, pod2}, + }) + require.Len(t, res, 2) + require.Contains(t, res, "node1") + require.Contains(t, res, "node2") + require.Equal(t, *res["node1"], pod1) + require.Equal(t, *res["node2"], pod2) +} + +func TestGetNodeIDToStatusMap(t *testing.T) { + // Test cases: + // * a nil node list should return an empty map + // * an empty node list should return an empty map + // * a node list with two nodes should return a map with two entries + // * a node list with different statuses should return a map with two entries with the correct statuses + // * a node list with a node with an empty node ID should not include the node in the map + + res := getNodeIDToStatusMap(nil) + require.Empty(t, res) + + res = getNodeIDToStatusMap([]diagv1.NodeStatus{}) + require.Empty(t, res) + + res = getNodeIDToStatusMap([]diagv1.NodeStatus{ + { + NodeID: "node1", + Status: diagv1.NodeStatusInProgress, + }, + }) + require.Len(t, res, 1) + require.Contains(t, res, "node1") + require.Equal(t, res["node1"], diagv1.NodeStatusInProgress) + + res = getNodeIDToStatusMap([]diagv1.NodeStatus{ + { + NodeID: "node1", + Status: diagv1.NodeStatusInProgress, + }, + { + NodeID: "node2", + Status: diagv1.NodeStatusCompleted, + }, + }) + require.Len(t, res, 2) + require.Contains(t, res, "node1") + require.Contains(t, res, "node2") + require.Equal(t, res["node1"], diagv1.NodeStatusInProgress) + require.Equal(t, res["node2"], diagv1.NodeStatusCompleted) + + res = getNodeIDToStatusMap([]diagv1.NodeStatus{ + { + NodeID: "", + Status: diagv1.NodeStatusInProgress, + }, + }) + require.Empty(t, res) +} + +func TestGetPodsDiff(t *testing.T) { + c := Controller{} + + // Node 0 will be missing a status entirely: as node statuses is already populated we won't add a new entry + // Node 1 will have a status but be missing a pod (should create a pod) + // Node 2 will be completed successfully (should delete a pod) + // Node 3 will be completed and failed (should delete a pod) + // Node 4 will be in progress (should not change anything) + // Node 5 will not match the selector (should delete the existing pod) + + // Test object setup + n := 6 + pods := make([]v1.Pod, n) + nodes := make([]v1.Node, n) + nodeIDToName := make(map[string]string) + for i := 0; i < n; i++ { + pods[i] = v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("pod%d", i)}, + Spec: v1.PodSpec{NodeName: fmt.Sprintf("node%d", i)}, + } + nodes[i] = v1.Node{ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("node%d", i)}} + nodeIDToName[fmt.Sprintf("id%d", i)] = fmt.Sprintf("node%d", i) + } + + prs, err := c.getPodsDiff( + &v1.PodList{Items: pods[2:]}, // Only pass in a subset of the nodes to simulate some not existing + &v1.NodeList{Items: nodes}, + &diagv1.PortworxDiag{ + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + IDs: []string{"id0", "id1", "id2", "id3", "id4"}, + }, + }, + }, + Status: diagv1.PortworxDiagStatus{ + NodeStatuses: []diagv1.NodeStatus{ + {NodeID: "id1", Status: diagv1.NodeStatusPending}, + {NodeID: "id2", Status: diagv1.NodeStatusCompleted}, + {NodeID: "id3", Status: diagv1.NodeStatusFailed}, + {NodeID: "id4", Status: diagv1.NodeStatusInProgress}, + }, + }, + }, nil, nodeIDToName) + require.Empty(t, prs.nodeStatusesToAdd) + require.ElementsMatch(t, prs.nodesToCreatePodsFor, []string{"node1"}) + require.ElementsMatch(t, prs.podsToDelete, []*v1.Pod{&pods[2], &pods[3], &pods[5]}) + require.NoError(t, err) +} + +func TestGetOverallPhase(t *testing.T) { + phase, msg := getOverallPhase([]diagv1.NodeStatus{}) + require.Equal(t, diagv1.DiagStatusPending, phase) + require.Empty(t, msg) + + // If all phases are empty or pending, the overall phase should be pending + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: ""}, + {Status: ""}, + {Status: diagv1.NodeStatusPending}, + }) + require.Equal(t, diagv1.DiagStatusPending, phase) + require.Empty(t, msg) + + // If all phases are completed, the overall phase should be completed + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusCompleted}, + {Status: diagv1.NodeStatusCompleted}, + {Status: diagv1.NodeStatusCompleted}, + }) + require.Equal(t, diagv1.DiagStatusCompleted, phase) + require.Equal(t, "All diags collected successfully", msg) + + // If all phases are failed, the overall phase should be failed + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusFailed}, + {Status: diagv1.NodeStatusFailed}, + {Status: diagv1.NodeStatusFailed}, + }) + require.Equal(t, diagv1.DiagStatusFailed, phase) + require.Equal(t, "All diags failed to collect", msg) + + // If some phases are pending and some are completed, the overall phase should be partial failure + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusPending}, + {Status: diagv1.NodeStatusCompleted}, + {Status: diagv1.NodeStatusCompleted}, + }) + require.Equal(t, diagv1.DiagStatusPartialFailure, phase) + require.Equal(t, "Some diags failed to collect", msg) + + // If some phases are failed and some are completed, the overall phase should be partial failure + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusFailed}, + {Status: diagv1.NodeStatusCompleted}, + {Status: diagv1.NodeStatusCompleted}, + }) + require.Equal(t, diagv1.DiagStatusPartialFailure, phase) + require.Equal(t, "Some diags failed to collect", msg) + + // If some phases are in progress, no matter what the phase should be in progress + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusInProgress}, + {Status: diagv1.NodeStatusCompleted}, + {Status: diagv1.NodeStatusFailed}, + {Status: diagv1.NodeStatusPending}, + }) + require.Equal(t, diagv1.DiagStatusInProgress, phase) + require.Equal(t, "Diag collection is in progress", msg) + + // If an unknown status slips in, the overall phase should be unknown unless another node is in progress + phase, msg = getOverallPhase([]diagv1.NodeStatus{ + {Status: diagv1.NodeStatusPending}, + {Status: "InvalidStatus"}, + }) + require.Equal(t, diagv1.DiagStatusUnknown, phase) + require.Empty(t, msg) +} + +func TestGetDiagObject(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + driver := testutil.MockDriver(mockCtrl) + k8sClient := testutil.FakeK8sClient(&diagv1.PortworxDiag{}) + podControl := &k8scontroller.FakePodControl{} + recorder := record.NewFakeRecorder(10) + controller := Controller{ + client: k8sClient, + Driver: driver, + podControl: podControl, + recorder: recorder, + } + + // First find with no objects created + diag, otherRunning, err := controller.getDiagObject(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "portworx", + }, + }) + require.Nil(t, diag) + require.False(t, otherRunning) + require.NoError(t, err) + + // Create another diag object + diagInProgress := &diagv1.PortworxDiag{ + ObjectMeta: metav1.ObjectMeta{ + Name: "otherdiag", + Namespace: "portworx", + }, + Status: diagv1.PortworxDiagStatus{ + Phase: diagv1.DiagStatusInProgress, + }, + } + err = k8sClient.Create(context.Background(), diagInProgress) + require.NoError(t, err) + + diag, otherRunning, err = controller.getDiagObject(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "portworx", + }, + }) + require.Nil(t, diag) + require.True(t, otherRunning) + require.NoError(t, err) + + // Create our diag object + diagOurs := &diagv1.PortworxDiag{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "portworx", + }, + Status: diagv1.PortworxDiagStatus{ + Phase: diagv1.DiagStatusInProgress, + }, + } + err = k8sClient.Create(context.Background(), diagOurs) + require.NoError(t, err) + + diag, otherRunning, err = controller.getDiagObject(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "portworx", + }, + }) + require.Equal(t, diagOurs, diag) + require.True(t, otherRunning) + require.NoError(t, err) + + // Delete the other diag object, now we should be able to run + err = k8sClient.Delete(context.Background(), diagInProgress) + require.NoError(t, err) + + diag, otherRunning, err = controller.getDiagObject(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: "test", + Namespace: "portworx", + }, + }) + require.Equal(t, diagOurs, diag) + require.False(t, otherRunning) + require.NoError(t, err) +} + +func TestReconcile_BasicLifecycle(t *testing.T) { + const ns = "test-ns" + + mockNodeServer, _, pxService, cleanupMocks := setUpGRPCMocks(t, ns) + defer cleanupMocks() + + k8sNodes := []*v1.Node{ + createK8sNode("k8s-node-1", 1), + createK8sNode("k8s-node-2", 1), + createK8sNode("k8s-node-3", 1), + } + + cluster := &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "stc", Namespace: ns}, + Status: corev1.StorageClusterStatus{ + ClusterUID: "cluster-uid", + }, + } + diag := &diagv1.PortworxDiag{ + ObjectMeta: metav1.ObjectMeta{Name: "test-diag", Namespace: ns}, + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + All: true, + }, + }, + }, + } + + var err error + + k8sClient := testutil.FakeK8sClient(diag, cluster, pxService) + for _, node := range k8sNodes { + err = k8sClient.Create(context.Background(), node) + require.NoError(t, err) + } + recorder := record.NewFakeRecorder(10) + podControl := &k8scontroller.FakePodControl{} + controller := Controller{ + client: k8sClient, + recorder: recorder, + podControl: podControl, + } + + mockNodeServer.EXPECT(). + EnumerateWithFilters(gomock.Any(), gomock.Any()). + Return(&api.SdkNodeEnumerateWithFiltersResponse{ + Nodes: []*api.StorageNode{ + createGRPCStorageNode("k8s-node-1", "uuid-1"), + createGRPCStorageNode("k8s-node-2", "uuid-2"), + createGRPCStorageNode("k8s-node-3", "uuid-3"), + }, + }, nil).Times(4) + + // Verify initial reconcile + _, err = controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + + // Check that all nodes have pods being created and have the node names set + require.Len(t, podControl.Templates, 3) + nodesWithPods := []string{} + for _, pod := range podControl.Templates { + nodesWithPods = append(nodesWithPods, pod.Spec.NodeName) + } + require.ElementsMatch(t, []string{"k8s-node-1", "k8s-node-2", "k8s-node-3"}, nodesWithPods) + + // Check that the updated diag has the proper node statuses + updatedDiag := &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + + require.Len(t, updatedDiag.Status.NodeStatuses, 3) + require.ElementsMatch(t, []diagv1.NodeStatus{ + {NodeID: "uuid-1", Status: diagv1.NodeStatusPending}, + {NodeID: "uuid-2", Status: diagv1.NodeStatusPending}, + {NodeID: "uuid-3", Status: diagv1.NodeStatusPending}, + }, updatedDiag.Status.NodeStatuses) + require.Equal(t, diagv1.DiagStatusPending, updatedDiag.Status.Phase) + require.Equal(t, cluster.Status.ClusterUID, updatedDiag.Status.ClusterUUID) + + // Go through and actually create the pods + createdPods := applyPodControlTemplates(t, k8sClient, podControl) + + // Verify that the diag status updates as diags move to in progress + for i := range updatedDiag.Status.NodeStatuses { + updatedDiag.Status.NodeStatuses[i].Status = diagv1.NodeStatusInProgress + } + err = k8sClient.Update(context.Background(), updatedDiag) + require.NoError(t, err) + + // Verify that missing pods get recreated + err = k8sClient.Delete(context.Background(), createdPods[0]) + require.NoError(t, err) + + _, err = controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + require.Len(t, podControl.Templates, 1) + require.Equal(t, createdPods[0].Spec.NodeName, podControl.Templates[0].Spec.NodeName) + + // Check that the updated diag has the proper phase + updatedDiag = &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + require.Equal(t, diagv1.DiagStatusInProgress, updatedDiag.Status.Phase) + + // Go through and re-create the pods again + applyPodControlTemplates(t, k8sClient, podControl) + + // Verify that the pods are deleted when a node status is marked as complete + updatedDiag.Status.NodeStatuses[0].Status = diagv1.NodeStatusCompleted + err = k8sClient.Update(context.Background(), updatedDiag) + require.NoError(t, err) + + _, err = controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + require.Len(t, podControl.DeletePodName, 1) + podControl.DeletePodName = []string{} + + // Check that the updated diag has the proper phase + updatedDiag = &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + require.Equal(t, diagv1.DiagStatusInProgress, updatedDiag.Status.Phase) + + // Verify that once all the pods are complete, the phase is complete and the pods are deleted + updatedDiag = &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + for i := range updatedDiag.Status.NodeStatuses { + updatedDiag.Status.NodeStatuses[i].Status = diagv1.NodeStatusCompleted + } + err = k8sClient.Update(context.Background(), updatedDiag) + require.NoError(t, err) + + _, err = controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + + // Check that the updated diag has the proper phase + updatedDiag = &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + require.Equal(t, diagv1.DiagStatusCompleted, updatedDiag.Status.Phase) + + // Check that the pods are deleted + require.Len(t, podControl.DeletePodName, 3) + podControl.DeletePodName = []string{} + + // Check that future reconciles do no work + _, err = controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + require.Empty(t, podControl.Templates) + require.Empty(t, podControl.DeletePodName) +} + +func TestReconcile_NodeSelectors(t *testing.T) { + const ns = "test-ns" + + mockNodeServer, _, pxService, cleanupMocks := setUpGRPCMocks(t, ns) + defer cleanupMocks() + + k8sNodes := []*v1.Node{ + createK8sNode("k8s-node-1", 1), + createK8sNode("k8s-node-2", 1), + createK8sNode("k8s-node-3", 1), + createK8sNode("k8s-node-4", 1), + } + k8sNodes[0].Labels["dothediag"] = "true" + k8sNodes[1].Labels["dothediag"] = "false" + + cluster := &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "stc", Namespace: ns}, + Status: corev1.StorageClusterStatus{ + ClusterUID: "cluster-uid", + }, + } + diag := &diagv1.PortworxDiag{ + ObjectMeta: metav1.ObjectMeta{Name: "test-diag", Namespace: ns}, + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + NodeSelector: diagv1.NodeSelector{ + All: false, + IDs: []string{"uuid-4"}, + Labels: map[string]string{ + "dothediag": "true", + }, + }, + }, + }, + } + + k8sClient := testutil.FakeK8sClient(diag, cluster, pxService) + for _, node := range k8sNodes { + err := k8sClient.Create(context.Background(), node) + require.NoError(t, err) + } + recorder := record.NewFakeRecorder(10) + podControl := &k8scontroller.FakePodControl{} + controller := Controller{ + client: k8sClient, + recorder: recorder, + podControl: podControl, + } + + mockNodeServer.EXPECT(). + EnumerateWithFilters(gomock.Any(), gomock.Any()). + Return(&api.SdkNodeEnumerateWithFiltersResponse{ + Nodes: []*api.StorageNode{ + createGRPCStorageNode("k8s-node-1", "uuid-1"), + createGRPCStorageNode("k8s-node-2", "uuid-2"), + createGRPCStorageNode("k8s-node-3", "uuid-3"), + createGRPCStorageNode("k8s-node-4", "uuid-4"), + }, + }, nil). + Times(1) + + _, err := controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + + // Check that all nodes have pods being created and have the node names set + require.Len(t, podControl.Templates, 2) + nodesWithPods := []string{} + for _, pod := range podControl.Templates { + nodesWithPods = append(nodesWithPods, pod.Spec.NodeName) + } + require.ElementsMatch(t, []string{"k8s-node-1", "k8s-node-4"}, nodesWithPods) + + // Check that the updated diag has the proper node statuses + updatedDiag := &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + + require.Len(t, updatedDiag.Status.NodeStatuses, 2) + require.ElementsMatch(t, []diagv1.NodeStatus{ + {NodeID: "uuid-1", Status: diagv1.NodeStatusPending}, + {NodeID: "uuid-4", Status: diagv1.NodeStatusPending}, + }, updatedDiag.Status.NodeStatuses) + require.Equal(t, diagv1.DiagStatusPending, updatedDiag.Status.Phase) + require.Equal(t, cluster.Status.ClusterUID, updatedDiag.Status.ClusterUUID) +} + +func TestReconcile_VolumeSelectors(t *testing.T) { + const ns = "test-ns" + + mockNodeServer, mockVolumeServer, pxService, cleanupMocks := setUpGRPCMocks(t, ns) + defer cleanupMocks() + + k8sNodes := []*v1.Node{ + createK8sNode("k8s-node-1", 1), + createK8sNode("k8s-node-2", 1), + createK8sNode("k8s-node-3", 1), + createK8sNode("k8s-node-4", 1), + } + k8sNodes[0].Labels["dothediag"] = "true" + k8sNodes[1].Labels["dothediag"] = "false" + + cluster := &corev1.StorageCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "stc", Namespace: ns}, + Status: corev1.StorageClusterStatus{ + ClusterUID: "cluster-uid", + }, + } + diag := &diagv1.PortworxDiag{ + ObjectMeta: metav1.ObjectMeta{Name: "test-diag", Namespace: ns}, + Spec: diagv1.PortworxDiagSpec{ + Portworx: &diagv1.PortworxComponent{ + VolumeSelector: diagv1.VolumeSelector{ + IDs: []string{"vol-4"}, + Labels: map[string]string{ + "dothediag": "true", + }, + }, + }, + }, + } + + k8sClient := testutil.FakeK8sClient(diag, cluster, pxService) + for _, node := range k8sNodes { + err := k8sClient.Create(context.Background(), node) + require.NoError(t, err) + } + recorder := record.NewFakeRecorder(10) + podControl := &k8scontroller.FakePodControl{} + controller := Controller{ + client: k8sClient, + recorder: recorder, + podControl: podControl, + } + + mockNodeServer.EXPECT(). + EnumerateWithFilters(gomock.Any(), gomock.Any()). + Return(&api.SdkNodeEnumerateWithFiltersResponse{ + Nodes: []*api.StorageNode{ + createGRPCStorageNode("k8s-node-1", "uuid-1"), + createGRPCStorageNode("k8s-node-2", "uuid-2"), + createGRPCStorageNode("k8s-node-3", "uuid-3"), + createGRPCStorageNode("k8s-node-4", "uuid-4"), + }, + }, nil). + Times(1) + mockVolumeServer.EXPECT(). + Inspect(gomock.Any(), VolumeInspectRequestWithVolumeID("vol-4")). + Return(&api.SdkVolumeInspectResponse{ + Volume: &api.Volume{ + ReplicaSets: []*api.ReplicaSet{{Nodes: []string{"uuid-4"}}}, + }}, nil). + Times(1) + mockVolumeServer.EXPECT(). + InspectWithFilters(gomock.Any(), VolumeInspectWithFilterRequestWithLabels(map[string]string{"dothediag": "true"})). + Return(&api.SdkVolumeInspectWithFiltersResponse{ + Volumes: []*api.SdkVolumeInspectResponse{ + {Volume: &api.Volume{ReplicaSets: []*api.ReplicaSet{{Nodes: []string{"uuid-1"}}}}}, + }, + }, nil). + Times(1) + + _, err := controller.Reconcile(context.TODO(), reconcile.Request{ + NamespacedName: types.NamespacedName{Name: diag.Name, Namespace: diag.Namespace}, + }) + require.NoError(t, err) + + // Check that all nodes have pods being created and have the node names set + require.Len(t, podControl.Templates, 2) + nodesWithPods := []string{} + for _, pod := range podControl.Templates { + nodesWithPods = append(nodesWithPods, pod.Spec.NodeName) + } + require.ElementsMatch(t, []string{"k8s-node-1", "k8s-node-4"}, nodesWithPods) + + // Check that the updated diag has the proper node statuses + updatedDiag := &diagv1.PortworxDiag{} + err = testutil.Get(k8sClient, updatedDiag, diag.Name, diag.Namespace) + require.NoError(t, err) + + require.Len(t, updatedDiag.Status.NodeStatuses, 2) + require.ElementsMatch(t, []diagv1.NodeStatus{ + {NodeID: "uuid-1", Status: diagv1.NodeStatusPending}, + {NodeID: "uuid-4", Status: diagv1.NodeStatusPending}, + }, updatedDiag.Status.NodeStatuses) + require.Equal(t, diagv1.DiagStatusPending, updatedDiag.Status.Phase) + require.Equal(t, cluster.Status.ClusterUID, updatedDiag.Status.ClusterUUID) +} diff --git a/pkg/controller/portworxdiag/matchers_test.go b/pkg/controller/portworxdiag/matchers_test.go new file mode 100644 index 0000000000..6a8cbb82d9 --- /dev/null +++ b/pkg/controller/portworxdiag/matchers_test.go @@ -0,0 +1,52 @@ +package portworxdiag + +import ( + "fmt" + "reflect" + + "github.com/libopenstorage/openstorage/api" +) + +// This file contains various gomock matcher utilities that can be used to better check calls +// to the mock GRPC server. + +type volumeInspectRequestMatcher struct { + volumeID string +} + +func (v volumeInspectRequestMatcher) Matches(x interface{}) bool { + req, ok := x.(*api.SdkVolumeInspectRequest) + if !ok { + return false + } + return req.GetVolumeId() == v.volumeID +} + +func (v volumeInspectRequestMatcher) String() string { + return "matches volume inspect request with volume ID " + v.volumeID +} + +func VolumeInspectRequestWithVolumeID(volumeID string) *volumeInspectRequestMatcher { + return &volumeInspectRequestMatcher{volumeID} +} + +type volumeInspectWithFiltersRequestWithLabelsMatcher struct { + labels map[string]string +} + +func (v volumeInspectWithFiltersRequestWithLabelsMatcher) Matches(x interface{}) bool { + req, ok := x.(*api.SdkVolumeInspectWithFiltersRequest) + if !ok { + return false + } + // Deep-compare the labels + return reflect.DeepEqual(req.GetLabels(), v.labels) +} + +func (v volumeInspectWithFiltersRequestWithLabelsMatcher) String() string { + return fmt.Sprintf("matches volume inspect request with labels %v", v.labels) +} + +func VolumeInspectWithFilterRequestWithLabels(labels map[string]string) *volumeInspectWithFiltersRequestWithLabelsMatcher { + return &volumeInspectWithFiltersRequestWithLabelsMatcher{labels} +} diff --git a/pkg/controller/portworxdiag/pod.go b/pkg/controller/portworxdiag/pod.go new file mode 100644 index 0000000000..717b06d4d6 --- /dev/null +++ b/pkg/controller/portworxdiag/pod.go @@ -0,0 +1,115 @@ +package portworxdiag + +import ( + "fmt" + "os" + + "github.com/libopenstorage/operator/drivers/storage/portworx" + + pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" + corev1 "github.com/libopenstorage/operator/pkg/apis/core/v1" + portworxv1 "github.com/libopenstorage/operator/pkg/apis/portworx/v1" + "github.com/libopenstorage/operator/pkg/util" + k8sutil "github.com/libopenstorage/operator/pkg/util/k8s" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + PortworxDiagLabel = "portworx-diag" +) + +func v1Volume(name, path string, hpType v1.HostPathType) v1.Volume { + return v1.Volume{ + Name: name, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: path, + Type: &hpType, + }, + }, + } +} + +func volumes() []v1.Volume { + return []v1.Volume{ + v1Volume("procmount", "/proc", v1.HostPathUnset), + v1Volume("diagsdump", "/var/cores", v1.HostPathUnset), + } +} + +func volumeMounts() []v1.VolumeMount { + return []v1.VolumeMount{ + {Name: "procmount", MountPath: "/host_proc"}, + {Name: "diagsdump", MountPath: "/var/cores"}, + } +} + +func makeDiagPodTemplate(cluster *corev1.StorageCluster, diag *portworxv1.PortworxDiag, ns string, nodeName string, nodeID string) (*v1.PodTemplateSpec, error) { + terminationGP := int64(10) + privileged := true + + diagImage := fmt.Sprintf("%s:master", portworx.DefaultPortworxImage) + if img := os.Getenv("DIAG_IMAGE"); len(img) > 0 { + diagImage = img + } + diagImageURN := util.GetImageURN(cluster, diagImage) + + isController := false + blockOwnerDeletion := true + + podTemplateSpec := &v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "px-diag", + Namespace: ns, + Labels: map[string]string{ + "name": PortworxDiagLabel, + portworxv1.LabelPortworxDiagName: diag.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "portworx.io/v1", + Kind: "PortworxDiag", + Name: diag.Name, + Controller: &isController, + BlockOwnerDeletion: &blockOwnerDeletion, + }, + }, + }, + Spec: v1.PodSpec{ + NodeName: nodeName, + HostPID: true, + RestartPolicy: v1.RestartPolicyOnFailure, + ServiceAccountName: pxutil.PortworxServiceAccountName(cluster), + TerminationGracePeriodSeconds: &terminationGP, + Volumes: volumes(), + Containers: []v1.Container{ + { + Name: "px-diag-collector", + Image: diagImageURN, + ImagePullPolicy: v1.PullAlways, + Args: []string{ + "--diags", + "--diags-obj-name", + diag.Name, + "--diags-obj-namespace", + diag.Namespace, + "--diags-node-id", + nodeID, + }, + SecurityContext: &v1.SecurityContext{ + Privileged: &privileged, + }, + VolumeMounts: volumeMounts(), + }, + }, + }, + } + if cluster.Spec.ImagePullSecret != nil && *cluster.Spec.ImagePullSecret != "" { + podTemplateSpec.Spec.ImagePullSecrets = []v1.LocalObjectReference{ + {Name: *cluster.Spec.ImagePullSecret}, + } + } + k8sutil.AddOrUpdateStoragePodTolerations(&podTemplateSpec.Spec) + return podTemplateSpec, nil +} diff --git a/pkg/controller/portworxdiag/portworxdiag.go b/pkg/controller/portworxdiag/portworxdiag.go new file mode 100644 index 0000000000..1038e76711 --- /dev/null +++ b/pkg/controller/portworxdiag/portworxdiag.go @@ -0,0 +1,823 @@ +package portworxdiag + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/libopenstorage/openstorage/api" + pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" + apiextensionsops "github.com/portworx/sched-ops/k8s/apiextensions" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/record" + k8scontroller "k8s.io/kubernetes/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/libopenstorage/operator/drivers/storage" + corev1 "github.com/libopenstorage/operator/pkg/apis/core/v1" + diagv1 "github.com/libopenstorage/operator/pkg/apis/portworx/v1" + "github.com/libopenstorage/operator/pkg/util" + "github.com/libopenstorage/operator/pkg/util/k8s" +) + +const ( + // ControllerName is the name of the controller + ControllerName = "portworxdiag-controller" + validateCRDInterval = 5 * time.Second + validateCRDTimeout = 1 * time.Minute + crdBasePath = "/crds" + portworxDiagCRDFile = "portworx.io_portworxdiags.yaml" +) + +var _ reconcile.Reconciler = &Controller{} + +var ( + controllerKind = diagv1.SchemeGroupVersion.WithKind("PortworxDiag") + crdBaseDir = getCRDBasePath +) + +// Controller reconciles a StorageCluster object +type Controller struct { + client client.Client + scheme *runtime.Scheme + recorder record.EventRecorder + podControl k8scontroller.PodControlInterface + Driver storage.Driver + ctrl controller.Controller + grpcConn *grpc.ClientConn +} + +// Init initialize the portworx diag controller. +func (c *Controller) Init(mgr manager.Manager) error { + c.client = mgr.GetClient() + c.scheme = mgr.GetScheme() + c.recorder = mgr.GetEventRecorderFor(ControllerName) + + var err error + // Create a new controller + c.ctrl, err = controller.New(ControllerName, mgr, controller.Options{Reconciler: c}) + if err != nil { + return err + } + + clientset, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + return fmt.Errorf("error getting kubernetes client: %v", err) + } + // Create pod control interface object to manage pods under storage cluster + c.podControl = k8scontroller.RealPodControl{ + KubeClient: clientset, + Recorder: c.recorder, + } + + return nil +} + +// StartWatch starts the watch on the PortworxDiag object type. +func (c *Controller) StartWatch() error { + if c.ctrl == nil { + return fmt.Errorf("controller not initialized to start a watch") + } + + err := c.ctrl.Watch( + &source.Kind{Type: &diagv1.PortworxDiag{}}, + &handler.EnqueueRequestForObject{}, + ) + if err != nil { + return fmt.Errorf("failed to watch PortworxDiags: %v", err) + } + + // Watch for changes to Pods that belong to PortworxDiag object + err = c.ctrl.Watch( + &source.Kind{Type: &v1.Pod{}}, + &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &diagv1.PortworxDiag{}, + }, + ) + if err != nil { + return err + } + + return nil +} + +func (c *Controller) getDiagObject(ctx context.Context, req reconcile.Request) (*diagv1.PortworxDiag, bool, error) { + // List all PortworxDiag instances, pick out ours, and set it to "Pending" if other diags are running + diags := &diagv1.PortworxDiagList{} + err := c.client.List(context.TODO(), diags, &client.ListOptions{Namespace: req.Namespace}) + if err != nil { + // Error reading the objects - requeue the request (no items returns a successful empty list). + return nil, false, err + } + if len(diags.Items) == 0 { + return nil, false, nil + } + + // Sort all diags by creation timestamp + sort.Slice(diags.Items, func(i, j int) bool { + return diags.Items[i].CreationTimestamp.Before(&diags.Items[j].CreationTimestamp) + }) + + var diag *diagv1.PortworxDiag + otherDiagRunning := false + for _, d := range diags.Items { // Run diags in order of creation: if another is running before us, it was created earlier so let it go + if d.Name == req.Name { + diag = d.DeepCopy() + break + } + if d.Status.Phase == diagv1.DiagStatusInProgress { + otherDiagRunning = true + } + } + return diag, otherDiagRunning, nil +} + +func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := logrus.WithFields(map[string]interface{}{ + "Request.Namespace": req.Namespace, + "Request.Name": req.Name, + }) + log.Infof("Reconciling PortworxDiag") + + diag, otherDiagRunning, err := c.getDiagObject(ctx, req) + if err != nil { + return reconcile.Result{}, err + } + if diag == nil { + // Request objects not found, could have been deleted after reconcile request. + return reconcile.Result{}, nil + } + + if otherDiagRunning { + logrus.Infof("Other diag is running, waiting for it to complete before starting a new one") + err = c.patchPhase(diag, diagv1.DiagStatusPending, "Waiting for other PortworxDiag objects to complete before starting this one") + if err != nil { + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, err.Error()) + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil + } + + if err := c.syncPortworxDiag(diag); err != nil { + // Ignore object revision conflict errors, as PortworxDiag could have been edited. + // The next reconcile loop should be able to resolve the issue. + if strings.Contains(err.Error(), k8s.UpdateRevisionConflictErr) { + logrus.Warnf("failed to sync PortworxDiag %s: %v", req, err) + return reconcile.Result{}, nil + } + + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, err.Error()) + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil +} + +func (c *Controller) fetchSTC() (*corev1.StorageCluster, error) { + stcs := &corev1.StorageClusterList{} + err := c.client.List(context.TODO(), stcs, &client.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list StorageClusters: %v", err) + } + if len(stcs.Items) > 1 { + return nil, fmt.Errorf("more than one StorageCluster found, stopping") + } + if len(stcs.Items) == 0 { + return nil, fmt.Errorf("no StorageCluster found, stopping") + } + return &stcs.Items[0], nil +} + +func (c *Controller) getDiagPods(ns, diagName string) (*v1.PodList, error) { + pods := &v1.PodList{} + err := c.client.List(context.TODO(), pods, &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + "name": PortworxDiagLabel, + diagv1.LabelPortworxDiagName: diagName, + }), + Namespace: ns, + }) + if err == nil || errors.IsNotFound(err) { + return pods, nil + } + return nil, fmt.Errorf("failed to list existing diag pods: %v", err) +} + +func getNodeToPodMap(podList *v1.PodList) map[string]*v1.Pod { + pods := make(map[string]*v1.Pod) + if podList == nil { + return pods + } + for _, p := range podList.Items { + tmp := p // To avoid referencing the loop variable + pods[p.Spec.NodeName] = &tmp + } + return pods +} + +func getNodeIDToStatusMap(nodeStatuses []diagv1.NodeStatus) map[string]string { + statuses := make(map[string]string) + for _, s := range nodeStatuses { + if s.NodeID == "" { + continue + } + statuses[s.NodeID] = s.Status + } + return statuses +} + +func (c *Controller) getNodeIDsWithSelectedVolumes(diag *diagv1.PortworxDiag, stc *corev1.StorageCluster) ([]string, error) { + if diag == nil || diag.Spec.Portworx == nil || (diag.Spec.Portworx.VolumeSelector.IDs == nil && diag.Spec.Portworx.VolumeSelector.Labels == nil) { + return []string{}, nil + } + + nodeIDMap := map[string]bool{} + + volumeClient := api.NewOpenStorageVolumeClient(c.grpcConn) + ctx, err := pxutil.SetupContextWithToken(context.Background(), stc, c.client, false) + if err != nil { + return nil, err + } + + for _, id := range diag.Spec.Portworx.VolumeSelector.IDs { + + // Inspect volume to get location of replicas + volumeInspectResponse, err := volumeClient.Inspect( + ctx, + &api.SdkVolumeInspectRequest{ + VolumeId: id, + }, + ) + if err != nil { + logrus.Warnf("Failed to inspect volume %s: %v", id, err) + // Try the next volume + continue + } + + // Add all nodes that have a replica of this volume + for _, rs := range volumeInspectResponse.Volume.ReplicaSets { + for _, r := range rs.Nodes { + nodeIDMap[r] = true + } + } + } + + // Inspect all volumes by labels + if len(diag.Spec.Portworx.VolumeSelector.Labels) > 0 { + volumeInspectResponse, err := volumeClient.InspectWithFilters( + ctx, + &api.SdkVolumeInspectWithFiltersRequest{ + Labels: diag.Spec.Portworx.VolumeSelector.Labels, + }, + ) + if err != nil { + logrus.Warnf("Failed to inspect volumes with labels %v: %v", diag.Spec.Portworx.VolumeSelector.Labels, err) + } else { + for _, v := range volumeInspectResponse.Volumes { + + // Add all nodes that have a replica of this volume + for _, rs := range v.Volume.ReplicaSets { + for _, r := range rs.Nodes { + nodeIDMap[r] = true + } + } + } + } + } + + // Convert to slice + nodeIDs := make([]string, 0, len(nodeIDMap)) + for k := range nodeIDMap { + nodeIDs = append(nodeIDs, k) + } + + return nodeIDs, nil +} + +type podReconcileStatus struct { + podsToDelete []*v1.Pod + nodesToCreatePodsFor []string + nodeStatusesToAdd []*diagv1.NodeStatus +} + +func shouldPodBeOnNode(targetNodeID string, targetNodeName string, k8sNodes []v1.Node, nodeIDsWithReplicas []string, diag *diagv1.PortworxDiag) bool { + if diag == nil || diag.Spec.Portworx == nil { + // Just in case, sanity check + return false + } + + // If we're selecting all nodes, short-circuit + if diag.Spec.Portworx.NodeSelector.All { + return true + } + // Check if this is one of the nodes matching our volume selector + for _, id := range nodeIDsWithReplicas { + if id == targetNodeID { + return true + } + } + + // If there's no node selector and we're not selecting all nodes, collect none + if len(diag.Spec.Portworx.NodeSelector.IDs) == 0 && len(diag.Spec.Portworx.NodeSelector.Labels) == 0 { + return false + } + + // Filter by node ID first since it's the one we already have (save on API calls) + // If there's no IDs provided, do nothing and move on to the label match. + for _, id := range diag.Spec.Portworx.NodeSelector.IDs { + if id == targetNodeID { + return true + } + } + + // Node ID didn't match, check if node labels match label selectors + if len(diag.Spec.Portworx.NodeSelector.Labels) == 0 { + return false + } + + // Find node object in list + var node *v1.Node + for _, n := range k8sNodes { + if strings.EqualFold(n.Name, targetNodeName) { + nTemp := n // To avoid referencing the loop variable + node = &nTemp + break + } + } + + if node == nil { + return false + } + + // Check if node labels matches label selectors + for k, v := range diag.Spec.Portworx.NodeSelector.Labels { + label, ok := node.Labels[k] + if !ok { + return false // Label missing entirely + } + if label != v { + return false // Label values don't match + } + } + return true // All labels in selector passed +} + +func (c *Controller) getNodesToHavePods(nodes *v1.NodeList, diag *diagv1.PortworxDiag, stc *corev1.StorageCluster, nodeIDToNodeName map[string]string, nodeIDToStatus map[string]string) (map[string]bool, error) { + nodesToHavePods := map[string]bool{} + + // If we have node statuses populated already, use that as the master list to avoid recomputing labels + // If not, then do the hard work of calculating which nodes should have pods and reconcile based on that + if len(nodeIDToStatus) > 0 { + for nodeID := range nodeIDToStatus { + nodesToHavePods[nodeID] = true + } + return nodesToHavePods, nil + } + // If we have no spec to work with, return an empty list, nothing to do + if diag.Spec.Portworx == nil { + return nil, nil + } + + if diag.Spec.Portworx.NodeSelector.All { + // If we're selecting all nodes, just add all nodes + for nodeID := range nodeIDToNodeName { + nodesToHavePods[nodeID] = true + } + return nodesToHavePods, nil + } + + // If we're not selecting all nodes, get the list of nodes with volumes we care about + nodeIDsWithReplicas, err := c.getNodeIDsWithSelectedVolumes(diag, stc) + if err != nil { + return nil, err + } + + for nodeID, nodeName := range nodeIDToNodeName { + shouldExist := shouldPodBeOnNode(nodeID, nodeName, nodes.Items, nodeIDsWithReplicas, diag) + if shouldExist { + nodesToHavePods[nodeID] = true + } + } + + return nodesToHavePods, nil +} + +// getPodsDiff will return the pods that need to be created and deleted, as well as how many pods exist (but are not complete) and +// how many are complete +func (c *Controller) getPodsDiff(pods *v1.PodList, nodes *v1.NodeList, diag *diagv1.PortworxDiag, stc *corev1.StorageCluster, nodeIDToNodeName map[string]string) (podReconcileStatus, error) { + // Check on all of our storage nodes + prs := podReconcileStatus{ + podsToDelete: make([]*v1.Pod, 0), + nodesToCreatePodsFor: make([]string, 0), + nodeStatusesToAdd: make([]*diagv1.NodeStatus, 0), + } + + nodeToPod := getNodeToPodMap(pods) + nodeIDToStatus := getNodeIDToStatusMap(diag.Status.NodeStatuses) + + nodesToHavePods, err := c.getNodesToHavePods(nodes, diag, stc, nodeIDToNodeName, nodeIDToStatus) + if err != nil { + return prs, err + } + + for nodeID, nodeName := range nodeIDToNodeName { + existingPod := nodeToPod[nodeName] + + _, shouldExist := nodesToHavePods[nodeID] + + status, ok := nodeIDToStatus[nodeID] + if !ok && shouldExist { + // This node is missing a status in the Diag, go and add it + prs.nodeStatusesToAdd = append(prs.nodeStatusesToAdd, &diagv1.NodeStatus{NodeID: nodeID, Status: diagv1.NodeStatusPending, Message: ""}) + if existingPod == nil { + // Also create a pod if it's missing + prs.nodesToCreatePodsFor = append(prs.nodesToCreatePodsFor, nodeName) + } + continue + } + + // This node exists in the status + // Delete any pods that are complete/failed + if status == diagv1.NodeStatusCompleted || status == diagv1.NodeStatusFailed { + if existingPod != nil { + prs.podsToDelete = append(prs.podsToDelete, existingPod) + } + continue + } + + // If a pod shouldn't exist, don't add one + if !shouldExist { + // Delete any pods that shouldn't exist (shouldn't ever happen, but just in case) + if existingPod != nil { + prs.podsToDelete = append(prs.podsToDelete, existingPod) + } + continue + } + + // Diag should still be running... create pod if it's missing + if existingPod == nil { + prs.nodesToCreatePodsFor = append(prs.nodesToCreatePodsFor, nodeName) + } + continue + } + + return prs, nil +} + +func getOverallPhase(statuses []diagv1.NodeStatus) (string, string) { + // If all nodes are not yet started or empty: phase is "Not Yet Started" + // If all nodes in status are complete: phase is "Completed" + // If all nodes are failed: phase is "Failed" + // If all nodes are either complete or failed: phase is "Partial Failure" + // If at least one node is in progress: phase is "In Progress" + // Worst case, return an "unknown" status + + if len(statuses) == 0 { + return diagv1.DiagStatusPending, "" + } + + phaseCount := map[string]int{} + for _, n := range statuses { + if _, ok := phaseCount[n.Status]; !ok { + phaseCount[n.Status] = 1 + continue + } + phaseCount[n.Status] += 1 + } + + emptyPendingCount := 0 + if emptyCount, ok := phaseCount[""]; ok { + emptyPendingCount += emptyCount + } + if pendingCount, ok := phaseCount[diagv1.NodeStatusPending]; ok { + emptyPendingCount += pendingCount + } + + if emptyPendingCount == len(statuses) { + return diagv1.DiagStatusPending, "" + } + + completeCount, ok := phaseCount[diagv1.NodeStatusCompleted] + if ok && completeCount == len(statuses) { + return diagv1.DiagStatusCompleted, "All diags collected successfully" + } + + failedCount, ok := phaseCount[diagv1.NodeStatusFailed] + if ok && failedCount == len(statuses) { + return diagv1.DiagStatusFailed, "All diags failed to collect" + } + + // Count "Pending" pods as "Failed" here, as if all the others have finished it probably means it can't be scheduled + pendingCount, ok := phaseCount[diagv1.NodeStatusPending] + if !ok { + pendingCount = 0 + } + + if failedCount+pendingCount+completeCount == len(statuses) { + return diagv1.DiagStatusPartialFailure, "Some diags failed to collect" + } + + if inProgressCount, ok := phaseCount[diagv1.NodeStatusInProgress]; ok && inProgressCount > 0 { + return diagv1.DiagStatusInProgress, "Diag collection is in progress" + } + + return diagv1.DiagStatusUnknown, "" +} + +func getMissingStatusPatch(diag *diagv1.PortworxDiag) map[string]interface{} { + if diag.Status.Phase != "" || diag.Status.ClusterUUID != "" || diag.Status.NodeStatuses != nil { + return nil + } + return map[string]interface{}{ + "op": "add", + "path": "/status", + "value": diagv1.PortworxDiagStatus{Phase: diagv1.DiagStatusPending, NodeStatuses: []diagv1.NodeStatus{}}, + } +} + +func getChangedClusterUUIDPatch(diag *diagv1.PortworxDiag, stc *corev1.StorageCluster) map[string]interface{} { + if diag.Status.ClusterUUID == stc.Status.ClusterUID { + return nil + } + return map[string]interface{}{ + "op": "add", + "path": "/status/clusterUuid", + "value": stc.Status.ClusterUID, + } +} + +func getOverallPhasePatch(diag *diagv1.PortworxDiag) []map[string]interface{} { + patches := []map[string]interface{}{} + newPhase, newMessage := getOverallPhase(diag.Status.NodeStatuses) + logrus.Debugf("New phase for PortworxDiag is '%s'", newPhase) + if diag.Status.Phase != newPhase { + op := "add" + if diag.Status.Phase != "" { + op = "replace" + } + patches = append(patches, map[string]interface{}{ + "op": op, + "path": "/status/phase", + "value": newPhase, + }) + } + if diag.Status.Message != newMessage { + op := "add" + if diag.Status.Message != "" { + op = "replace" + } + patches = append(patches, map[string]interface{}{ + "op": op, + "path": "/status/message", + "value": newMessage, + }) + } + + return patches +} + +func getMissingNodeStatusesPatch(diag *diagv1.PortworxDiag, nodeStatusesToAdd []*diagv1.NodeStatus) []map[string]interface{} { + if len(nodeStatusesToAdd) == 0 { + return nil + } + patches := []map[string]interface{}{} + if diag.Status.NodeStatuses == nil { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/status/nodes", + "value": []diagv1.NodeStatus{}, + }) + } + for _, toAdd := range nodeStatusesToAdd { + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/status/nodes/-", + "value": toAdd, + }) + } + return patches +} + +func (c *Controller) updateDiagFields(diag *diagv1.PortworxDiag, stc *corev1.StorageCluster, prs *podReconcileStatus) error { + patches := []map[string]interface{}{} + + if patch := getMissingStatusPatch(diag); patch != nil { + patches = append(patches, patch) + } + + if patch := getChangedClusterUUIDPatch(diag, stc); patch != nil { + patches = append(patches, patch) + } + + if phasePatches := getOverallPhasePatch(diag); len(phasePatches) > 0 { + patches = append(patches, phasePatches...) + } + + if phasePatches := getMissingNodeStatusesPatch(diag, prs.nodeStatusesToAdd); len(phasePatches) > 0 { + patches = append(patches, phasePatches...) + } + + body, err := json.Marshal(patches) + if err != nil { + return fmt.Errorf("failed to marshal json patch to JSON: %v", err) + } + + err = c.client.Status().Patch(context.TODO(), diag, client.RawPatch(types.JSONPatchType, body)) + if err != nil { + return fmt.Errorf("failed to update phase for PortworxDiag CR: %v", err) + } + return nil +} + +func (c *Controller) patchPhase(diag *diagv1.PortworxDiag, newPhase string, newMessage string) error { + patches := []map[string]interface{}{} + + if patch := getMissingStatusPatch(diag); patch != nil { + patches = append(patches, patch) + } + patches = append(patches, map[string]interface{}{ + "op": "add", + "path": "/status/phase", + "value": newPhase, + }, map[string]interface{}{ + "op": "add", + "path": "/status/message", + "value": newMessage, + }) + body, err := json.Marshal(patches) + if err != nil { + return fmt.Errorf("failed to marshal json patch to JSON: %v", err) + } + err = c.client.Status().Patch(context.TODO(), diag, client.RawPatch(types.JSONPatchType, body)) + if err != nil { + return fmt.Errorf("failed to update phase for PortworxDiag CR: %v", err) + } + return nil +} + +func (c *Controller) syncPortworxDiag(diag *diagv1.PortworxDiag) error { + // If diag is already done, don't do any more work + switch diag.Status.Phase { + case diagv1.DiagStatusPartialFailure: + fallthrough + case diagv1.DiagStatusCompleted: + fallthrough + case diagv1.DiagStatusFailed: + logrus.Infof("PortworxDiag %s in namespace %s is already in status %s, no work to do", diag.Name, diag.Namespace, diag.Status.Phase) + return nil + default: + break + } + + logrus.Info("Enter syncPortworxDiag") + stc, err := c.fetchSTC() + if err != nil { + logrus.WithError(err).Error("Failed to find StorageCluster object") + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, fmt.Sprintf("Failed to find StorageCluster object: %v", err)) + patchErr := c.patchPhase(diag, diagv1.DiagStatusFailed, fmt.Sprintf("Failed to find StorageCluster object: %v", err)) + if patchErr != nil { + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, fmt.Sprintf("failed to patch PortworxDiag with %s status: %v", diagv1.DiagStatusFailed, patchErr)) + return fmt.Errorf("failed to patch PortworxDiag with %s status: %v", diagv1.DiagStatusFailed, patchErr) + } + + return fmt.Errorf("failed to find StorageCluster object: %v", err) + } + + if stc.Namespace != diag.Namespace { + logrus.Errorf("Diag %s in namespace %s is not in the same namespace as target cluster (namespace %s). Ensure the PortworxDiag object is created in the same namespace as the StorageCluster object", diag.Name, diag.Namespace, stc.Namespace) + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, fmt.Sprintf("Diag %s in namespace %s is not in the same namespace as target cluster (namespace %s). Ensure the PortworxDiag object is created in the same namespace as the StorageCluster object", diag.Name, diag.Namespace, stc.Namespace)) + err = c.patchPhase(diag, diagv1.DiagStatusFailed, fmt.Sprintf("Diag %s in namespace %s is not in the same namespace as target cluster (namespace %s). Ensure the PortworxDiag object is created in the same namespace as the StorageCluster object", diag.Name, diag.Namespace, stc.Namespace)) + if err != nil { + k8s.WarningEvent(c.recorder, diag, util.FailedSyncReason, fmt.Sprintf("failed to patch PortworxDiag with %s status: %v", diagv1.DiagStatusFailed, err)) + return fmt.Errorf("failed to patch PortworxDiag with %s status: %v", diagv1.DiagStatusFailed, err) + } + + return fmt.Errorf("diag %s in namespace %s is not in the same namespace as target cluster (namespace %s). Ensure the PortworxDiag object is created in the same namespace as the StorageCluster object", diag.Name, diag.Namespace, stc.Namespace) + } + + conn, err := pxutil.GetPortworxConn(c.grpcConn, c.client, diag.Namespace) + if err != nil { + logrus.WithError(err).Warn("Failed to open Portworx GRPC connection, future calls will use the k8s client which may have outdated info") + } + c.grpcConn = conn + + // GetStorageNodeMapping will properly handle if conn is nil + nodeNameToNodeID, nodeIDToNodeName, err := pxutil.GetStorageNodeMapping(stc, c.grpcConn, c.client) + if err != nil { + logrus.WithError(err).Error("Failed to get mapping from k8s nodes to Portworx node IDs") + return err + } + + pods, err := c.getDiagPods(diag.Namespace, diag.Name) + if err != nil { + return err + } + + // List all k8s nodes in the cluster, but only if we have a label selector + // Otherwise just pass an empty list through + nodes := &v1.NodeList{Items: []v1.Node{}} + if diag.Spec.Portworx.NodeSelector.Labels != nil { + err = c.client.List(context.Background(), nodes) + if err != nil { + logrus.WithError(err).Error("Failed to list nodes in cluster") + return fmt.Errorf("failed to list k8s nodes: %v", err) + } + } + + // Get what changes we need to make between real and desired + prs, err := c.getPodsDiff(pods, nodes, diag, stc, nodeIDToNodeName) + if err != nil { + logrus.WithError(err).Error("Failed to check pods for required operations") + return err + } + + err = c.updateDiagFields(diag, stc, &prs) + if err != nil { + logrus.WithError(err).Error("Failed to update status fields in PortworxDiag CR") + return err + } + + if len(prs.nodesToCreatePodsFor) > 0 { + logrus.Infof("Need to create diag pods for nodes: %v", prs.nodesToCreatePodsFor) + + // Create pods for these nodes + for _, nodeName := range prs.nodesToCreatePodsFor { + nodeID := nodeNameToNodeID[nodeName] + podTemplate, err := makeDiagPodTemplate(stc, diag, stc.Namespace, nodeName, nodeID) + if err != nil { + logrus.WithError(err).Errorf("Failed to create diags collection pod template") + k8s.WarningEvent(c.recorder, diag, "PodCreateFailed", fmt.Sprintf("Failed to create diags collection pod template: %v", err)) + continue + // Don't exit entirely, keep trying to create the rest + } + err = c.podControl.CreatePods(context.TODO(), diag.Namespace, podTemplate, diag, metav1.NewControllerRef(diag, controllerKind)) + if err != nil { + logrus.WithError(err).Warnf("Failed to create diags collection pod") + k8s.WarningEvent(c.recorder, diag, "PodCreateFailed", fmt.Sprintf("Failed to create diags collection pod: %v", err)) + // Don't exit entirely, keep trying to create the rest + } + } + } + // TODO: what do we do if there are extra untracked pods? Unlikely, but possible + if len(prs.podsToDelete) > 0 { + logrus.Infof("Need to delete %d completed diag pods", len(prs.podsToDelete)) + // If there are any pods to delete that are completed, delete them + for _, p := range prs.podsToDelete { + err = c.podControl.DeletePod(context.TODO(), p.Namespace, p.Name, diag) + if err != nil && !errors.IsNotFound(err) { + logrus.WithError(err).Warnf("Failed to delete completed diags collection pod, it may still hang around") + k8s.WarningEvent(c.recorder, diag, "PodDeleteFailed", fmt.Sprintf("Failed to delete completed diags collection pod: %v", err)) + // Don't exit, keep trying to clean up the rest + } + } + } + + logrus.Info("syncPortworxDiag completed successfully") + return nil +} + +// RegisterCRD registers and validates CRDs +func (c *Controller) RegisterCRD() error { + crd, err := k8s.GetCRDFromFile(portworxDiagCRDFile, crdBaseDir()) + if err != nil { + return err + } + latestCRD, err := apiextensionsops.Instance().GetCRD(crd.Name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + if err = apiextensionsops.Instance().RegisterCRD(crd); err != nil { + return err + } + } else if err != nil { + return err + } else { + crd.ResourceVersion = latestCRD.ResourceVersion + if _, err := apiextensionsops.Instance().UpdateCRD(crd); err != nil { + return err + } + } + + resource := fmt.Sprintf("%s.%s", crd.Spec.Names.Plural, crd.Spec.Group) + return apiextensionsops.Instance().ValidateCRD(resource, validateCRDTimeout, validateCRDInterval) +} + +func getCRDBasePath() string { + return crdBasePath +} diff --git a/pkg/mock/sdkserver.go b/pkg/mock/sdkserver.go index ff43bdf727..5092ada825 100644 --- a/pkg/mock/sdkserver.go +++ b/pkg/mock/sdkserver.go @@ -18,6 +18,7 @@ type SdkServers struct { Cluster *MockOpenStorageClusterServer Node *MockOpenStorageNodeServer Role *MockOpenStorageRoleServer + Volume *MockOpenStorageVolumeServer PortworxService *MockPortworxServiceServer ClusterDomains *MockOpenStorageClusterDomainsServer } diff --git a/tools.go b/tools.go index 84a9b7d508..fc62789da1 100644 --- a/tools.go +++ b/tools.go @@ -6,6 +6,8 @@ package tools import ( // Needed to generate client code for APIs _ "k8s.io/code-generator" + // Needed to generate CRD manifests and deepcopy functions for objects + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" // Needed because of https://github.com/golang/mock/tree/v1.6.0#reflect-vendoring-error _ "github.com/golang/mock/mockgen/model" ) diff --git a/vendor/github.com/gobuffalo/flect/.gitignore b/vendor/github.com/gobuffalo/flect/.gitignore new file mode 100644 index 0000000000..3689718594 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/.gitignore @@ -0,0 +1,29 @@ +*.log +.DS_Store +doc +tmp +pkg +*.gem +*.pid +coverage +coverage.data +build/* +*.pbxuser +*.mode1v3 +.svn +profile +.console_history +.sass-cache/* +.rake_tasks~ +*.log.lck +solr/ +.jhw-cache/ +jhw.* +*.sublime* +node_modules/ +dist/ +generated/ +.vendor/ +bin/* +gin-bin +.idea/ diff --git a/vendor/github.com/gobuffalo/flect/.gometalinter.json b/vendor/github.com/gobuffalo/flect/.gometalinter.json new file mode 100644 index 0000000000..e4f65a36e8 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/.gometalinter.json @@ -0,0 +1,3 @@ +{ + "Enable": ["vet", "golint", "goimports", "deadcode", "gotype", "ineffassign", "misspell", "nakedret", "unconvert", "megacheck", "varcheck"] +} diff --git a/vendor/github.com/gobuffalo/flect/LICENSE b/vendor/github.com/gobuffalo/flect/LICENSE new file mode 100644 index 0000000000..649efd4372 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Mark Bates + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/gobuffalo/flect/Makefile b/vendor/github.com/gobuffalo/flect/Makefile new file mode 100644 index 0000000000..0ac539f1c2 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/Makefile @@ -0,0 +1,61 @@ +TAGS ?= "" +GO_BIN ?= "go" + +install: + $(GO_BIN) install -tags ${TAGS} -v . + make tidy + +tidy: +ifeq ($(GO111MODULE),on) + $(GO_BIN) mod tidy +else + echo skipping go mod tidy +endif + +deps: + $(GO_BIN) get -tags ${TAGS} -t ./... + make tidy + +build: + $(GO_BIN) build -v . + make tidy + +test: + $(GO_BIN) test -cover -tags ${TAGS} ./... + make tidy + +ci-deps: + $(GO_BIN) get -tags ${TAGS} -t ./... + +ci-test: + $(GO_BIN) test -tags ${TAGS} -race ./... + +lint: + go get github.com/golangci/golangci-lint/cmd/golangci-lint + golangci-lint run --enable-all + make tidy + +update: +ifeq ($(GO111MODULE),on) + rm go.* + $(GO_BIN) mod init + $(GO_BIN) mod tidy +else + $(GO_BIN) get -u -tags ${TAGS} +endif + make test + make install + make tidy + +release-test: + $(GO_BIN) test -tags ${TAGS} -race ./... + make tidy + +release: + $(GO_BIN) get github.com/gobuffalo/release + make tidy + release -y -f version.go --skip-packr + make tidy + + + diff --git a/vendor/github.com/gobuffalo/flect/README.md b/vendor/github.com/gobuffalo/flect/README.md new file mode 100644 index 0000000000..2d9a1bd3b6 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/README.md @@ -0,0 +1,36 @@ +# Flect + +

+GoDoc +CI +Go Report Card +

+ +This is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. + +## Installation + +```bash +$ go get -u -v github.com/gobuffalo/flect +``` + +## `github.com/gobuffalo/flect` +GoDoc + +The `github.com/gobuffalo/flect` package contains "basic" inflection tools, like pluralization, singularization, etc... + +### The `Ident` Type + +In addition to helpful methods that take in a `string` and return a `string`, there is an `Ident` type that can be used to create new, custom, inflection rules. + +The `Ident` type contains two fields. + +* `Original` - This is the original `string` that was used to create the `Ident` +* `Parts` - This is a `[]string` that represents all of the "parts" of the string, that have been split apart, making the segments easier to work with + +Examples of creating new inflection rules using `Ident` can be found in the `github.com/gobuffalo/flect/name` package. + +## `github.com/gobuffalo/flect/name` +GoDoc + +The `github.com/gobuffalo/flect/name` package contains more "business" inflection rules like creating proper names, table names, etc... diff --git a/vendor/github.com/gobuffalo/flect/SHOULDERS.md b/vendor/github.com/gobuffalo/flect/SHOULDERS.md new file mode 100644 index 0000000000..8c359f157e --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/SHOULDERS.md @@ -0,0 +1,10 @@ +# github.com/gobuffalo/flect Stands on the Shoulders of Giants + +github.com/gobuffalo/flect does not try to reinvent the wheel! Instead, it uses the already great wheels developed by the Go community and puts them all together in the best way possible. Without these giants, this project would not be possible. Please make sure to check them out and thank them for all of their hard work. + +Thank you to the following **GIANTS**: + + +* [github.com/davecgh/go-spew](https://godoc.org/github.com/davecgh/go-spew) + +* [github.com/stretchr/testify](https://godoc.org/github.com/stretchr/testify) diff --git a/vendor/github.com/gobuffalo/flect/acronyms.go b/vendor/github.com/gobuffalo/flect/acronyms.go new file mode 100644 index 0000000000..b169724a4c --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/acronyms.go @@ -0,0 +1,152 @@ +package flect + +import "sync" + +var acronymsMoot = &sync.RWMutex{} + +var baseAcronyms = map[string]bool{ + "OK": true, + "UTF8": true, + "HTML": true, + "JSON": true, + "JWT": true, + "ID": true, + "UUID": true, + "SQL": true, + "ACK": true, + "ACL": true, + "ADSL": true, + "AES": true, + "ANSI": true, + "API": true, + "ARP": true, + "ATM": true, + "BGP": true, + "BSS": true, + "CCITT": true, + "CHAP": true, + "CIDR": true, + "CIR": true, + "CLI": true, + "CPE": true, + "CPU": true, + "CRC": true, + "CRT": true, + "CSMA": true, + "CMOS": true, + "DCE": true, + "DEC": true, + "DES": true, + "DHCP": true, + "DNS": true, + "DRAM": true, + "DSL": true, + "DSLAM": true, + "DTE": true, + "DMI": true, + "EHA": true, + "EIA": true, + "EIGRP": true, + "EOF": true, + "ESS": true, + "FCC": true, + "FCS": true, + "FDDI": true, + "FTP": true, + "GBIC": true, + "gbps": true, + "GEPOF": true, + "HDLC": true, + "HTTP": true, + "HTTPS": true, + "IANA": true, + "ICMP": true, + "IDF": true, + "IDS": true, + "IEEE": true, + "IETF": true, + "IMAP": true, + "IP": true, + "IPS": true, + "ISDN": true, + "ISP": true, + "kbps": true, + "LACP": true, + "LAN": true, + "LAPB": true, + "LAPF": true, + "LLC": true, + "MAC": true, + "Mbps": true, + "MC": true, + "MDF": true, + "MIB": true, + "MoCA": true, + "MPLS": true, + "MTU": true, + "NAC": true, + "NAT": true, + "NBMA": true, + "NIC": true, + "NRZ": true, + "NRZI": true, + "NVRAM": true, + "OSI": true, + "OSPF": true, + "OUI": true, + "PAP": true, + "PAT": true, + "PC": true, + "PIM": true, + "PCM": true, + "PDU": true, + "POP3": true, + "POTS": true, + "PPP": true, + "PPTP": true, + "PTT": true, + "PVST": true, + "RAM": true, + "RARP": true, + "RFC": true, + "RIP": true, + "RLL": true, + "ROM": true, + "RSTP": true, + "RTP": true, + "RCP": true, + "SDLC": true, + "SFD": true, + "SFP": true, + "SLARP": true, + "SLIP": true, + "SMTP": true, + "SNA": true, + "SNAP": true, + "SNMP": true, + "SOF": true, + "SRAM": true, + "SSH": true, + "SSID": true, + "STP": true, + "SYN": true, + "TDM": true, + "TFTP": true, + "TIA": true, + "TOFU": true, + "UDP": true, + "URL": true, + "URI": true, + "USB": true, + "UTP": true, + "VC": true, + "VLAN": true, + "VLSM": true, + "VPN": true, + "W3C": true, + "WAN": true, + "WEP": true, + "WiFi": true, + "WPA": true, + "WWW": true, +} diff --git a/vendor/github.com/gobuffalo/flect/camelize.go b/vendor/github.com/gobuffalo/flect/camelize.go new file mode 100644 index 0000000000..d8851c8f5e --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/camelize.go @@ -0,0 +1,44 @@ +package flect + +import ( + "strings" + "unicode" +) + +// Camelize returns a camelize version of a string +// bob dylan = bobDylan +// widget_id = widgetID +// WidgetID = widgetID +func Camelize(s string) string { + return New(s).Camelize().String() +} + +// Camelize returns a camelize version of a string +// bob dylan = bobDylan +// widget_id = widgetID +// WidgetID = widgetID +func (i Ident) Camelize() Ident { + var out []string + for i, part := range i.Parts { + var x string + var capped bool + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + if i == 0 { + x += string(unicode.ToLower(c)) + continue + } + if !capped { + capped = true + x += string(unicode.ToUpper(c)) + continue + } + x += string(c) + } + } + if x != "" { + out = append(out, x) + } + } + return New(strings.Join(out, "")) +} diff --git a/vendor/github.com/gobuffalo/flect/capitalize.go b/vendor/github.com/gobuffalo/flect/capitalize.go new file mode 100644 index 0000000000..78334fc0f9 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/capitalize.go @@ -0,0 +1,24 @@ +package flect + +import "unicode" + +// Capitalize will cap the first letter of string +// user = User +// bob dylan = Bob dylan +// widget_id = Widget_id +func Capitalize(s string) string { + return New(s).Capitalize().String() +} + +// Capitalize will cap the first letter of string +// user = User +// bob dylan = Bob dylan +// widget_id = Widget_id +func (i Ident) Capitalize() Ident { + if len(i.Parts) == 0 { + return New("") + } + runes := []rune(i.Original) + runes[0] = unicode.ToTitle(runes[0]) + return New(string(runes)) +} diff --git a/vendor/github.com/gobuffalo/flect/custom_data.go b/vendor/github.com/gobuffalo/flect/custom_data.go new file mode 100644 index 0000000000..efb445f671 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/custom_data.go @@ -0,0 +1,88 @@ +package flect + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func init() { + loadCustomData("inflections.json", "INFLECT_PATH", "could not read inflection file", LoadInflections) + loadCustomData("acronyms.json", "ACRONYMS_PATH", "could not read acronyms file", LoadAcronyms) +} + +//CustomDataParser are functions that parse data like acronyms or +//plurals in the shape of a io.Reader it receives. +type CustomDataParser func(io.Reader) error + +func loadCustomData(defaultFile, env, readErrorMessage string, parser CustomDataParser) { + pwd, _ := os.Getwd() + path, found := os.LookupEnv(env) + if !found { + path = filepath.Join(pwd, defaultFile) + } + + if _, err := os.Stat(path); err != nil { + return + } + + b, err := ioutil.ReadFile(path) + if err != nil { + fmt.Printf("%s %s (%s)\n", readErrorMessage, path, err) + return + } + + if err = parser(bytes.NewReader(b)); err != nil { + fmt.Println(err) + } +} + +//LoadAcronyms loads rules from io.Reader param +func LoadAcronyms(r io.Reader) error { + m := []string{} + err := json.NewDecoder(r).Decode(&m) + + if err != nil { + return fmt.Errorf("could not decode acronyms JSON from reader: %s", err) + } + + acronymsMoot.Lock() + defer acronymsMoot.Unlock() + + for _, acronym := range m { + baseAcronyms[acronym] = true + } + + return nil +} + +//LoadInflections loads rules from io.Reader param +func LoadInflections(r io.Reader) error { + m := map[string]string{} + + err := json.NewDecoder(r).Decode(&m) + if err != nil { + return fmt.Errorf("could not decode inflection JSON from reader: %s", err) + } + + pluralMoot.Lock() + defer pluralMoot.Unlock() + singularMoot.Lock() + defer singularMoot.Unlock() + + for s, p := range m { + if strings.Contains(s, " ") || strings.Contains(p, " ") { + // flect works with parts, so multi-words should not be allowed + return fmt.Errorf("inflection elements should be a single word") + } + singleToPlural[s] = p + pluralToSingle[p] = s + } + + return nil +} diff --git a/vendor/github.com/gobuffalo/flect/dasherize.go b/vendor/github.com/gobuffalo/flect/dasherize.go new file mode 100644 index 0000000000..c7a8a33e36 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/dasherize.go @@ -0,0 +1,34 @@ +package flect + +import ( + "strings" + "unicode" +) + +// Dasherize returns an alphanumeric, lowercased, dashed string +// Donald E. Knuth = donald-e-knuth +// Test with + sign = test-with-sign +// admin/WidgetID = admin-widget-id +func Dasherize(s string) string { + return New(s).Dasherize().String() +} + +// Dasherize returns an alphanumeric, lowercased, dashed string +// Donald E. Knuth = donald-e-knuth +// Test with + sign = test-with-sign +// admin/WidgetID = admin-widget-id +func (i Ident) Dasherize() Ident { + var parts []string + + for _, part := range i.Parts { + var x string + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + x += string(c) + } + } + parts = xappend(parts, x) + } + + return New(strings.ToLower(strings.Join(parts, "-"))) +} diff --git a/vendor/github.com/gobuffalo/flect/flect.go b/vendor/github.com/gobuffalo/flect/flect.go new file mode 100644 index 0000000000..ee81b6f2bc --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/flect.go @@ -0,0 +1,43 @@ +/* +Package flect is a new inflection engine to replace [https://github.com/markbates/inflect](https://github.com/markbates/inflect) designed to be more modular, more readable, and easier to fix issues on than the original. +*/ +package flect + +import ( + "strings" + "unicode" +) + +var spaces = []rune{'_', ' ', ':', '-', '/'} + +func isSpace(c rune) bool { + for _, r := range spaces { + if r == c { + return true + } + } + return unicode.IsSpace(c) +} + +func xappend(a []string, ss ...string) []string { + for _, s := range ss { + s = strings.TrimSpace(s) + for _, x := range spaces { + s = strings.Trim(s, string(x)) + } + if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { + s = strings.ToUpper(s) + } + if s != "" { + a = append(a, s) + } + } + return a +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/vendor/github.com/gobuffalo/flect/humanize.go b/vendor/github.com/gobuffalo/flect/humanize.go new file mode 100644 index 0000000000..6a0b75af7b --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/humanize.go @@ -0,0 +1,35 @@ +package flect + +import ( + "strings" +) + +// Humanize returns first letter of sentence capitalized. +// Common acronyms are capitalized as well. +// Other capital letters in string are left as provided. +// employee_salary = Employee salary +// employee_id = employee ID +// employee_mobile_number = Employee mobile number +// first_Name = First Name +// firstName = First Name +func Humanize(s string) string { + return New(s).Humanize().String() +} + +// Humanize First letter of sentence capitalized +func (i Ident) Humanize() Ident { + if len(i.Original) == 0 { + return New("") + } + + var parts []string + for index, part := range i.Parts { + if index == 0 { + part = strings.Title(i.Parts[0]) + } + + parts = xappend(parts, part) + } + + return New(strings.Join(parts, " ")) +} diff --git a/vendor/github.com/gobuffalo/flect/ident.go b/vendor/github.com/gobuffalo/flect/ident.go new file mode 100644 index 0000000000..9189e9a39b --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/ident.go @@ -0,0 +1,122 @@ +package flect + +import ( + "encoding" + "strings" + "unicode" + "unicode/utf8" +) + +// Ident represents the string and it's parts +type Ident struct { + Original string + Parts []string +} + +// String implements fmt.Stringer and returns the original string +func (i Ident) String() string { + return i.Original +} + +// New creates a new Ident from the string +func New(s string) Ident { + i := Ident{ + Original: s, + Parts: toParts(s), + } + + return i +} + +func toParts(s string) []string { + parts := []string{} + s = strings.TrimSpace(s) + if len(s) == 0 { + return parts + } + if _, ok := baseAcronyms[strings.ToUpper(s)]; ok { + return []string{strings.ToUpper(s)} + } + var prev rune + var x strings.Builder + x.Grow(len(s)) + for _, c := range s { + // fmt.Println("### cs ->", cs) + // fmt.Println("### unicode.IsControl(c) ->", unicode.IsControl(c)) + // fmt.Println("### unicode.IsDigit(c) ->", unicode.IsDigit(c)) + // fmt.Println("### unicode.IsGraphic(c) ->", unicode.IsGraphic(c)) + // fmt.Println("### unicode.IsLetter(c) ->", unicode.IsLetter(c)) + // fmt.Println("### unicode.IsLower(c) ->", unicode.IsLower(c)) + // fmt.Println("### unicode.IsMark(c) ->", unicode.IsMark(c)) + // fmt.Println("### unicode.IsPrint(c) ->", unicode.IsPrint(c)) + // fmt.Println("### unicode.IsPunct(c) ->", unicode.IsPunct(c)) + // fmt.Println("### unicode.IsSpace(c) ->", unicode.IsSpace(c)) + // fmt.Println("### unicode.IsTitle(c) ->", unicode.IsTitle(c)) + // fmt.Println("### unicode.IsUpper(c) ->", unicode.IsUpper(c)) + if !utf8.ValidRune(c) { + continue + } + + if isSpace(c) { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + + if unicode.IsUpper(c) && !unicode.IsUpper(prev) { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + if unicode.IsUpper(c) && baseAcronyms[strings.ToUpper(x.String())] { + parts = xappend(parts, x.String()) + x.Reset() + x.WriteRune(c) + prev = c + continue + } + if unicode.IsLetter(c) || unicode.IsDigit(c) || unicode.IsPunct(c) || c == '`' { + prev = c + x.WriteRune(c) + continue + } + + parts = xappend(parts, x.String()) + x.Reset() + prev = c + } + parts = xappend(parts, x.String()) + + return parts +} + +var _ encoding.TextUnmarshaler = &Ident{} +var _ encoding.TextMarshaler = &Ident{} + +// LastPart returns the last part/word of the original string +func (i *Ident) LastPart() string { + if len(i.Parts) == 0 { + return "" + } + return i.Parts[len(i.Parts)-1] +} + +// ReplaceSuffix creates a new Ident with the original suffix replaced by new +func (i Ident) ReplaceSuffix(orig, new string) Ident { + return New(strings.TrimSuffix(i.Original, orig) + new) +} + +//UnmarshalText unmarshalls byte array into the Ident +func (i *Ident) UnmarshalText(data []byte) error { + (*i) = New(string(data)) + return nil +} + +//MarshalText marshals Ident into byte array +func (i Ident) MarshalText() ([]byte, error) { + return []byte(i.Original), nil +} diff --git a/vendor/github.com/gobuffalo/flect/lower_upper.go b/vendor/github.com/gobuffalo/flect/lower_upper.go new file mode 100644 index 0000000000..930da58d8c --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/lower_upper.go @@ -0,0 +1,13 @@ +package flect + +import "strings" + +// ToUpper is a convience wrapper for strings.ToUpper +func (i Ident) ToUpper() Ident { + return New(strings.ToUpper(i.Original)) +} + +// ToLower is a convience wrapper for strings.ToLower +func (i Ident) ToLower() Ident { + return New(strings.ToLower(i.Original)) +} diff --git a/vendor/github.com/gobuffalo/flect/ordinalize.go b/vendor/github.com/gobuffalo/flect/ordinalize.go new file mode 100644 index 0000000000..1ce27b3a0d --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/ordinalize.go @@ -0,0 +1,43 @@ +package flect + +import ( + "fmt" + "strconv" +) + +// Ordinalize converts a number to an ordinal version +// 42 = 42nd +// 45 = 45th +// 1 = 1st +func Ordinalize(s string) string { + return New(s).Ordinalize().String() +} + +// Ordinalize converts a number to an ordinal version +// 42 = 42nd +// 45 = 45th +// 1 = 1st +func (i Ident) Ordinalize() Ident { + number, err := strconv.Atoi(i.Original) + if err != nil { + return i + } + var s string + switch abs(number) % 100 { + case 11, 12, 13: + s = fmt.Sprintf("%dth", number) + default: + switch abs(number) % 10 { + case 1: + s = fmt.Sprintf("%dst", number) + case 2: + s = fmt.Sprintf("%dnd", number) + case 3: + s = fmt.Sprintf("%drd", number) + } + } + if s != "" { + return New(s) + } + return New(fmt.Sprintf("%dth", number)) +} diff --git a/vendor/github.com/gobuffalo/flect/pascalize.go b/vendor/github.com/gobuffalo/flect/pascalize.go new file mode 100644 index 0000000000..6396d0d4e7 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/pascalize.go @@ -0,0 +1,32 @@ +package flect + +import ( + "strings" +) + +// Pascalize returns a string with each segment capitalized +// user = User +// bob dylan = BobDylan +// widget_id = WidgetID +func Pascalize(s string) string { + return New(s).Pascalize().String() +} + +// Pascalize returns a string with each segment capitalized +// user = User +// bob dylan = BobDylan +// widget_id = WidgetID +func (i Ident) Pascalize() Ident { + c := i.Camelize() + if len(c.String()) == 0 { + return c + } + if len(i.Parts) == 0 { + return i + } + capLen := 1 + if _, ok := baseAcronyms[strings.ToUpper(i.Parts[0])]; ok { + capLen = len(i.Parts[0]) + } + return New(string(strings.ToUpper(c.Original[0:capLen])) + c.Original[capLen:]) +} diff --git a/vendor/github.com/gobuffalo/flect/plural_rules.go b/vendor/github.com/gobuffalo/flect/plural_rules.go new file mode 100644 index 0000000000..7205ec028a --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/plural_rules.go @@ -0,0 +1,286 @@ +package flect + +var pluralRules = []rule{} + +// AddPlural adds a rule that will replace the given suffix with the replacement suffix. +func AddPlural(suffix string, repl string) { + pluralMoot.Lock() + defer pluralMoot.Unlock() + pluralRules = append([]rule{{ + suffix: suffix, + fn: func(s string) string { + s = s[:len(s)-len(suffix)] + return s + repl + }, + }}, pluralRules...) + + pluralRules = append([]rule{{ + suffix: repl, + fn: noop, + }}, pluralRules...) +} + +var singleToPlural = map[string]string{ + "aircraft": "aircraft", + "alias": "aliases", + "alumna": "alumnae", + "alumnus": "alumni", + "analysis": "analyses", + "antenna": "antennas", + "antithesis": "antitheses", + "apex": "apexes", + "appendix": "appendices", + "axis": "axes", + "bacillus": "bacilli", + "bacterium": "bacteria", + "basis": "bases", + "beau": "beaus", + "bison": "bison", + "bureau": "bureaus", + "bus": "buses", + "campus": "campuses", + "caucus": "caucuses", + "child": "children", + "château": "châteaux", + "circus": "circuses", + "codex": "codices", + "concerto": "concertos", + "corpus": "corpora", + "crisis": "crises", + "criterion": "criteria", + "curriculum": "curriculums", + "datum": "data", + "deer": "deer", + "diagnosis": "diagnoses", + "die": "dice", + "dwarf": "dwarves", + "ellipsis": "ellipses", + "equipment": "equipment", + "erratum": "errata", + "fez": "fezzes", + "fish": "fish", + "focus": "foci", + "foo": "foos", + "foot": "feet", + "formula": "formulas", + "fungus": "fungi", + "genus": "genera", + "goose": "geese", + "graffito": "graffiti", + "grouse": "grouse", + "half": "halves", + "halo": "halos", + "hoof": "hooves", + "human": "humans", + "hypothesis": "hypotheses", + "index": "indices", + "information": "information", + "jeans": "jeans", + "larva": "larvae", + "libretto": "librettos", + "loaf": "loaves", + "locus": "loci", + "louse": "lice", + "matrix": "matrices", + "medium": "media", + "minutia": "minutiae", + "money": "money", + "moose": "moose", + "mouse": "mice", + "nebula": "nebulae", + "news": "news", + "nucleus": "nuclei", + "oasis": "oases", + "octopus": "octopi", + "offspring": "offspring", + "opus": "opera", + "ovum": "ova", + "ox": "oxen", + "parenthesis": "parentheses", + "person": "people", + "phenomenon": "phenomena", + "photo": "photos", + "phylum": "phyla", + "piano": "pianos", + "plus": "pluses", + "police": "police", + "prognosis": "prognoses", + "prometheus": "prometheuses", + "quiz": "quizzes", + "quota": "quotas", + "radius": "radiuses", + "referendum": "referendums", + "ress": "resses", + "rice": "rice", + "salmon": "salmon", + "sex": "sexes", + "series": "series", + "sheep": "sheep", + "shoe": "shoes", + "shrimp": "shrimp", + "species": "species", + "stimulus": "stimuli", + "stratum": "strata", + "swine": "swine", + "syllabus": "syllabi", + "symposium": "symposiums", + "synapse": "synapses", + "synopsis": "synopses", + "tableau": "tableaus", + "testis": "testes", + "thesis": "theses", + "thief": "thieves", + "tooth": "teeth", + "trout": "trout", + "tuna": "tuna", + "vedalia": "vedalias", + "vertebra": "vertebrae", + "vertix": "vertices", + "vita": "vitae", + "vortex": "vortices", + "wharf": "wharves", + "wife": "wives", + "woman": "women", + "wolf": "wolves", + "you": "you", +} + +var pluralToSingle = map[string]string{} + +func init() { + for k, v := range singleToPlural { + pluralToSingle[v] = k + } +} + +type singularToPluralSuffix struct { + singular string + plural string +} + +var singularToPluralSuffixList = []singularToPluralSuffix{ + {"campus", "campuses"}, + {"person", "people"}, + {"phylum", "phyla"}, + {"randum", "randa"}, + {"actus", "acti"}, + {"adium", "adia"}, + {"basis", "basis"}, + {"child", "children"}, + {"chive", "chives"}, + {"focus", "foci"}, + {"genus", "genera"}, + {"hello", "hellos"}, + {"jeans", "jeans"}, + {"louse", "lice"}, + {"media", "media"}, + {"mouse", "mice"}, + {"movie", "movies"}, + {"oasis", "oasis"}, + {"atum", "ata"}, + {"atus", "atuses"}, + {"base", "bases"}, + {"cess", "cesses"}, + {"dium", "diums"}, + {"eses", "esis"}, + {"half", "halves"}, + {"hive", "hives"}, + {"iano", "ianos"}, + {"irus", "iri"}, + {"isis", "ises"}, + {"leus", "li"}, + {"mnus", "mni"}, + {"move", "moves"}, + {"news", "news"}, + {"odex", "odice"}, + {"oose", "eese"}, + {"ouse", "ouses"}, + {"ovum", "ova"}, + {"shoe", "shoes"}, + {"stis", "stes"}, + {"tive", "tives"}, + {"vice", "vices"}, + {"wife", "wives"}, + {"afe", "aves"}, + {"bfe", "bves"}, + {"box", "boxes"}, + {"cfe", "cves"}, + {"dfe", "dves"}, + {"dge", "dges"}, + {"efe", "eves"}, + {"gfe", "gves"}, + {"hfe", "hves"}, + {"ife", "ives"}, + {"itz", "itzes"}, + {"ium", "ia"}, + {"ize", "izes"}, + {"jfe", "jves"}, + {"kfe", "kves"}, + {"man", "men"}, + {"mfe", "mves"}, + {"nfe", "nves"}, + {"nna", "nnas"}, + {"oaf", "oaves"}, + {"oci", "ocus"}, + {"ode", "odes"}, + {"ofe", "oves"}, + {"pfe", "pves"}, + {"qfe", "qves"}, + {"quy", "quies"}, + {"rfe", "rves"}, + {"sfe", "sves"}, + {"tfe", "tves"}, + {"tum", "ta"}, + {"tus", "tuses"}, + {"ufe", "uves"}, + {"ula", "ulae"}, + {"ula", "ulas"}, + {"uli", "ulus"}, + {"use", "uses"}, + {"uss", "usses"}, + {"vfe", "vves"}, + {"wfe", "wves"}, + {"xfe", "xves"}, + {"yfe", "yves"}, + {"you", "you"}, + {"zfe", "zves"}, + {"by", "bies"}, + {"ch", "ches"}, + {"cy", "cies"}, + {"dy", "dies"}, + {"ex", "ices"}, + {"fy", "fies"}, + {"gy", "gies"}, + {"hy", "hies"}, + {"io", "ios"}, + {"jy", "jies"}, + {"ky", "kies"}, + {"lf", "lves"}, + {"ly", "lies"}, + {"my", "mies"}, + {"ny", "nies"}, + {"py", "pies"}, + {"qy", "qies"}, + {"rf", "rves"}, + {"ry", "ries"}, + {"sh", "shes"}, + {"ss", "sses"}, + {"sy", "sies"}, + {"ty", "ties"}, + {"tz", "tzes"}, + {"va", "vae"}, + {"vy", "vies"}, + {"wy", "wies"}, + {"xy", "xies"}, + {"zy", "zies"}, + {"zz", "zzes"}, + {"o", "oes"}, + {"x", "xes"}, +} + +func init() { + for i := len(singularToPluralSuffixList) - 1; i >= 0; i-- { + AddPlural(singularToPluralSuffixList[i].singular, singularToPluralSuffixList[i].plural) + AddSingular(singularToPluralSuffixList[i].plural, singularToPluralSuffixList[i].singular) + } +} diff --git a/vendor/github.com/gobuffalo/flect/pluralize.go b/vendor/github.com/gobuffalo/flect/pluralize.go new file mode 100644 index 0000000000..88f9ec5545 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/pluralize.go @@ -0,0 +1,62 @@ +package flect + +import ( + "strings" + "sync" +) + +var pluralMoot = &sync.RWMutex{} + +// Pluralize returns a plural version of the string +// user = users +// person = people +// datum = data +func Pluralize(s string) string { + return New(s).Pluralize().String() +} + +// PluralizeWithSize will pluralize a string taking a number number into account. +// PluralizeWithSize("user", 1) = user +// PluralizeWithSize("user", 2) = users +func PluralizeWithSize(s string, i int) string { + if i == 1 || i == -1 { + return New(s).Singularize().String() + } + return New(s).Pluralize().String() +} + +// Pluralize returns a plural version of the string +// user = users +// person = people +// datum = data +func (i Ident) Pluralize() Ident { + s := i.LastPart() + if len(s) == 0 { + return New("") + } + + pluralMoot.RLock() + defer pluralMoot.RUnlock() + + ls := strings.ToLower(s) + if _, ok := pluralToSingle[ls]; ok { + return i + } + if p, ok := singleToPlural[ls]; ok { + if s == Capitalize(s) { + p = Capitalize(p) + } + return i.ReplaceSuffix(s, p) + } + for _, r := range pluralRules { + if strings.HasSuffix(ls, r.suffix) { + return i.ReplaceSuffix(s, r.fn(s)) + } + } + + if strings.HasSuffix(ls, "s") { + return i + } + + return New(i.String() + "s") +} diff --git a/vendor/github.com/gobuffalo/flect/rule.go b/vendor/github.com/gobuffalo/flect/rule.go new file mode 100644 index 0000000000..dc616b337d --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/rule.go @@ -0,0 +1,10 @@ +package flect + +type ruleFn func(string) string + +type rule struct { + suffix string + fn ruleFn +} + +func noop(s string) string { return s } diff --git a/vendor/github.com/gobuffalo/flect/singular_rules.go b/vendor/github.com/gobuffalo/flect/singular_rules.go new file mode 100644 index 0000000000..9b85a873b9 --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/singular_rules.go @@ -0,0 +1,23 @@ +package flect + +var singularRules = []rule{} + +// AddSingular adds a rule that will replace the given suffix with the replacement suffix. +func AddSingular(ext string, repl string) { + singularMoot.Lock() + defer singularMoot.Unlock() + singularRules = append([]rule{{ + suffix: ext, + fn: func(s string) string { + s = s[:len(s)-len(ext)] + return s + repl + }, + }}, singularRules...) + + singularRules = append([]rule{{ + suffix: repl, + fn: func(s string) string { + return s + }, + }}, singularRules...) +} diff --git a/vendor/github.com/gobuffalo/flect/singularize.go b/vendor/github.com/gobuffalo/flect/singularize.go new file mode 100644 index 0000000000..244e382c3c --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/singularize.go @@ -0,0 +1,61 @@ +package flect + +import ( + "strings" + "sync" +) + +var singularMoot = &sync.RWMutex{} + +// Singularize returns a singular version of the string +// users = user +// data = datum +// people = person +func Singularize(s string) string { + return New(s).Singularize().String() +} + +// SingularizeWithSize will singular a string taking a number number into account. +// SingularizeWithSize("user", 1) = user +// SingularizeWithSize("user", 2) = users +func SingularizeWithSize(s string, i int) string { + if i == 1 || i == -1 { + return New(s).Singularize().String() + } + return New(s).Pluralize().String() +} + +// Singularize returns a singular version of the string +// users = user +// data = datum +// people = person +func (i Ident) Singularize() Ident { + s := i.LastPart() + if len(s) == 0 { + return i + } + + singularMoot.RLock() + defer singularMoot.RUnlock() + + ls := strings.ToLower(s) + if p, ok := pluralToSingle[ls]; ok { + if s == Capitalize(s) { + p = Capitalize(p) + } + return i.ReplaceSuffix(s, p) + } + if _, ok := singleToPlural[ls]; ok { + return i + } + for _, r := range singularRules { + if strings.HasSuffix(ls, r.suffix) { + return i.ReplaceSuffix(s, r.fn(s)) + } + } + + if strings.HasSuffix(s, "s") { + return i.ReplaceSuffix("s", "") + } + return i +} diff --git a/vendor/github.com/gobuffalo/flect/titleize.go b/vendor/github.com/gobuffalo/flect/titleize.go new file mode 100644 index 0000000000..cbbf08a5aa --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/titleize.go @@ -0,0 +1,30 @@ +package flect + +import ( + "strings" + "unicode" +) + +// Titleize will capitalize the start of each part +// "Nice to see you!" = "Nice To See You!" +// "i've read a book! have you?" = "I've Read A Book! Have You?" +// "This is `code` ok" = "This Is `code` OK" +func Titleize(s string) string { + return New(s).Titleize().String() +} + +// Titleize will capitalize the start of each part +// "Nice to see you!" = "Nice To See You!" +// "i've read a book! have you?" = "I've Read A Book! Have You?" +// "This is `code` ok" = "This Is `code` OK" +func (i Ident) Titleize() Ident { + var parts []string + for _, part := range i.Parts { + x := string(unicode.ToTitle(rune(part[0]))) + if len(part) > 1 { + x += part[1:] + } + parts = append(parts, x) + } + return New(strings.Join(parts, " ")) +} diff --git a/vendor/github.com/gobuffalo/flect/underscore.go b/vendor/github.com/gobuffalo/flect/underscore.go new file mode 100644 index 0000000000..d42859a59a --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/underscore.go @@ -0,0 +1,35 @@ +package flect + +import ( + "strings" + "unicode" +) + +// Underscore a string +// bob dylan --> bob_dylan +// Nice to see you! --> nice_to_see_you +// widgetID --> widget_id +func Underscore(s string) string { + return New(s).Underscore().String() +} + +// Underscore a string +// bob dylan --> bob_dylan +// Nice to see you! --> nice_to_see_you +// widgetID --> widget_id +func (i Ident) Underscore() Ident { + out := make([]string, 0, len(i.Parts)) + for _, part := range i.Parts { + var x strings.Builder + x.Grow(len(part)) + for _, c := range part { + if unicode.IsLetter(c) || unicode.IsDigit(c) { + x.WriteRune(c) + } + } + if x.Len() > 0 { + out = append(out, x.String()) + } + } + return New(strings.ToLower(strings.Join(out, "_"))) +} diff --git a/vendor/github.com/gobuffalo/flect/version.go b/vendor/github.com/gobuffalo/flect/version.go new file mode 100644 index 0000000000..9624d5df2d --- /dev/null +++ b/vendor/github.com/gobuffalo/flect/version.go @@ -0,0 +1,4 @@ +package flect + +//Version holds Flect version number +const Version = "v0.1.6" diff --git a/vendor/github.com/inconshreveable/mousetrap/LICENSE b/vendor/github.com/inconshreveable/mousetrap/LICENSE new file mode 100644 index 0000000000..5f920e9732 --- /dev/null +++ b/vendor/github.com/inconshreveable/mousetrap/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Alan Shreve (@inconshreveable) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/inconshreveable/mousetrap/README.md b/vendor/github.com/inconshreveable/mousetrap/README.md new file mode 100644 index 0000000000..7a950d1774 --- /dev/null +++ b/vendor/github.com/inconshreveable/mousetrap/README.md @@ -0,0 +1,23 @@ +# mousetrap + +mousetrap is a tiny library that answers a single question. + +On a Windows machine, was the process invoked by someone double clicking on +the executable file while browsing in explorer? + +### Motivation + +Windows developers unfamiliar with command line tools will often "double-click" +the executable for a tool. Because most CLI tools print the help and then exit +when invoked without arguments, this is often very frustrating for those users. + +mousetrap provides a way to detect these invocations so that you can provide +more helpful behavior and instructions on how to run the CLI tool. To see what +this looks like, both from an organizational and a technical perspective, see +https://inconshreveable.com/09-09-2014/sweat-the-small-stuff/ + +### The interface + +The library exposes a single interface: + + func StartedByExplorer() (bool) diff --git a/vendor/github.com/inconshreveable/mousetrap/trap_others.go b/vendor/github.com/inconshreveable/mousetrap/trap_others.go new file mode 100644 index 0000000000..06a91f0868 --- /dev/null +++ b/vendor/github.com/inconshreveable/mousetrap/trap_others.go @@ -0,0 +1,16 @@ +//go:build !windows +// +build !windows + +package mousetrap + +// StartedByExplorer returns true if the program was invoked by the user +// double-clicking on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +// +// On non-Windows platforms, it always returns false. +func StartedByExplorer() bool { + return false +} diff --git a/vendor/github.com/inconshreveable/mousetrap/trap_windows.go b/vendor/github.com/inconshreveable/mousetrap/trap_windows.go new file mode 100644 index 0000000000..0c56880216 --- /dev/null +++ b/vendor/github.com/inconshreveable/mousetrap/trap_windows.go @@ -0,0 +1,42 @@ +package mousetrap + +import ( + "syscall" + "unsafe" +) + +func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { + snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, err + } + defer syscall.CloseHandle(snapshot) + var procEntry syscall.ProcessEntry32 + procEntry.Size = uint32(unsafe.Sizeof(procEntry)) + if err = syscall.Process32First(snapshot, &procEntry); err != nil { + return nil, err + } + for { + if procEntry.ProcessID == uint32(pid) { + return &procEntry, nil + } + err = syscall.Process32Next(snapshot, &procEntry) + if err != nil { + return nil, err + } + } +} + +// StartedByExplorer returns true if the program was invoked by the user double-clicking +// on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +func StartedByExplorer() bool { + pe, err := getProcessEntry(syscall.Getppid()) + if err != nil { + return false + } + return "explorer.exe" == syscall.UTF16ToString(pe.ExeFile[:]) +} diff --git a/vendor/github.com/spf13/cobra/.gitignore b/vendor/github.com/spf13/cobra/.gitignore new file mode 100644 index 0000000000..c7b459e4dd --- /dev/null +++ b/vendor/github.com/spf13/cobra/.gitignore @@ -0,0 +1,39 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +# Vim files https://github.com/github/gitignore/blob/master/Global/Vim.gitignore +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +*.exe +cobra.test +bin + +.idea/ +*.iml diff --git a/vendor/github.com/spf13/cobra/.golangci.yml b/vendor/github.com/spf13/cobra/.golangci.yml new file mode 100644 index 0000000000..2578d94b5e --- /dev/null +++ b/vendor/github.com/spf13/cobra/.golangci.yml @@ -0,0 +1,62 @@ +# Copyright 2013-2023 The Cobra Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +run: + deadline: 5m + +linters: + disable-all: true + enable: + #- bodyclose + - deadcode + #- depguard + #- dogsled + #- dupl + - errcheck + #- exhaustive + #- funlen + - gas + #- gochecknoinits + - goconst + #- gocritic + #- gocyclo + #- gofmt + - goimports + - golint + #- gomnd + #- goprintffuncname + #- gosec + #- gosimple + - govet + - ineffassign + - interfacer + #- lll + - maligned + - megacheck + #- misspell + #- nakedret + #- noctx + #- nolintlint + #- rowserrcheck + #- scopelint + #- staticcheck + - structcheck + #- stylecheck + #- typecheck + - unconvert + #- unparam + #- unused + - varcheck + #- whitespace + fast: false diff --git a/vendor/github.com/spf13/cobra/.mailmap b/vendor/github.com/spf13/cobra/.mailmap new file mode 100644 index 0000000000..94ec53068a --- /dev/null +++ b/vendor/github.com/spf13/cobra/.mailmap @@ -0,0 +1,3 @@ +Steve Francia +Bjørn Erik Pedersen +Fabiano Franz diff --git a/vendor/github.com/spf13/cobra/CONDUCT.md b/vendor/github.com/spf13/cobra/CONDUCT.md new file mode 100644 index 0000000000..9d16f88fd1 --- /dev/null +++ b/vendor/github.com/spf13/cobra/CONDUCT.md @@ -0,0 +1,37 @@ +## Cobra User Contract + +### Versioning +Cobra will follow a steady release cadence. Non breaking changes will be released as minor versions quarterly. Patch bug releases are at the discretion of the maintainers. Users can expect security patch fixes to be released within relatively short order of a CVE becoming known. For more information on security patch fixes see the CVE section below. Releases will follow [Semantic Versioning](https://semver.org/). Users tracking the Master branch should expect unpredictable breaking changes as the project continues to move forward. For stability, it is highly recommended to use a release. + +### Backward Compatibility +We will maintain two major releases in a moving window. The N-1 release will only receive bug fixes and security updates and will be dropped once N+1 is released. + +### Deprecation +Deprecation of Go versions or dependent packages will only occur in major releases. To reduce the change of this taking users by surprise, any large deprecation will be preceded by an announcement in the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) and an Issue on Github. + +### CVE +Maintainers will make every effort to release security patches in the case of a medium to high severity CVE directly impacting the library. The speed in which these patches reach a release is up to the discretion of the maintainers. A low severity CVE may be a lower priority than a high severity one. + +### Communication +Cobra maintainers will use GitHub issues and the [#cobra slack channel](https://gophers.slack.com/archives/CD3LP1199) as the primary means of communication with the community. This is to foster open communication with all users and contributors. + +### Breaking Changes +Breaking changes are generally allowed in the master branch, as this is the branch used to develop the next release of Cobra. + +There may be times, however, when master is closed for breaking changes. This is likely to happen as we near the release of a new version. + +Breaking changes are not allowed in release branches, as these represent minor versions that have already been released. These version have consumers who expect the APIs, behaviors, etc, to remain stable during the lifetime of the patch stream for the minor release. + +Examples of breaking changes include: +- Removing or renaming exported constant, variable, type, or function. +- Updating the version of critical libraries such as `spf13/pflag`, `spf13/viper` etc... + - Some version updates may be acceptable for picking up bug fixes, but maintainers must exercise caution when reviewing. + +There may, at times, need to be exceptions where breaking changes are allowed in release branches. These are at the discretion of the project's maintainers, and must be carefully considered before merging. + +### CI Testing +Maintainers will ensure the Cobra test suite utilizes the current supported versions of Golang. + +### Disclaimer +Changes to this document and the contents therein are at the discretion of the maintainers. +None of the contents of this document are legally binding in any way to the maintainers or the users. diff --git a/vendor/github.com/spf13/cobra/CONTRIBUTING.md b/vendor/github.com/spf13/cobra/CONTRIBUTING.md new file mode 100644 index 0000000000..6f356e6a82 --- /dev/null +++ b/vendor/github.com/spf13/cobra/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to Cobra + +Thank you so much for contributing to Cobra. We appreciate your time and help. +Here are some guidelines to help you get started. + +## Code of Conduct + +Be kind and respectful to the members of the community. Take time to educate +others who are seeking help. Harassment of any kind will not be tolerated. + +## Questions + +If you have questions regarding Cobra, feel free to ask it in the community +[#cobra Slack channel][cobra-slack] + +## Filing a bug or feature + +1. Before filing an issue, please check the existing issues to see if a + similar one was already opened. If there is one already opened, feel free + to comment on it. +1. If you believe you've found a bug, please provide detailed steps of + reproduction, the version of Cobra and anything else you believe will be + useful to help troubleshoot it (e.g. OS environment, environment variables, + etc...). Also state the current behavior vs. the expected behavior. +1. If you'd like to see a feature or an enhancement please open an issue with + a clear title and description of what the feature is and why it would be + beneficial to the project and its users. + +## Submitting changes + +1. CLA: Upon submitting a Pull Request (PR), contributors will be prompted to + sign a CLA. Please sign the CLA :slightly_smiling_face: +1. Tests: If you are submitting code, please ensure you have adequate tests + for the feature. Tests can be run via `go test ./...` or `make test`. +1. Since this is golang project, ensure the new code is properly formatted to + ensure code consistency. Run `make all`. + +### Quick steps to contribute + +1. Fork the project. +1. Download your fork to your PC (`git clone https://github.com/your_username/cobra && cd cobra`) +1. Create your feature branch (`git checkout -b my-new-feature`) +1. Make changes and run tests (`make test`) +1. Add them to staging (`git add .`) +1. Commit your changes (`git commit -m 'Add some feature'`) +1. Push to the branch (`git push origin my-new-feature`) +1. Create new pull request + + +[cobra-slack]: https://gophers.slack.com/archives/CD3LP1199 diff --git a/vendor/github.com/spf13/cobra/LICENSE.txt b/vendor/github.com/spf13/cobra/LICENSE.txt new file mode 100644 index 0000000000..298f0e2665 --- /dev/null +++ b/vendor/github.com/spf13/cobra/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/spf13/cobra/MAINTAINERS b/vendor/github.com/spf13/cobra/MAINTAINERS new file mode 100644 index 0000000000..4c5ac3dd99 --- /dev/null +++ b/vendor/github.com/spf13/cobra/MAINTAINERS @@ -0,0 +1,13 @@ +maintainers: +- spf13 +- johnSchnake +- jpmcb +- marckhouzam +inactive: +- anthonyfok +- bep +- bogem +- broady +- eparis +- jharshman +- wfernandes diff --git a/vendor/github.com/spf13/cobra/Makefile b/vendor/github.com/spf13/cobra/Makefile new file mode 100644 index 0000000000..0da8d7aa08 --- /dev/null +++ b/vendor/github.com/spf13/cobra/Makefile @@ -0,0 +1,35 @@ +BIN="./bin" +SRC=$(shell find . -name "*.go") + +ifeq (, $(shell which golangci-lint)) +$(warning "could not find golangci-lint in $(PATH), run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh") +endif + +.PHONY: fmt lint test install_deps clean + +default: all + +all: fmt test + +fmt: + $(info ******************** checking formatting ********************) + @test -z $(shell gofmt -l $(SRC)) || (gofmt -d $(SRC); exit 1) + +lint: + $(info ******************** running lint tools ********************) + golangci-lint run -v + +test: install_deps + $(info ******************** running tests ********************) + go test -v ./... + +richtest: install_deps + $(info ******************** running tests with kyoh86/richgo ********************) + richgo test -v ./... + +install_deps: + $(info ******************** downloading dependencies ********************) + go get -v ./... + +clean: + rm -rf $(BIN) diff --git a/vendor/github.com/spf13/cobra/README.md b/vendor/github.com/spf13/cobra/README.md new file mode 100644 index 0000000000..592c0b8ab0 --- /dev/null +++ b/vendor/github.com/spf13/cobra/README.md @@ -0,0 +1,112 @@ +![cobra logo](assets/CobraMain.png) + +Cobra is a library for creating powerful modern CLI applications. + +Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), +[Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to +name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra. + +[![](https://img.shields.io/github/actions/workflow/status/spf13/cobra/test.yml?branch=main&longCache=true&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest) +[![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra) +[![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra) +[![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199) + +# Overview + +Cobra is a library providing a simple interface to create powerful modern CLI +interfaces similar to git & go tools. + +Cobra provides: +* Easy subcommand-based CLIs: `app server`, `app fetch`, etc. +* Fully POSIX-compliant flags (including short & long versions) +* Nested subcommands +* Global, local and cascading flags +* Intelligent suggestions (`app srver`... did you mean `app server`?) +* Automatic help generation for commands and flags +* Grouping help for subcommands +* Automatic help flag recognition of `-h`, `--help`, etc. +* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell) +* Automatically generated man pages for your application +* Command aliases so you can change things without breaking them +* The flexibility to define your own help, usage, etc. +* Optional seamless integration with [viper](https://github.com/spf13/viper) for 12-factor apps + +# Concepts + +Cobra is built on a structure of commands, arguments & flags. + +**Commands** represent actions, **Args** are things and **Flags** are modifiers for those actions. + +The best applications read like sentences when used, and as a result, users +intuitively know how to interact with them. + +The pattern to follow is +`APPNAME VERB NOUN --ADJECTIVE` + or +`APPNAME COMMAND ARG --FLAG`. + +A few good real world examples may better illustrate this point. + +In the following example, 'server' is a command, and 'port' is a flag: + + hugo server --port=1313 + +In this command we are telling Git to clone the url bare. + + git clone URL --bare + +## Commands + +Command is the central point of the application. Each interaction that +the application supports will be contained in a Command. A command can +have children commands and optionally run an action. + +In the example above, 'server' is the command. + +[More about cobra.Command](https://pkg.go.dev/github.com/spf13/cobra#Command) + +## Flags + +A flag is a way to modify the behavior of a command. Cobra supports +fully POSIX-compliant flags as well as the Go [flag package](https://golang.org/pkg/flag/). +A Cobra command can define flags that persist through to children commands +and flags that are only available to that command. + +In the example above, 'port' is the flag. + +Flag functionality is provided by the [pflag +library](https://github.com/spf13/pflag), a fork of the flag standard library +which maintains the same interface while adding POSIX compliance. + +# Installing +Using Cobra is easy. First, use `go get` to install the latest version +of the library. + +``` +go get -u github.com/spf13/cobra@latest +``` + +Next, include Cobra in your application: + +```go +import "github.com/spf13/cobra" +``` + +# Usage +`cobra-cli` is a command line program to generate cobra applications and command files. +It will bootstrap your application scaffolding to rapidly +develop a Cobra-based application. It is the easiest way to incorporate Cobra into your application. + +It can be installed by running: + +``` +go install github.com/spf13/cobra-cli@latest +``` + +For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md) + +For complete details on using the Cobra library, please read the [The Cobra User Guide](user_guide.md). + +# License + +Cobra is released under the Apache 2.0 license. See [LICENSE.txt](https://github.com/spf13/cobra/blob/master/LICENSE.txt) diff --git a/vendor/github.com/spf13/cobra/active_help.go b/vendor/github.com/spf13/cobra/active_help.go new file mode 100644 index 0000000000..2d0239437a --- /dev/null +++ b/vendor/github.com/spf13/cobra/active_help.go @@ -0,0 +1,63 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "fmt" + "os" + "strings" +) + +const ( + activeHelpMarker = "_activeHelp_ " + // The below values should not be changed: programs will be using them explicitly + // in their user documentation, and users will be using them explicitly. + activeHelpEnvVarSuffix = "_ACTIVE_HELP" + activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP" + activeHelpGlobalDisable = "0" +) + +// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. +// Such strings will be processed by the completion script and will be shown as ActiveHelp +// to the user. +// The array parameter should be the array that will contain the completions. +// This function can be called multiple times before and/or after completions are added to +// the array. Each time this function is called with the same array, the new +// ActiveHelp line will be shown below the previous ones when completion is triggered. +func AppendActiveHelp(compArray []string, activeHelpStr string) []string { + return append(compArray, fmt.Sprintf("%s%s", activeHelpMarker, activeHelpStr)) +} + +// GetActiveHelpConfig returns the value of the ActiveHelp environment variable +// _ACTIVE_HELP where is the name of the root command in upper +// case, with all - replaced by _. +// It will always return "0" if the global environment variable COBRA_ACTIVE_HELP +// is set to "0". +func GetActiveHelpConfig(cmd *Command) string { + activeHelpCfg := os.Getenv(activeHelpGlobalEnvVar) + if activeHelpCfg != activeHelpGlobalDisable { + activeHelpCfg = os.Getenv(activeHelpEnvVar(cmd.Root().Name())) + } + return activeHelpCfg +} + +// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment +// variable. It has the format _ACTIVE_HELP where is the name of the +// root command in upper case, with all - replaced by _. +func activeHelpEnvVar(name string) string { + // This format should not be changed: users will be using it explicitly. + activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) + return strings.ReplaceAll(activeHelpEnvVar, "-", "_") +} diff --git a/vendor/github.com/spf13/cobra/active_help.md b/vendor/github.com/spf13/cobra/active_help.md new file mode 100644 index 0000000000..5e7f59af38 --- /dev/null +++ b/vendor/github.com/spf13/cobra/active_help.md @@ -0,0 +1,157 @@ +# Active Help + +Active Help is a framework provided by Cobra which allows a program to define messages (hints, warnings, etc) that will be printed during program usage. It aims to make it easier for your users to learn how to use your program. If configured by the program, Active Help is printed when the user triggers shell completion. + +For example, +``` +bash-5.1$ helm repo add [tab] +You must choose a name for the repo you are adding. + +bash-5.1$ bin/helm package [tab] +Please specify the path to the chart to package + +bash-5.1$ bin/helm package [tab][tab] +bin/ internal/ scripts/ pkg/ testdata/ +``` + +**Hint**: A good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions to guide the user in knowing what is expected by the program. +## Supported shells + +Active Help is currently only supported for the following shells: +- Bash (using [bash completion V2](shell_completions.md#bash-completion-v2) only). Note that bash 4.4 or higher is required for the prompt to appear when an Active Help message is printed. +- Zsh + +## Adding Active Help messages + +As Active Help uses the shell completion system, the implementation of Active Help messages is done by enhancing custom dynamic completions. If you are not familiar with dynamic completions, please refer to [Shell Completions](shell_completions.md). + +Adding Active Help is done through the use of the `cobra.AppendActiveHelp(...)` function, where the program repeatedly adds Active Help messages to the list of completions. Keep reading for details. + +### Active Help for nouns + +Adding Active Help when completing a noun is done within the `ValidArgsFunction(...)` of a command. Please notice the use of `cobra.AppendActiveHelp(...)` in the following example: + +```go +cmd := &cobra.Command{ + Use: "add [NAME] [URL]", + Short: "add a chart repository", + Args: require.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return addRepo(args) + }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + if len(args) == 0 { + comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") + } else if len(args) == 1 { + comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") + } else { + comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") + } + return comps, cobra.ShellCompDirectiveNoFileComp + }, +} +``` +The example above defines the completions (none, in this specific example) as well as the Active Help messages for the `helm repo add` command. It yields the following behavior: +``` +bash-5.1$ helm repo add [tab] +You must choose a name for the repo you are adding + +bash-5.1$ helm repo add grafana [tab] +You must specify the URL for the repo you are adding + +bash-5.1$ helm repo add grafana https://grafana.github.io/helm-charts [tab] +This command does not take any more arguments +``` +**Hint**: As can be seen in the above example, a good place to use Active Help messages is when the normal completion system does not provide any suggestions. In such cases, Active Help nicely supplements the normal shell completions. + +### Active Help for flags + +Providing Active Help for flags is done in the same fashion as for nouns, but using the completion function registered for the flag. For example: +```go +_ = cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 2 { + return cobra.AppendActiveHelp(nil, "You must first specify the chart to install before the --version flag can be completed"), cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[1], toComplete) + }) +``` +The example above prints an Active Help message when not enough information was given by the user to complete the `--version` flag. +``` +bash-5.1$ bin/helm install myrelease --version 2.0.[tab] +You must first specify the chart to install before the --version flag can be completed + +bash-5.1$ bin/helm install myrelease bitnami/solr --version 2.0.[tab][tab] +2.0.1 2.0.2 2.0.3 +``` + +## User control of Active Help + +You may want to allow your users to disable Active Help or choose between different levels of Active Help. It is entirely up to the program to define the type of configurability of Active Help that it wants to offer, if any. +Allowing to configure Active Help is entirely optional; you can use Active Help in your program without doing anything about Active Help configuration. + +The way to configure Active Help is to use the program's Active Help environment +variable. That variable is named `_ACTIVE_HELP` where `` is the name of your +program in uppercase with any `-` replaced by an `_`. The variable should be set by the user to whatever +Active Help configuration values are supported by the program. + +For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user +would set the desired behavior to `local` by doing `export HELM_ACTIVE_HELP=local` in their shell. + +For simplicity, when in `cmd.ValidArgsFunction(...)` or a flag's completion function, the program should read the +Active Help configuration using the `cobra.GetActiveHelpConfig(cmd)` function and select what Active Help messages +should or should not be added (instead of reading the environment variable directly). + +For example: +```go +ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + activeHelpLevel := cobra.GetActiveHelpConfig(cmd) + + var comps []string + if len(args) == 0 { + if activeHelpLevel != "off" { + comps = cobra.AppendActiveHelp(comps, "You must choose a name for the repo you are adding") + } + } else if len(args) == 1 { + if activeHelpLevel != "off" { + comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repo you are adding") + } + } else { + if activeHelpLevel == "local" { + comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments") + } + } + return comps, cobra.ShellCompDirectiveNoFileComp +}, +``` +**Note 1**: If the `_ACTIVE_HELP` environment variable is set to the string "0", Cobra will automatically disable all Active Help output (even if some output was specified by the program using the `cobra.AppendActiveHelp(...)` function). Using "0" can simplify your code in situations where you want to blindly disable Active Help without having to call `cobra.GetActiveHelpConfig(cmd)` explicitly. + +**Note 2**: If a user wants to disable Active Help for every single program based on Cobra, she can set the environment variable `COBRA_ACTIVE_HELP` to "0". In this case `cobra.GetActiveHelpConfig(cmd)` will return "0" no matter what the variable `_ACTIVE_HELP` is set to. + +**Note 3**: If the user does not set `_ACTIVE_HELP` or `COBRA_ACTIVE_HELP` (which will be a common case), the default value for the Active Help configuration returned by `cobra.GetActiveHelpConfig(cmd)` will be the empty string. +## Active Help with Cobra's default completion command + +Cobra provides a default `completion` command for programs that wish to use it. +When using the default `completion` command, Active Help is configurable in the same +fashion as described above using environment variables. You may wish to document this in more +details for your users. + +## Debugging Active Help + +Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](shell_completions.md#debugging) for details. + +When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below: +``` +$ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h +bitnami/haproxy +bitnami/harbor +_activeHelp_ WARNING: cannot re-use a name that is still in use +:0 +Completion ended with directive: ShellCompDirectiveDefault + +$ HELM_ACTIVE_HELP=0 bin/helm __complete install wordpress bitnami/h +bitnami/haproxy +bitnami/harbor +:0 +Completion ended with directive: ShellCompDirectiveDefault +``` diff --git a/vendor/github.com/spf13/cobra/args.go b/vendor/github.com/spf13/cobra/args.go new file mode 100644 index 0000000000..e79ec33a81 --- /dev/null +++ b/vendor/github.com/spf13/cobra/args.go @@ -0,0 +1,131 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "fmt" + "strings" +) + +type PositionalArgs func(cmd *Command, args []string) error + +// legacyArgs validation has the following behaviour: +// - root commands with no subcommands can take arbitrary arguments +// - root commands with subcommands will do subcommand validity checking +// - subcommands will always accept arbitrary arguments +func legacyArgs(cmd *Command, args []string) error { + // no subcommand, always take args + if !cmd.HasSubCommands() { + return nil + } + + // root command with subcommands, do subcommand checking. + if !cmd.HasParent() && len(args) > 0 { + return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) + } + return nil +} + +// NoArgs returns an error if any args are included. +func NoArgs(cmd *Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } + return nil +} + +// OnlyValidArgs returns an error if there are any positional args that are not in +// the `ValidArgs` field of `Command` +func OnlyValidArgs(cmd *Command, args []string) error { + if len(cmd.ValidArgs) > 0 { + // Remove any description that may be included in ValidArgs. + // A description is following a tab character. + var validArgs []string + for _, v := range cmd.ValidArgs { + validArgs = append(validArgs, strings.Split(v, "\t")[0]) + } + for _, v := range args { + if !stringInSlice(v, validArgs) { + return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0])) + } + } + } + return nil +} + +// ArbitraryArgs never returns an error. +func ArbitraryArgs(cmd *Command, args []string) error { + return nil +} + +// MinimumNArgs returns an error if there is not at least N args. +func MinimumNArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) < n { + return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) + } + return nil + } +} + +// MaximumNArgs returns an error if there are more than N args. +func MaximumNArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) > n { + return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args)) + } + return nil + } +} + +// ExactArgs returns an error if there are not exactly n args. +func ExactArgs(n int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) != n { + return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) + } + return nil + } +} + +// RangeArgs returns an error if the number of args is not within the expected range. +func RangeArgs(min int, max int) PositionalArgs { + return func(cmd *Command, args []string) error { + if len(args) < min || len(args) > max { + return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args)) + } + return nil + } +} + +// MatchAll allows combining several PositionalArgs to work in concert. +func MatchAll(pargs ...PositionalArgs) PositionalArgs { + return func(cmd *Command, args []string) error { + for _, parg := range pargs { + if err := parg(cmd, args); err != nil { + return err + } + } + return nil + } +} + +// ExactValidArgs returns an error if there are not exactly N positional args OR +// there are any positional args that are not in the `ValidArgs` field of `Command` +// +// Deprecated: use MatchAll(ExactArgs(n), OnlyValidArgs) instead +func ExactValidArgs(n int) PositionalArgs { + return MatchAll(ExactArgs(n), OnlyValidArgs) +} diff --git a/vendor/github.com/spf13/cobra/bash_completions.go b/vendor/github.com/spf13/cobra/bash_completions.go new file mode 100644 index 0000000000..10c78847de --- /dev/null +++ b/vendor/github.com/spf13/cobra/bash_completions.go @@ -0,0 +1,712 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/spf13/pflag" +) + +// Annotations for Bash completion. +const ( + BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" + BashCompCustom = "cobra_annotation_bash_completion_custom" + BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" + BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" +) + +func writePreamble(buf io.StringWriter, name string) { + WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name)) + WriteStringAndCheck(buf, fmt.Sprintf(` +__%[1]s_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__%[1]s_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__%[1]s_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__%[1]s_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__%[1]s_handle_go_custom_completion() +{ + __%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" + + local shellCompDirectiveError=%[3]d + local shellCompDirectiveNoSpace=%[4]d + local shellCompDirectiveNoFileComp=%[5]d + local shellCompDirectiveFilterFileExt=%[6]d + local shellCompDirectiveFilterDirs=%[7]d + + local out requestComp lastParam lastChar comp directive args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly %[1]s allows to handle aliases + args=("${words[@]:1}") + # Disable ActiveHelp which is not supported for bash completion v1 + requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" + + if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%%:*} + if [ "${directive}" = "${out}" ]; then + # There is not directive specified + directive=0 + fi + __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" + __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + # Error code. No completion. + __%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code" + return + else + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __%[1]s_debug "${FUNCNAME[0]}: activating no space" + compopt -o nospace + fi + fi + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __%[1]s_debug "${FUNCNAME[0]}: activating no file completion" + compopt +o default + fi + fi + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local fullFilter filter filteringCmd + # Do not use quotes around the $out variable or else newline + # characters will be kept. + for filter in ${out}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __%[1]s_debug "File filtering command: $filteringCmd" + $filteringCmd + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + # Use printf to strip any trailing newline + subdir=$(printf "%%s" "${out}") + if [ -n "$subdir" ]; then + __%[1]s_debug "Listing directories in $subdir" + __%[1]s_handle_subdirs_in_dir_flag "$subdir" + else + __%[1]s_debug "Listing directories in ." + _filedir -d + fi + else + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${out}" -- "$cur") + fi +} + +__%[1]s_handle_reply() +{ + __%[1]s_debug "${FUNCNAME[0]}" + local comp + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${allflags[*]}" -- "$cur") + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%%=*}" + __%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION:-}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + + if [[ -z "${flag_parsing_disabled}" ]]; then + # If flag parsing is enabled, we have completed the flags and can return. + # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough + # to possibly call handle_go_custom_completion. + return 0; + fi + ;; + esac + + # check if we are handling a flag with special work handling + local index + __%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions+=("${must_have_one_noun[@]}") + elif [[ -n "${has_completion_function}" ]]; then + # if a go completion function is provided, defer to that function + __%[1]s_handle_go_custom_completion + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${completions[*]}" -- "$cur") + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${noun_aliases[*]}" -- "$cur") + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + if declare -F __%[1]s_custom_func >/dev/null; then + # try command name qualified custom func + __%[1]s_custom_func + else + # otherwise fall back to unqualified for compatibility + declare -F __custom_func >/dev/null && __custom_func + fi + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__%[1]s_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__%[1]s_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return +} + +__%[1]s_handle_flag() +{ + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue="" + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then + __%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__%[1]s_handle_noun() +{ + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__%[1]s_handle_command() +{ + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_%[1]s_root_command" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} + +__%[1]s_handle_word() +{ + if [[ $c -ge $cword ]]; then + __%[1]s_handle_reply + return + fi + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __%[1]s_handle_flag + elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then + __%[1]s_handle_command + elif [[ $c -eq 0 ]]; then + __%[1]s_handle_command + elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __%[1]s_handle_command + else + __%[1]s_handle_noun + fi + else + __%[1]s_handle_noun + fi + __%[1]s_handle_word +} + +`, name, ShellCompNoDescRequestCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name))) +} + +func writePostscript(buf io.StringWriter, name string) { + name = strings.ReplaceAll(name, ":", "__") + WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name)) + WriteStringAndCheck(buf, fmt.Sprintf(`{ + local cur prev words cword split + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __%[1]s_init_completion -n "=" || return + fi + + local c=0 + local flag_parsing_disabled= + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("%[1]s") + local command_aliases=() + local must_have_one_flag=() + local must_have_one_noun=() + local has_completion_function="" + local last_command="" + local nouns=() + local noun_aliases=() + + __%[1]s_handle_word +} + +`, name)) + WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_%s %s +else + complete -o default -o nospace -F __start_%s %s +fi + +`, name, name, name, name)) + WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n") +} + +func writeCommands(buf io.StringWriter, cmd *Command) { + WriteStringAndCheck(buf, " commands=()\n") + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() && c != cmd.helpCommand { + continue + } + WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name())) + writeCmdAliases(buf, c) + } + WriteStringAndCheck(buf, "\n") +} + +func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *Command) { + for key, value := range annotations { + switch key { + case BashCompFilenameExt: + WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + + var ext string + if len(value) > 0 { + ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|") + } else { + ext = "_filedir" + } + WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + case BashCompCustom: + WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + + if len(value) > 0 { + handlers := strings.Join(value, "; ") + WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers)) + } else { + WriteStringAndCheck(buf, " flags_completion+=(:)\n") + } + case BashCompSubdirsInDir: + WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + + var ext string + if len(value) == 1 { + ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0] + } else { + ext = "_filedir -d" + } + WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + } + } +} + +const cbn = "\")\n" + +func writeShortFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) { + name := flag.Shorthand + format := " " + if len(flag.NoOptDefVal) == 0 { + format += "two_word_" + } + format += "flags+=(\"-%s" + cbn + WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + writeFlagHandler(buf, "-"+name, flag.Annotations, cmd) +} + +func writeFlag(buf io.StringWriter, flag *pflag.Flag, cmd *Command) { + name := flag.Name + format := " flags+=(\"--%s" + if len(flag.NoOptDefVal) == 0 { + format += "=" + } + format += cbn + WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + if len(flag.NoOptDefVal) == 0 { + format = " two_word_flags+=(\"--%s" + cbn + WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + } + writeFlagHandler(buf, "--"+name, flag.Annotations, cmd) +} + +func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) { + name := flag.Name + format := " local_nonpersistent_flags+=(\"--%[1]s" + cbn + if len(flag.NoOptDefVal) == 0 { + format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn + } + WriteStringAndCheck(buf, fmt.Sprintf(format, name)) + if len(flag.Shorthand) > 0 { + WriteStringAndCheck(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand)) + } +} + +// prepareCustomAnnotationsForFlags setup annotations for go completions for registered flags +func prepareCustomAnnotationsForFlags(cmd *Command) { + flagCompletionMutex.RLock() + defer flagCompletionMutex.RUnlock() + for flag := range flagCompletionFunctions { + // Make sure the completion script calls the __*_go_custom_completion function for + // every registered flag. We need to do this here (and not when the flag was registered + // for completion) so that we can know the root command name for the prefix + // of ___go_custom_completion + if flag.Annotations == nil { + flag.Annotations = map[string][]string{} + } + flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())} + } +} + +func writeFlags(buf io.StringWriter, cmd *Command) { + prepareCustomAnnotationsForFlags(cmd) + WriteStringAndCheck(buf, ` flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + +`) + + if cmd.DisableFlagParsing { + WriteStringAndCheck(buf, " flag_parsing_disabled=1\n") + } + + localNonPersistentFlags := cmd.LocalNonPersistentFlags() + cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + writeFlag(buf, flag, cmd) + if len(flag.Shorthand) > 0 { + writeShortFlag(buf, flag, cmd) + } + // localNonPersistentFlags are used to stop the completion of subcommands when one is set + // if TraverseChildren is true we should allow to complete subcommands + if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren { + writeLocalNonPersistentFlag(buf, flag) + } + }) + cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + writeFlag(buf, flag, cmd) + if len(flag.Shorthand) > 0 { + writeShortFlag(buf, flag, cmd) + } + }) + + WriteStringAndCheck(buf, "\n") +} + +func writeRequiredFlag(buf io.StringWriter, cmd *Command) { + WriteStringAndCheck(buf, " must_have_one_flag=()\n") + flags := cmd.NonInheritedFlags() + flags.VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + for key := range flag.Annotations { + switch key { + case BashCompOneRequiredFlag: + format := " must_have_one_flag+=(\"--%s" + if flag.Value.Type() != "bool" { + format += "=" + } + format += cbn + WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name)) + + if len(flag.Shorthand) > 0 { + WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand)) + } + } + } + }) +} + +func writeRequiredNouns(buf io.StringWriter, cmd *Command) { + WriteStringAndCheck(buf, " must_have_one_noun=()\n") + sort.Strings(cmd.ValidArgs) + for _, value := range cmd.ValidArgs { + // Remove any description that may be included following a tab character. + // Descriptions are not supported by bash completion. + value = strings.Split(value, "\t")[0] + WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_noun+=(%q)\n", value)) + } + if cmd.ValidArgsFunction != nil { + WriteStringAndCheck(buf, " has_completion_function=1\n") + } +} + +func writeCmdAliases(buf io.StringWriter, cmd *Command) { + if len(cmd.Aliases) == 0 { + return + } + + sort.Strings(cmd.Aliases) + + WriteStringAndCheck(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n")) + for _, value := range cmd.Aliases { + WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value)) + WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name())) + } + WriteStringAndCheck(buf, ` fi`) + WriteStringAndCheck(buf, "\n") +} +func writeArgAliases(buf io.StringWriter, cmd *Command) { + WriteStringAndCheck(buf, " noun_aliases=()\n") + sort.Strings(cmd.ArgAliases) + for _, value := range cmd.ArgAliases { + WriteStringAndCheck(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value)) + } +} + +func gen(buf io.StringWriter, cmd *Command) { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() && c != cmd.helpCommand { + continue + } + gen(buf, c) + } + commandName := cmd.CommandPath() + commandName = strings.ReplaceAll(commandName, " ", "_") + commandName = strings.ReplaceAll(commandName, ":", "__") + + if cmd.Root() == cmd { + WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName)) + } else { + WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName)) + } + + WriteStringAndCheck(buf, fmt.Sprintf(" last_command=%q\n", commandName)) + WriteStringAndCheck(buf, "\n") + WriteStringAndCheck(buf, " command_aliases=()\n") + WriteStringAndCheck(buf, "\n") + + writeCommands(buf, cmd) + writeFlags(buf, cmd) + writeRequiredFlag(buf, cmd) + writeRequiredNouns(buf, cmd) + writeArgAliases(buf, cmd) + WriteStringAndCheck(buf, "}\n\n") +} + +// GenBashCompletion generates bash completion file and writes to the passed writer. +func (c *Command) GenBashCompletion(w io.Writer) error { + buf := new(bytes.Buffer) + writePreamble(buf, c.Name()) + if len(c.BashCompletionFunction) > 0 { + buf.WriteString(c.BashCompletionFunction + "\n") + } + gen(buf, c) + writePostscript(buf, c.Name()) + + _, err := buf.WriteTo(w) + return err +} + +func nonCompletableFlag(flag *pflag.Flag) bool { + return flag.Hidden || len(flag.Deprecated) > 0 +} + +// GenBashCompletionFile generates bash completion file. +func (c *Command) GenBashCompletionFile(filename string) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenBashCompletion(outFile) +} diff --git a/vendor/github.com/spf13/cobra/bash_completions.md b/vendor/github.com/spf13/cobra/bash_completions.md new file mode 100644 index 0000000000..52919b2fa6 --- /dev/null +++ b/vendor/github.com/spf13/cobra/bash_completions.md @@ -0,0 +1,93 @@ +# Generating Bash Completions For Your cobra.Command + +Please refer to [Shell Completions](shell_completions.md) for details. + +## Bash legacy dynamic completions + +For backward compatibility, Cobra still supports its legacy dynamic completion solution (described below). Unlike the `ValidArgsFunction` solution, the legacy solution will only work for Bash shell-completion and not for other shells. This legacy solution can be used along-side `ValidArgsFunction` and `RegisterFlagCompletionFunc()`, as long as both solutions are not used for the same command. This provides a path to gradually migrate from the legacy solution to the new solution. + +**Note**: Cobra's default `completion` command uses bash completion V2. If you are currently using Cobra's legacy dynamic completion solution, you should not use the default `completion` command but continue using your own. + +The legacy solution allows you to inject bash functions into the bash completion script. Those bash functions are responsible for providing the completion choices for your own completions. + +Some code that works in kubernetes: + +```bash +const ( + bash_completion_func = `__kubectl_parse_get() +{ + local kubectl_output out + if kubectl_output=$(kubectl get --no-headers "$1" 2>/dev/null); then + out=($(echo "${kubectl_output}" | awk '{print $1}')) + COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) ) + fi +} + +__kubectl_get_resource() +{ + if [[ ${#nouns[@]} -eq 0 ]]; then + return 1 + fi + __kubectl_parse_get ${nouns[${#nouns[@]} -1]} + if [[ $? -eq 0 ]]; then + return 0 + fi +} + +__kubectl_custom_func() { + case ${last_command} in + kubectl_get | kubectl_describe | kubectl_delete | kubectl_stop) + __kubectl_get_resource + return + ;; + *) + ;; + esac +} +`) +``` + +And then I set that in my command definition: + +```go +cmds := &cobra.Command{ + Use: "kubectl", + Short: "kubectl controls the Kubernetes cluster manager", + Long: `kubectl controls the Kubernetes cluster manager. + +Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, + Run: runHelp, + BashCompletionFunction: bash_completion_func, +} +``` + +The `BashCompletionFunction` option is really only valid/useful on the root command. Doing the above will cause `__kubectl_custom_func()` (`___custom_func()`) to be called when the built in processor was unable to find a solution. In the case of kubernetes a valid command might look something like `kubectl get pod [mypod]`. If you type `kubectl get pod [tab][tab]` the `__kubectl_customc_func()` will run because the cobra.Command only understood "kubectl" and "get." `__kubectl_custom_func()` will see that the cobra.Command is "kubectl_get" and will thus call another helper `__kubectl_get_resource()`. `__kubectl_get_resource` will look at the 'nouns' collected. In our example the only noun will be `pod`. So it will call `__kubectl_parse_get pod`. `__kubectl_parse_get` will actually call out to kubernetes and get any pods. It will then set `COMPREPLY` to valid pods! + +Similarly, for flags: + +```go + annotation := make(map[string][]string) + annotation[cobra.BashCompCustom] = []string{"__kubectl_get_namespaces"} + + flag := &pflag.Flag{ + Name: "namespace", + Usage: usage, + Annotations: annotation, + } + cmd.Flags().AddFlag(flag) +``` + +In addition add the `__kubectl_get_namespaces` implementation in the `BashCompletionFunction` +value, e.g.: + +```bash +__kubectl_get_namespaces() +{ + local template + template="{{ range .items }}{{ .metadata.name }} {{ end }}" + local kubectl_out + if kubectl_out=$(kubectl get -o template --template="${template}" namespace 2>/dev/null); then + COMPREPLY=( $( compgen -W "${kubectl_out}[*]" -- "$cur" ) ) + fi +} +``` diff --git a/vendor/github.com/spf13/cobra/bash_completionsV2.go b/vendor/github.com/spf13/cobra/bash_completionsV2.go new file mode 100644 index 0000000000..19b09560c1 --- /dev/null +++ b/vendor/github.com/spf13/cobra/bash_completionsV2.go @@ -0,0 +1,396 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { + buf := new(bytes.Buffer) + genBashComp(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) + return err +} + +func genBashComp(buf io.StringWriter, name string, includeDesc bool) { + compCmd := ShellCompRequestCmd + if !includeDesc { + compCmd = ShellCompNoDescRequestCmd + } + + WriteStringAndCheck(buf, fmt.Sprintf(`# bash completion V2 for %-36[1]s -*- shell-script -*- + +__%[1]s_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Macs have bash3 for which the bash-completion package doesn't include +# _init_completion. This is a minimal version of that function. +__%[1]s_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +# This function calls the %[1]s program to obtain the completion +# results and the directive. It fills the 'out' and 'directive' vars. +__%[1]s_get_completion_results() { + local requestComp lastParam lastChar args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly %[1]s allows to handle aliases + args=("${words[@]:1}") + requestComp="${words[0]} %[2]s ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" + + if [[ -z ${cur} && ${lastChar} != = ]]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __%[1]s_debug "Adding extra empty parameter" + requestComp="${requestComp} ''" + fi + + # When completing a flag with an = (e.g., %[1]s -n=) + # bash focuses on the part after the =, so we need to remove + # the flag part from $cur + if [[ ${cur} == -*=* ]]; then + cur="${cur#*=}" + fi + + __%[1]s_debug "Calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%%:*} + if [[ ${directive} == "${out}" ]]; then + # There is not directive specified + directive=0 + fi + __%[1]s_debug "The completion directive is: ${directive}" + __%[1]s_debug "The completions are: ${out}" +} + +__%[1]s_process_completion_results() { + local shellCompDirectiveError=%[3]d + local shellCompDirectiveNoSpace=%[4]d + local shellCompDirectiveNoFileComp=%[5]d + local shellCompDirectiveFilterFileExt=%[6]d + local shellCompDirectiveFilterDirs=%[7]d + local shellCompDirectiveKeepOrder=%[8]d + + if (((directive & shellCompDirectiveError) != 0)); then + # Error code. No completion. + __%[1]s_debug "Received error from custom completion go code" + return + else + if (((directive & shellCompDirectiveNoSpace) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __%[1]s_debug "Activating no space" + compopt -o nospace + else + __%[1]s_debug "No space directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveKeepOrder) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + # no sort isn't supported for bash less than < 4.4 + if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then + __%[1]s_debug "No sort directive not supported in this version of bash" + else + __%[1]s_debug "Activating keep order" + compopt -o nosort + fi + else + __%[1]s_debug "No sort directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveNoFileComp) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __%[1]s_debug "Activating no file completion" + compopt +o default + else + __%[1]s_debug "No file completion directive not supported in this version of bash" + fi + fi + fi + + # Separate activeHelp from normal completions + local completions=() + local activeHelp=() + __%[1]s_extract_activeHelp + + if (((directive & shellCompDirectiveFilterFileExt) != 0)); then + # File extension filtering + local fullFilter filter filteringCmd + + # Do not use quotes around the $completions variable or else newline + # characters will be kept. + for filter in ${completions[*]}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __%[1]s_debug "File filtering command: $filteringCmd" + $filteringCmd + elif (((directive & shellCompDirectiveFilterDirs) != 0)); then + # File completion for directories only + + local subdir + subdir=${completions[0]} + if [[ -n $subdir ]]; then + __%[1]s_debug "Listing directories in $subdir" + pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return + else + __%[1]s_debug "Listing directories in ." + _filedir -d + fi + else + __%[1]s_handle_completion_types + fi + + __%[1]s_handle_special_char "$cur" : + __%[1]s_handle_special_char "$cur" = + + # Print the activeHelp statements before we finish + if ((${#activeHelp[*]} != 0)); then + printf "\n"; + printf "%%s\n" "${activeHelp[@]}" + printf "\n" + + # The prompt format is only available from bash 4.4. + # We test if it is available before using it. + if (x=${PS1@P}) 2> /dev/null; then + printf "%%s" "${PS1@P}${COMP_LINE[@]}" + else + # Can't print the prompt. Just print the + # text the user had typed, it is workable enough. + printf "%%s" "${COMP_LINE[@]}" + fi + fi +} + +# Separate activeHelp lines from real completions. +# Fills the $activeHelp and $completions arrays. +__%[1]s_extract_activeHelp() { + local activeHelpMarker="%[9]s" + local endIndex=${#activeHelpMarker} + + while IFS='' read -r comp; do + if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then + comp=${comp:endIndex} + __%[1]s_debug "ActiveHelp found: $comp" + if [[ -n $comp ]]; then + activeHelp+=("$comp") + fi + else + # Not an activeHelp line but a normal completion + completions+=("$comp") + fi + done <<<"${out}" +} + +__%[1]s_handle_completion_types() { + __%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" + + case $COMP_TYPE in + 37|42) + # Type: menu-complete/menu-complete-backward and insert-completions + # If the user requested inserting one completion at a time, or all + # completions at once on the command-line we must remove the descriptions. + # https://github.com/spf13/cobra/issues/1508 + local tab=$'\t' comp + while IFS='' read -r comp; do + [[ -z $comp ]] && continue + # Strip any description + comp=${comp%%%%$tab*} + # Only consider the completions that match + if [[ $comp == "$cur"* ]]; then + COMPREPLY+=("$comp") + fi + done < <(printf "%%s\n" "${completions[@]}") + ;; + + *) + # Type: complete (normal completion) + __%[1]s_handle_standard_completion_case + ;; + esac +} + +__%[1]s_handle_standard_completion_case() { + local tab=$'\t' comp + + # Short circuit to optimize if we don't have descriptions + if [[ "${completions[*]}" != *$tab* ]]; then + IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") + return 0 + fi + + local longest=0 + local compline + # Look for the longest completion so that we can format things nicely + while IFS='' read -r compline; do + [[ -z $compline ]] && continue + # Strip any description before checking the length + comp=${compline%%%%$tab*} + # Only consider the completions that match + [[ $comp == "$cur"* ]] || continue + COMPREPLY+=("$compline") + if ((${#comp}>longest)); then + longest=${#comp} + fi + done < <(printf "%%s\n" "${completions[@]}") + + # If there is a single completion left, remove the description text + if ((${#COMPREPLY[*]} == 1)); then + __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" + comp="${COMPREPLY[0]%%%%$tab*}" + __%[1]s_debug "Removed description from single completion, which is now: ${comp}" + COMPREPLY[0]=$comp + else # Format the descriptions + __%[1]s_format_comp_descriptions $longest + fi +} + +__%[1]s_handle_special_char() +{ + local comp="$1" + local char=$2 + if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then + local word=${comp%%"${comp##*${char}}"} + local idx=${#COMPREPLY[*]} + while ((--idx >= 0)); do + COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} + done + fi +} + +__%[1]s_format_comp_descriptions() +{ + local tab=$'\t' + local comp desc maxdesclength + local longest=$1 + + local i ci + for ci in ${!COMPREPLY[*]}; do + comp=${COMPREPLY[ci]} + # Properly format the description string which follows a tab character if there is one + if [[ "$comp" == *$tab* ]]; then + __%[1]s_debug "Original comp: $comp" + desc=${comp#*$tab} + comp=${comp%%%%$tab*} + + # $COLUMNS stores the current shell width. + # Remove an extra 4 because we add 2 spaces and 2 parentheses. + maxdesclength=$(( COLUMNS - longest - 4 )) + + # Make sure we can fit a description of at least 8 characters + # if we are to align the descriptions. + if ((maxdesclength > 8)); then + # Add the proper number of spaces to align the descriptions + for ((i = ${#comp} ; i < longest ; i++)); do + comp+=" " + done + else + # Don't pad the descriptions so we can fit more text after the completion + maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + fi + + # If there is enough space for any description text, + # truncate the descriptions that are too long for the shell width + if ((maxdesclength > 0)); then + if ((${#desc} > maxdesclength)); then + desc=${desc:0:$(( maxdesclength - 1 ))} + desc+="…" + fi + comp+=" ($desc)" + fi + COMPREPLY[ci]=$comp + __%[1]s_debug "Final comp: $comp" + fi + done +} + +__start_%[1]s() +{ + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n =: || return + else + __%[1]s_init_completion -n =: || return + fi + + __%[1]s_debug + __%[1]s_debug "========= starting completion logic ==========" + __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __%[1]s_debug "Truncated words[*]: ${words[*]}," + + local out directive + __%[1]s_get_completion_results + __%[1]s_process_completion_results +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_%[1]s %[1]s +else + complete -o default -o nospace -F __start_%[1]s %[1]s +fi + +# ex: ts=4 sw=4 et filetype=sh +`, name, compCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, + activeHelpMarker)) +} + +// GenBashCompletionFileV2 generates Bash completion version 2. +func (c *Command) GenBashCompletionFileV2(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenBashCompletionV2(outFile, includeDesc) +} + +// GenBashCompletionV2 generates Bash completion file version 2 +// and writes it to the passed writer. +func (c *Command) GenBashCompletionV2(w io.Writer, includeDesc bool) error { + return c.genBashCompletion(w, includeDesc) +} diff --git a/vendor/github.com/spf13/cobra/cobra.go b/vendor/github.com/spf13/cobra/cobra.go new file mode 100644 index 0000000000..b07b44a0ce --- /dev/null +++ b/vendor/github.com/spf13/cobra/cobra.go @@ -0,0 +1,239 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Commands similar to git, go tools and other modern CLI tools +// inspired by go, go-Commander, gh and subcommand + +package cobra + +import ( + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" + "text/template" + "time" + "unicode" +) + +var templateFuncs = template.FuncMap{ + "trim": strings.TrimSpace, + "trimRightSpace": trimRightSpace, + "trimTrailingWhitespaces": trimRightSpace, + "appendIfNotPresent": appendIfNotPresent, + "rpad": rpad, + "gt": Gt, + "eq": Eq, +} + +var initializers []func() +var finalizers []func() + +const ( + defaultPrefixMatching = false + defaultCommandSorting = true + defaultCaseInsensitive = false +) + +// EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing +// to automatically enable in CLI tools. +// Set this to true to enable it. +var EnablePrefixMatching = defaultPrefixMatching + +// EnableCommandSorting controls sorting of the slice of commands, which is turned on by default. +// To disable sorting, set it to false. +var EnableCommandSorting = defaultCommandSorting + +// EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default) +var EnableCaseInsensitive = defaultCaseInsensitive + +// MousetrapHelpText enables an information splash screen on Windows +// if the CLI is started from explorer.exe. +// To disable the mousetrap, just set this variable to blank string (""). +// Works only on Microsoft Windows. +var MousetrapHelpText = `This is a command line tool. + +You need to open cmd.exe and run it from there. +` + +// MousetrapDisplayDuration controls how long the MousetrapHelpText message is displayed on Windows +// if the CLI is started from explorer.exe. Set to 0 to wait for the return key to be pressed. +// To disable the mousetrap, just set MousetrapHelpText to blank string (""). +// Works only on Microsoft Windows. +var MousetrapDisplayDuration = 5 * time.Second + +// AddTemplateFunc adds a template function that's available to Usage and Help +// template generation. +func AddTemplateFunc(name string, tmplFunc interface{}) { + templateFuncs[name] = tmplFunc +} + +// AddTemplateFuncs adds multiple template functions that are available to Usage and +// Help template generation. +func AddTemplateFuncs(tmplFuncs template.FuncMap) { + for k, v := range tmplFuncs { + templateFuncs[k] = v + } +} + +// OnInitialize sets the passed functions to be run when each command's +// Execute method is called. +func OnInitialize(y ...func()) { + initializers = append(initializers, y...) +} + +// OnFinalize sets the passed functions to be run when each command's +// Execute method is terminated. +func OnFinalize(y ...func()) { + finalizers = append(finalizers, y...) +} + +// FIXME Gt is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. + +// Gt takes two types and checks whether the first type is greater than the second. In case of types Arrays, Chans, +// Maps and Slices, Gt will compare their lengths. Ints are compared directly while strings are first parsed as +// ints and then compared. +func Gt(a interface{}, b interface{}) bool { + var left, right int64 + av := reflect.ValueOf(a) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + left = int64(av.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + left = av.Int() + case reflect.String: + left, _ = strconv.ParseInt(av.String(), 10, 64) + } + + bv := reflect.ValueOf(b) + + switch bv.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + right = int64(bv.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + right = bv.Int() + case reflect.String: + right, _ = strconv.ParseInt(bv.String(), 10, 64) + } + + return left > right +} + +// FIXME Eq is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. + +// Eq takes two types and checks whether they are equal. Supported types are int and string. Unsupported types will panic. +func Eq(a interface{}, b interface{}) bool { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + panic("Eq called on unsupported type") + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return av.Int() == bv.Int() + case reflect.String: + return av.String() == bv.String() + } + return false +} + +func trimRightSpace(s string) string { + return strings.TrimRightFunc(s, unicode.IsSpace) +} + +// FIXME appendIfNotPresent is unused by cobra and should be removed in a version 2. It exists only for compatibility with users of cobra. + +// appendIfNotPresent will append stringToAppend to the end of s, but only if it's not yet present in s. +func appendIfNotPresent(s, stringToAppend string) string { + if strings.Contains(s, stringToAppend) { + return s + } + return s + " " + stringToAppend +} + +// rpad adds padding to the right of a string. +func rpad(s string, padding int) string { + formattedString := fmt.Sprintf("%%-%ds", padding) + return fmt.Sprintf(formattedString, s) +} + +// tmpl executes the given template text on data, writing the result to w. +func tmpl(w io.Writer, text string, data interface{}) error { + t := template.New("top") + t.Funcs(templateFuncs) + template.Must(t.Parse(text)) + return t.Execute(w, data) +} + +// ld compares two strings and returns the levenshtein distance between them. +func ld(s, t string, ignoreCase bool) int { + if ignoreCase { + s = strings.ToLower(s) + t = strings.ToLower(t) + } + d := make([][]int, len(s)+1) + for i := range d { + d[i] = make([]int, len(t)+1) + } + for i := range d { + d[i][0] = i + } + for j := range d[0] { + d[0][j] = j + } + for j := 1; j <= len(t); j++ { + for i := 1; i <= len(s); i++ { + if s[i-1] == t[j-1] { + d[i][j] = d[i-1][j-1] + } else { + min := d[i-1][j] + if d[i][j-1] < min { + min = d[i][j-1] + } + if d[i-1][j-1] < min { + min = d[i-1][j-1] + } + d[i][j] = min + 1 + } + } + + } + return d[len(s)][len(t)] +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// CheckErr prints the msg with the prefix 'Error:' and exits with error code 1. If the msg is nil, it does nothing. +func CheckErr(msg interface{}) { + if msg != nil { + fmt.Fprintln(os.Stderr, "Error:", msg) + os.Exit(1) + } +} + +// WriteStringAndCheck writes a string into a buffer, and checks if the error is not nil. +func WriteStringAndCheck(b io.StringWriter, s string) { + _, err := b.WriteString(s) + CheckErr(err) +} diff --git a/vendor/github.com/spf13/cobra/command.go b/vendor/github.com/spf13/cobra/command.go new file mode 100644 index 0000000000..01f7c6f1c5 --- /dev/null +++ b/vendor/github.com/spf13/cobra/command.go @@ -0,0 +1,1834 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. +// In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code. +package cobra + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + flag "github.com/spf13/pflag" +) + +const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" + +// FParseErrWhitelist configures Flag parse errors to be ignored +type FParseErrWhitelist flag.ParseErrorsWhitelist + +// Group Structure to manage groups for commands +type Group struct { + ID string + Title string +} + +// Command is just that, a command for your application. +// E.g. 'go run ...' - 'run' is the command. Cobra requires +// you to define the usage and description as part of your command +// definition to ensure usability. +type Command struct { + // Use is the one-line usage message. + // Recommended syntax is as follows: + // [ ] identifies an optional argument. Arguments that are not enclosed in brackets are required. + // ... indicates that you can specify multiple values for the previous argument. + // | indicates mutually exclusive information. You can use the argument to the left of the separator or the + // argument to the right of the separator. You cannot use both arguments in a single use of the command. + // { } delimits a set of mutually exclusive arguments when one of the arguments is required. If the arguments are + // optional, they are enclosed in brackets ([ ]). + // Example: add [-F file | -D dir]... [-f format] profile + Use string + + // Aliases is an array of aliases that can be used instead of the first word in Use. + Aliases []string + + // SuggestFor is an array of command names for which this command will be suggested - + // similar to aliases but only suggests. + SuggestFor []string + + // Short is the short description shown in the 'help' output. + Short string + + // The group id under which this subcommand is grouped in the 'help' output of its parent. + GroupID string + + // Long is the long message shown in the 'help ' output. + Long string + + // Example is examples of how to use the command. + Example string + + // ValidArgs is list of all valid non-flag arguments that are accepted in shell completions + ValidArgs []string + // ValidArgsFunction is an optional function that provides valid non-flag arguments for shell completion. + // It is a dynamic version of using ValidArgs. + // Only one of ValidArgs and ValidArgsFunction can be used for a command. + ValidArgsFunction func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) + + // Expected arguments + Args PositionalArgs + + // ArgAliases is List of aliases for ValidArgs. + // These are not suggested to the user in the shell completion, + // but accepted if entered manually. + ArgAliases []string + + // BashCompletionFunction is custom bash functions used by the legacy bash autocompletion generator. + // For portability with other shells, it is recommended to instead use ValidArgsFunction + BashCompletionFunction string + + // Deprecated defines, if this command is deprecated and should print this string when used. + Deprecated string + + // Annotations are key/value pairs that can be used by applications to identify or + // group commands. + Annotations map[string]string + + // Version defines the version for this command. If this value is non-empty and the command does not + // define a "version" flag, a "version" boolean flag will be added to the command and, if specified, + // will print content of the "Version" variable. A shorthand "v" flag will also be added if the + // command does not define one. + Version string + + // The *Run functions are executed in the following order: + // * PersistentPreRun() + // * PreRun() + // * Run() + // * PostRun() + // * PersistentPostRun() + // All functions get the same args, the arguments after the command name. + // + // PersistentPreRun: children of this command will inherit and execute. + PersistentPreRun func(cmd *Command, args []string) + // PersistentPreRunE: PersistentPreRun but returns an error. + PersistentPreRunE func(cmd *Command, args []string) error + // PreRun: children of this command will not inherit. + PreRun func(cmd *Command, args []string) + // PreRunE: PreRun but returns an error. + PreRunE func(cmd *Command, args []string) error + // Run: Typically the actual work function. Most commands will only implement this. + Run func(cmd *Command, args []string) + // RunE: Run but returns an error. + RunE func(cmd *Command, args []string) error + // PostRun: run after the Run command. + PostRun func(cmd *Command, args []string) + // PostRunE: PostRun but returns an error. + PostRunE func(cmd *Command, args []string) error + // PersistentPostRun: children of this command will inherit and execute after PostRun. + PersistentPostRun func(cmd *Command, args []string) + // PersistentPostRunE: PersistentPostRun but returns an error. + PersistentPostRunE func(cmd *Command, args []string) error + + // groups for subcommands + commandgroups []*Group + + // args is actual args parsed from flags. + args []string + // flagErrorBuf contains all error messages from pflag. + flagErrorBuf *bytes.Buffer + // flags is full set of flags. + flags *flag.FlagSet + // pflags contains persistent flags. + pflags *flag.FlagSet + // lflags contains local flags. + lflags *flag.FlagSet + // iflags contains inherited flags. + iflags *flag.FlagSet + // parentsPflags is all persistent flags of cmd's parents. + parentsPflags *flag.FlagSet + // globNormFunc is the global normalization function + // that we can use on every pflag set and children commands + globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName + + // usageFunc is usage func defined by user. + usageFunc func(*Command) error + // usageTemplate is usage template defined by user. + usageTemplate string + // flagErrorFunc is func defined by user and it's called when the parsing of + // flags returns an error. + flagErrorFunc func(*Command, error) error + // helpTemplate is help template defined by user. + helpTemplate string + // helpFunc is help func defined by user. + helpFunc func(*Command, []string) + // helpCommand is command with usage 'help'. If it's not defined by user, + // cobra uses default help command. + helpCommand *Command + // helpCommandGroupID is the group id for the helpCommand + helpCommandGroupID string + + // completionCommandGroupID is the group id for the completion command + completionCommandGroupID string + + // versionTemplate is the version template defined by user. + versionTemplate string + + // inReader is a reader defined by the user that replaces stdin + inReader io.Reader + // outWriter is a writer defined by the user that replaces stdout + outWriter io.Writer + // errWriter is a writer defined by the user that replaces stderr + errWriter io.Writer + + // FParseErrWhitelist flag parse errors to be ignored + FParseErrWhitelist FParseErrWhitelist + + // CompletionOptions is a set of options to control the handling of shell completion + CompletionOptions CompletionOptions + + // commandsAreSorted defines, if command slice are sorted or not. + commandsAreSorted bool + // commandCalledAs is the name or alias value used to call this command. + commandCalledAs struct { + name string + called bool + } + + ctx context.Context + + // commands is the list of commands supported by this program. + commands []*Command + // parent is a parent command for this command. + parent *Command + // Max lengths of commands' string lengths for use in padding. + commandsMaxUseLen int + commandsMaxCommandPathLen int + commandsMaxNameLen int + + // TraverseChildren parses flags on all parents before executing child command. + TraverseChildren bool + + // Hidden defines, if this command is hidden and should NOT show up in the list of available commands. + Hidden bool + + // SilenceErrors is an option to quiet errors down stream. + SilenceErrors bool + + // SilenceUsage is an option to silence usage when an error occurs. + SilenceUsage bool + + // DisableFlagParsing disables the flag parsing. + // If this is true all flags will be passed to the command as arguments. + DisableFlagParsing bool + + // DisableAutoGenTag defines, if gen tag ("Auto generated by spf13/cobra...") + // will be printed by generating docs for this command. + DisableAutoGenTag bool + + // DisableFlagsInUseLine will disable the addition of [flags] to the usage + // line of a command when printing help or generating docs + DisableFlagsInUseLine bool + + // DisableSuggestions disables the suggestions based on Levenshtein distance + // that go along with 'unknown command' messages. + DisableSuggestions bool + + // SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions. + // Must be > 0. + SuggestionsMinimumDistance int +} + +// Context returns underlying command context. If command was executed +// with ExecuteContext or the context was set with SetContext, the +// previously set context will be returned. Otherwise, nil is returned. +// +// Notice that a call to Execute and ExecuteC will replace a nil context of +// a command with a context.Background, so a background context will be +// returned by Context after one of these functions has been called. +func (c *Command) Context() context.Context { + return c.ctx +} + +// SetContext sets context for the command. This context will be overwritten by +// Command.ExecuteContext or Command.ExecuteContextC. +func (c *Command) SetContext(ctx context.Context) { + c.ctx = ctx +} + +// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden +// particularly useful when testing. +func (c *Command) SetArgs(a []string) { + c.args = a +} + +// SetOutput sets the destination for usage and error messages. +// If output is nil, os.Stderr is used. +// Deprecated: Use SetOut and/or SetErr instead +func (c *Command) SetOutput(output io.Writer) { + c.outWriter = output + c.errWriter = output +} + +// SetOut sets the destination for usage messages. +// If newOut is nil, os.Stdout is used. +func (c *Command) SetOut(newOut io.Writer) { + c.outWriter = newOut +} + +// SetErr sets the destination for error messages. +// If newErr is nil, os.Stderr is used. +func (c *Command) SetErr(newErr io.Writer) { + c.errWriter = newErr +} + +// SetIn sets the source for input data +// If newIn is nil, os.Stdin is used. +func (c *Command) SetIn(newIn io.Reader) { + c.inReader = newIn +} + +// SetUsageFunc sets usage function. Usage can be defined by application. +func (c *Command) SetUsageFunc(f func(*Command) error) { + c.usageFunc = f +} + +// SetUsageTemplate sets usage template. Can be defined by Application. +func (c *Command) SetUsageTemplate(s string) { + c.usageTemplate = s +} + +// SetFlagErrorFunc sets a function to generate an error when flag parsing +// fails. +func (c *Command) SetFlagErrorFunc(f func(*Command, error) error) { + c.flagErrorFunc = f +} + +// SetHelpFunc sets help function. Can be defined by Application. +func (c *Command) SetHelpFunc(f func(*Command, []string)) { + c.helpFunc = f +} + +// SetHelpCommand sets help command. +func (c *Command) SetHelpCommand(cmd *Command) { + c.helpCommand = cmd +} + +// SetHelpCommandGroupID sets the group id of the help command. +func (c *Command) SetHelpCommandGroupID(groupID string) { + if c.helpCommand != nil { + c.helpCommand.GroupID = groupID + } + // helpCommandGroupID is used if no helpCommand is defined by the user + c.helpCommandGroupID = groupID +} + +// SetCompletionCommandGroupID sets the group id of the completion command. +func (c *Command) SetCompletionCommandGroupID(groupID string) { + // completionCommandGroupID is used if no completion command is defined by the user + c.Root().completionCommandGroupID = groupID +} + +// SetHelpTemplate sets help template to be used. Application can use it to set custom template. +func (c *Command) SetHelpTemplate(s string) { + c.helpTemplate = s +} + +// SetVersionTemplate sets version template to be used. Application can use it to set custom template. +func (c *Command) SetVersionTemplate(s string) { + c.versionTemplate = s +} + +// SetGlobalNormalizationFunc sets a normalization function to all flag sets and also to child commands. +// The user should not have a cyclic dependency on commands. +func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) { + c.Flags().SetNormalizeFunc(n) + c.PersistentFlags().SetNormalizeFunc(n) + c.globNormFunc = n + + for _, command := range c.commands { + command.SetGlobalNormalizationFunc(n) + } +} + +// OutOrStdout returns output to stdout. +func (c *Command) OutOrStdout() io.Writer { + return c.getOut(os.Stdout) +} + +// OutOrStderr returns output to stderr +func (c *Command) OutOrStderr() io.Writer { + return c.getOut(os.Stderr) +} + +// ErrOrStderr returns output to stderr +func (c *Command) ErrOrStderr() io.Writer { + return c.getErr(os.Stderr) +} + +// InOrStdin returns input to stdin +func (c *Command) InOrStdin() io.Reader { + return c.getIn(os.Stdin) +} + +func (c *Command) getOut(def io.Writer) io.Writer { + if c.outWriter != nil { + return c.outWriter + } + if c.HasParent() { + return c.parent.getOut(def) + } + return def +} + +func (c *Command) getErr(def io.Writer) io.Writer { + if c.errWriter != nil { + return c.errWriter + } + if c.HasParent() { + return c.parent.getErr(def) + } + return def +} + +func (c *Command) getIn(def io.Reader) io.Reader { + if c.inReader != nil { + return c.inReader + } + if c.HasParent() { + return c.parent.getIn(def) + } + return def +} + +// UsageFunc returns either the function set by SetUsageFunc for this command +// or a parent, or it returns a default usage function. +func (c *Command) UsageFunc() (f func(*Command) error) { + if c.usageFunc != nil { + return c.usageFunc + } + if c.HasParent() { + return c.Parent().UsageFunc() + } + return func(c *Command) error { + c.mergePersistentFlags() + err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c) + if err != nil { + c.PrintErrln(err) + } + return err + } +} + +// Usage puts out the usage for the command. +// Used when a user provides invalid input. +// Can be defined by user by overriding UsageFunc. +func (c *Command) Usage() error { + return c.UsageFunc()(c) +} + +// HelpFunc returns either the function set by SetHelpFunc for this command +// or a parent, or it returns a function with default help behavior. +func (c *Command) HelpFunc() func(*Command, []string) { + if c.helpFunc != nil { + return c.helpFunc + } + if c.HasParent() { + return c.Parent().HelpFunc() + } + return func(c *Command, a []string) { + c.mergePersistentFlags() + // The help should be sent to stdout + // See https://github.com/spf13/cobra/issues/1002 + err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c) + if err != nil { + c.PrintErrln(err) + } + } +} + +// Help puts out the help for the command. +// Used when a user calls help [command]. +// Can be defined by user by overriding HelpFunc. +func (c *Command) Help() error { + c.HelpFunc()(c, []string{}) + return nil +} + +// UsageString returns usage string. +func (c *Command) UsageString() string { + // Storing normal writers + tmpOutput := c.outWriter + tmpErr := c.errWriter + + bb := new(bytes.Buffer) + c.outWriter = bb + c.errWriter = bb + + CheckErr(c.Usage()) + + // Setting things back to normal + c.outWriter = tmpOutput + c.errWriter = tmpErr + + return bb.String() +} + +// FlagErrorFunc returns either the function set by SetFlagErrorFunc for this +// command or a parent, or it returns a function which returns the original +// error. +func (c *Command) FlagErrorFunc() (f func(*Command, error) error) { + if c.flagErrorFunc != nil { + return c.flagErrorFunc + } + + if c.HasParent() { + return c.parent.FlagErrorFunc() + } + return func(c *Command, err error) error { + return err + } +} + +var minUsagePadding = 25 + +// UsagePadding return padding for the usage. +func (c *Command) UsagePadding() int { + if c.parent == nil || minUsagePadding > c.parent.commandsMaxUseLen { + return minUsagePadding + } + return c.parent.commandsMaxUseLen +} + +var minCommandPathPadding = 11 + +// CommandPathPadding return padding for the command path. +func (c *Command) CommandPathPadding() int { + if c.parent == nil || minCommandPathPadding > c.parent.commandsMaxCommandPathLen { + return minCommandPathPadding + } + return c.parent.commandsMaxCommandPathLen +} + +var minNamePadding = 11 + +// NamePadding returns padding for the name. +func (c *Command) NamePadding() int { + if c.parent == nil || minNamePadding > c.parent.commandsMaxNameLen { + return minNamePadding + } + return c.parent.commandsMaxNameLen +} + +// UsageTemplate returns usage template for the command. +func (c *Command) UsageTemplate() string { + if c.usageTemplate != "" { + return c.usageTemplate + } + + if c.HasParent() { + return c.parent.UsageTemplate() + } + return `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` +} + +// HelpTemplate return help template for the command. +func (c *Command) HelpTemplate() string { + if c.helpTemplate != "" { + return c.helpTemplate + } + + if c.HasParent() { + return c.parent.HelpTemplate() + } + return `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} + +{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` +} + +// VersionTemplate return version template for the command. +func (c *Command) VersionTemplate() string { + if c.versionTemplate != "" { + return c.versionTemplate + } + + if c.HasParent() { + return c.parent.VersionTemplate() + } + return `{{with .Name}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}} +` +} + +func hasNoOptDefVal(name string, fs *flag.FlagSet) bool { + flag := fs.Lookup(name) + if flag == nil { + return false + } + return flag.NoOptDefVal != "" +} + +func shortHasNoOptDefVal(name string, fs *flag.FlagSet) bool { + if len(name) == 0 { + return false + } + + flag := fs.ShorthandLookup(name[:1]) + if flag == nil { + return false + } + return flag.NoOptDefVal != "" +} + +func stripFlags(args []string, c *Command) []string { + if len(args) == 0 { + return args + } + c.mergePersistentFlags() + + commands := []string{} + flags := c.Flags() + +Loop: + for len(args) > 0 { + s := args[0] + args = args[1:] + switch { + case s == "--": + // "--" terminates the flags + break Loop + case strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags): + // If '--flag arg' then + // delete arg from args. + fallthrough // (do the same as below) + case strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags): + // If '-f arg' then + // delete 'arg' from args or break the loop if len(args) <= 1. + if len(args) <= 1 { + break Loop + } else { + args = args[1:] + continue + } + case s != "" && !strings.HasPrefix(s, "-"): + commands = append(commands, s) + } + } + + return commands +} + +// argsMinusFirstX removes only the first x from args. Otherwise, commands that look like +// openshift admin policy add-role-to-user admin my-user, lose the admin argument (arg[4]). +// Special care needs to be taken not to remove a flag value. +func (c *Command) argsMinusFirstX(args []string, x string) []string { + if len(args) == 0 { + return args + } + c.mergePersistentFlags() + flags := c.Flags() + +Loop: + for pos := 0; pos < len(args); pos++ { + s := args[pos] + switch { + case s == "--": + // -- means we have reached the end of the parseable args. Break out of the loop now. + break Loop + case strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags): + fallthrough + case strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags): + // This is a flag without a default value, and an equal sign is not used. Increment pos in order to skip + // over the next arg, because that is the value of this flag. + pos++ + continue + case !strings.HasPrefix(s, "-"): + // This is not a flag or a flag value. Check to see if it matches what we're looking for, and if so, + // return the args, excluding the one at this position. + if s == x { + ret := []string{} + ret = append(ret, args[:pos]...) + ret = append(ret, args[pos+1:]...) + return ret + } + } + } + return args +} + +func isFlagArg(arg string) bool { + return ((len(arg) >= 3 && arg[0:2] == "--") || + (len(arg) >= 2 && arg[0] == '-' && arg[1] != '-')) +} + +// Find the target command given the args and command tree +// Meant to be run on the highest node. Only searches down. +func (c *Command) Find(args []string) (*Command, []string, error) { + var innerfind func(*Command, []string) (*Command, []string) + + innerfind = func(c *Command, innerArgs []string) (*Command, []string) { + argsWOflags := stripFlags(innerArgs, c) + if len(argsWOflags) == 0 { + return c, innerArgs + } + nextSubCmd := argsWOflags[0] + + cmd := c.findNext(nextSubCmd) + if cmd != nil { + return innerfind(cmd, c.argsMinusFirstX(innerArgs, nextSubCmd)) + } + return c, innerArgs + } + + commandFound, a := innerfind(c, args) + if commandFound.Args == nil { + return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound)) + } + return commandFound, a, nil +} + +func (c *Command) findSuggestions(arg string) string { + if c.DisableSuggestions { + return "" + } + if c.SuggestionsMinimumDistance <= 0 { + c.SuggestionsMinimumDistance = 2 + } + suggestionsString := "" + if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 { + suggestionsString += "\n\nDid you mean this?\n" + for _, s := range suggestions { + suggestionsString += fmt.Sprintf("\t%v\n", s) + } + } + return suggestionsString +} + +func (c *Command) findNext(next string) *Command { + matches := make([]*Command, 0) + for _, cmd := range c.commands { + if commandNameMatches(cmd.Name(), next) || cmd.HasAlias(next) { + cmd.commandCalledAs.name = next + return cmd + } + if EnablePrefixMatching && cmd.hasNameOrAliasPrefix(next) { + matches = append(matches, cmd) + } + } + + if len(matches) == 1 { + return matches[0] + } + + return nil +} + +// Traverse the command tree to find the command, and parse args for +// each parent. +func (c *Command) Traverse(args []string) (*Command, []string, error) { + flags := []string{} + inFlag := false + + for i, arg := range args { + switch { + // A long flag with a space separated value + case strings.HasPrefix(arg, "--") && !strings.Contains(arg, "="): + // TODO: this isn't quite right, we should really check ahead for 'true' or 'false' + inFlag = !hasNoOptDefVal(arg[2:], c.Flags()) + flags = append(flags, arg) + continue + // A short flag with a space separated value + case strings.HasPrefix(arg, "-") && !strings.Contains(arg, "=") && len(arg) == 2 && !shortHasNoOptDefVal(arg[1:], c.Flags()): + inFlag = true + flags = append(flags, arg) + continue + // The value for a flag + case inFlag: + inFlag = false + flags = append(flags, arg) + continue + // A flag without a value, or with an `=` separated value + case isFlagArg(arg): + flags = append(flags, arg) + continue + } + + cmd := c.findNext(arg) + if cmd == nil { + return c, args, nil + } + + if err := c.ParseFlags(flags); err != nil { + return nil, args, err + } + return cmd.Traverse(args[i+1:]) + } + return c, args, nil +} + +// SuggestionsFor provides suggestions for the typedName. +func (c *Command) SuggestionsFor(typedName string) []string { + suggestions := []string{} + for _, cmd := range c.commands { + if cmd.IsAvailableCommand() { + levenshteinDistance := ld(typedName, cmd.Name(), true) + suggestByLevenshtein := levenshteinDistance <= c.SuggestionsMinimumDistance + suggestByPrefix := strings.HasPrefix(strings.ToLower(cmd.Name()), strings.ToLower(typedName)) + if suggestByLevenshtein || suggestByPrefix { + suggestions = append(suggestions, cmd.Name()) + } + for _, explicitSuggestion := range cmd.SuggestFor { + if strings.EqualFold(typedName, explicitSuggestion) { + suggestions = append(suggestions, cmd.Name()) + } + } + } + } + return suggestions +} + +// VisitParents visits all parents of the command and invokes fn on each parent. +func (c *Command) VisitParents(fn func(*Command)) { + if c.HasParent() { + fn(c.Parent()) + c.Parent().VisitParents(fn) + } +} + +// Root finds root command. +func (c *Command) Root() *Command { + if c.HasParent() { + return c.Parent().Root() + } + return c +} + +// ArgsLenAtDash will return the length of c.Flags().Args at the moment +// when a -- was found during args parsing. +func (c *Command) ArgsLenAtDash() int { + return c.Flags().ArgsLenAtDash() +} + +func (c *Command) execute(a []string) (err error) { + if c == nil { + return fmt.Errorf("Called Execute() on a nil Command") + } + + if len(c.Deprecated) > 0 { + c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated) + } + + // initialize help and version flag at the last point possible to allow for user + // overriding + c.InitDefaultHelpFlag() + c.InitDefaultVersionFlag() + + err = c.ParseFlags(a) + if err != nil { + return c.FlagErrorFunc()(c, err) + } + + // If help is called, regardless of other flags, return we want help. + // Also say we need help if the command isn't runnable. + helpVal, err := c.Flags().GetBool("help") + if err != nil { + // should be impossible to get here as we always declare a help + // flag in InitDefaultHelpFlag() + c.Println("\"help\" flag declared as non-bool. Please correct your code") + return err + } + + if helpVal { + return flag.ErrHelp + } + + // for back-compat, only add version flag behavior if version is defined + if c.Version != "" { + versionVal, err := c.Flags().GetBool("version") + if err != nil { + c.Println("\"version\" flag declared as non-bool. Please correct your code") + return err + } + if versionVal { + err := tmpl(c.OutOrStdout(), c.VersionTemplate(), c) + if err != nil { + c.Println(err) + } + return err + } + } + + if !c.Runnable() { + return flag.ErrHelp + } + + c.preRun() + + defer c.postRun() + + argWoFlags := c.Flags().Args() + if c.DisableFlagParsing { + argWoFlags = a + } + + if err := c.ValidateArgs(argWoFlags); err != nil { + return err + } + + for p := c; p != nil; p = p.Parent() { + if p.PersistentPreRunE != nil { + if err := p.PersistentPreRunE(c, argWoFlags); err != nil { + return err + } + break + } else if p.PersistentPreRun != nil { + p.PersistentPreRun(c, argWoFlags) + break + } + } + if c.PreRunE != nil { + if err := c.PreRunE(c, argWoFlags); err != nil { + return err + } + } else if c.PreRun != nil { + c.PreRun(c, argWoFlags) + } + + if err := c.ValidateRequiredFlags(); err != nil { + return err + } + if err := c.ValidateFlagGroups(); err != nil { + return err + } + + if c.RunE != nil { + if err := c.RunE(c, argWoFlags); err != nil { + return err + } + } else { + c.Run(c, argWoFlags) + } + if c.PostRunE != nil { + if err := c.PostRunE(c, argWoFlags); err != nil { + return err + } + } else if c.PostRun != nil { + c.PostRun(c, argWoFlags) + } + for p := c; p != nil; p = p.Parent() { + if p.PersistentPostRunE != nil { + if err := p.PersistentPostRunE(c, argWoFlags); err != nil { + return err + } + break + } else if p.PersistentPostRun != nil { + p.PersistentPostRun(c, argWoFlags) + break + } + } + + return nil +} + +func (c *Command) preRun() { + for _, x := range initializers { + x() + } +} + +func (c *Command) postRun() { + for _, x := range finalizers { + x() + } +} + +// ExecuteContext is the same as Execute(), but sets the ctx on the command. +// Retrieve ctx by calling cmd.Context() inside your *Run lifecycle or ValidArgs +// functions. +func (c *Command) ExecuteContext(ctx context.Context) error { + c.ctx = ctx + return c.Execute() +} + +// Execute uses the args (os.Args[1:] by default) +// and run through the command tree finding appropriate matches +// for commands and then corresponding flags. +func (c *Command) Execute() error { + _, err := c.ExecuteC() + return err +} + +// ExecuteContextC is the same as ExecuteC(), but sets the ctx on the command. +// Retrieve ctx by calling cmd.Context() inside your *Run lifecycle or ValidArgs +// functions. +func (c *Command) ExecuteContextC(ctx context.Context) (*Command, error) { + c.ctx = ctx + return c.ExecuteC() +} + +// ExecuteC executes the command. +func (c *Command) ExecuteC() (cmd *Command, err error) { + if c.ctx == nil { + c.ctx = context.Background() + } + + // Regardless of what command execute is called on, run on Root only + if c.HasParent() { + return c.Root().ExecuteC() + } + + // windows hook + if preExecHookFn != nil { + preExecHookFn(c) + } + + // initialize help at the last point to allow for user overriding + c.InitDefaultHelpCmd() + // initialize completion at the last point to allow for user overriding + c.InitDefaultCompletionCmd() + + // Now that all commands have been created, let's make sure all groups + // are properly created also + c.checkCommandGroups() + + args := c.args + + // Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155 + if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" { + args = os.Args[1:] + } + + // initialize the hidden command to be used for shell completion + c.initCompleteCmd(args) + + var flags []string + if c.TraverseChildren { + cmd, flags, err = c.Traverse(args) + } else { + cmd, flags, err = c.Find(args) + } + if err != nil { + // If found parse to a subcommand and then failed, talk about the subcommand + if cmd != nil { + c = cmd + } + if !c.SilenceErrors { + c.PrintErrln("Error:", err.Error()) + c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) + } + return c, err + } + + cmd.commandCalledAs.called = true + if cmd.commandCalledAs.name == "" { + cmd.commandCalledAs.name = cmd.Name() + } + + // We have to pass global context to children command + // if context is present on the parent command. + if cmd.ctx == nil { + cmd.ctx = c.ctx + } + + err = cmd.execute(flags) + if err != nil { + // Always show help if requested, even if SilenceErrors is in + // effect + if errors.Is(err, flag.ErrHelp) { + cmd.HelpFunc()(cmd, args) + return cmd, nil + } + + // If root command has SilenceErrors flagged, + // all subcommands should respect it + if !cmd.SilenceErrors && !c.SilenceErrors { + c.PrintErrln("Error:", err.Error()) + } + + // If root command has SilenceUsage flagged, + // all subcommands should respect it + if !cmd.SilenceUsage && !c.SilenceUsage { + c.Println(cmd.UsageString()) + } + } + return cmd, err +} + +func (c *Command) ValidateArgs(args []string) error { + if c.Args == nil { + return ArbitraryArgs(c, args) + } + return c.Args(c, args) +} + +// ValidateRequiredFlags validates all required flags are present and returns an error otherwise +func (c *Command) ValidateRequiredFlags() error { + if c.DisableFlagParsing { + return nil + } + + flags := c.Flags() + missingFlagNames := []string{} + flags.VisitAll(func(pflag *flag.Flag) { + requiredAnnotation, found := pflag.Annotations[BashCompOneRequiredFlag] + if !found { + return + } + if (requiredAnnotation[0] == "true") && !pflag.Changed { + missingFlagNames = append(missingFlagNames, pflag.Name) + } + }) + + if len(missingFlagNames) > 0 { + return fmt.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlagNames, `", "`)) + } + return nil +} + +// checkCommandGroups checks if a command has been added to a group that does not exists. +// If so, we panic because it indicates a coding error that should be corrected. +func (c *Command) checkCommandGroups() { + for _, sub := range c.commands { + // if Group is not defined let the developer know right away + if sub.GroupID != "" && !c.ContainsGroup(sub.GroupID) { + panic(fmt.Sprintf("group id '%s' is not defined for subcommand '%s'", sub.GroupID, sub.CommandPath())) + } + + sub.checkCommandGroups() + } +} + +// InitDefaultHelpFlag adds default help flag to c. +// It is called automatically by executing the c or by calling help and usage. +// If c already has help flag, it will do nothing. +func (c *Command) InitDefaultHelpFlag() { + c.mergePersistentFlags() + if c.Flags().Lookup("help") == nil { + usage := "help for " + if c.Name() == "" { + usage += "this command" + } else { + usage += c.Name() + } + c.Flags().BoolP("help", "h", false, usage) + _ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"}) + } +} + +// InitDefaultVersionFlag adds default version flag to c. +// It is called automatically by executing the c. +// If c already has a version flag, it will do nothing. +// If c.Version is empty, it will do nothing. +func (c *Command) InitDefaultVersionFlag() { + if c.Version == "" { + return + } + + c.mergePersistentFlags() + if c.Flags().Lookup("version") == nil { + usage := "version for " + if c.Name() == "" { + usage += "this command" + } else { + usage += c.Name() + } + if c.Flags().ShorthandLookup("v") == nil { + c.Flags().BoolP("version", "v", false, usage) + } else { + c.Flags().Bool("version", false, usage) + } + _ = c.Flags().SetAnnotation("version", FlagSetByCobraAnnotation, []string{"true"}) + } +} + +// InitDefaultHelpCmd adds default help command to c. +// It is called automatically by executing the c or by calling help and usage. +// If c already has help command or c has no subcommands, it will do nothing. +func (c *Command) InitDefaultHelpCmd() { + if !c.HasSubCommands() { + return + } + + if c.helpCommand == nil { + c.helpCommand = &Command{ + Use: "help [command]", + Short: "Help about any command", + Long: `Help provides help for any command in the application. +Simply type ` + c.Name() + ` help [path to command] for full details.`, + ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + var completions []string + cmd, _, e := c.Root().Find(args) + if e != nil { + return nil, ShellCompDirectiveNoFileComp + } + if cmd == nil { + // Root help command. + cmd = c.Root() + } + for _, subCmd := range cmd.Commands() { + if subCmd.IsAvailableCommand() || subCmd == cmd.helpCommand { + if strings.HasPrefix(subCmd.Name(), toComplete) { + completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) + } + } + } + return completions, ShellCompDirectiveNoFileComp + }, + Run: func(c *Command, args []string) { + cmd, _, e := c.Root().Find(args) + if cmd == nil || e != nil { + c.Printf("Unknown help topic %#q\n", args) + CheckErr(c.Root().Usage()) + } else { + cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown + cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown + CheckErr(cmd.Help()) + } + }, + GroupID: c.helpCommandGroupID, + } + } + c.RemoveCommand(c.helpCommand) + c.AddCommand(c.helpCommand) +} + +// ResetCommands delete parent, subcommand and help command from c. +func (c *Command) ResetCommands() { + c.parent = nil + c.commands = nil + c.helpCommand = nil + c.parentsPflags = nil +} + +// Sorts commands by their names. +type commandSorterByName []*Command + +func (c commandSorterByName) Len() int { return len(c) } +func (c commandSorterByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c commandSorterByName) Less(i, j int) bool { return c[i].Name() < c[j].Name() } + +// Commands returns a sorted slice of child commands. +func (c *Command) Commands() []*Command { + // do not sort commands if it already sorted or sorting was disabled + if EnableCommandSorting && !c.commandsAreSorted { + sort.Sort(commandSorterByName(c.commands)) + c.commandsAreSorted = true + } + return c.commands +} + +// AddCommand adds one or more commands to this parent command. +func (c *Command) AddCommand(cmds ...*Command) { + for i, x := range cmds { + if cmds[i] == c { + panic("Command can't be a child of itself") + } + cmds[i].parent = c + // update max lengths + usageLen := len(x.Use) + if usageLen > c.commandsMaxUseLen { + c.commandsMaxUseLen = usageLen + } + commandPathLen := len(x.CommandPath()) + if commandPathLen > c.commandsMaxCommandPathLen { + c.commandsMaxCommandPathLen = commandPathLen + } + nameLen := len(x.Name()) + if nameLen > c.commandsMaxNameLen { + c.commandsMaxNameLen = nameLen + } + // If global normalization function exists, update all children + if c.globNormFunc != nil { + x.SetGlobalNormalizationFunc(c.globNormFunc) + } + c.commands = append(c.commands, x) + c.commandsAreSorted = false + } +} + +// Groups returns a slice of child command groups. +func (c *Command) Groups() []*Group { + return c.commandgroups +} + +// AllChildCommandsHaveGroup returns if all subcommands are assigned to a group +func (c *Command) AllChildCommandsHaveGroup() bool { + for _, sub := range c.commands { + if (sub.IsAvailableCommand() || sub == c.helpCommand) && sub.GroupID == "" { + return false + } + } + return true +} + +// ContainsGroup return if groupID exists in the list of command groups. +func (c *Command) ContainsGroup(groupID string) bool { + for _, x := range c.commandgroups { + if x.ID == groupID { + return true + } + } + return false +} + +// AddGroup adds one or more command groups to this parent command. +func (c *Command) AddGroup(groups ...*Group) { + c.commandgroups = append(c.commandgroups, groups...) +} + +// RemoveCommand removes one or more commands from a parent command. +func (c *Command) RemoveCommand(cmds ...*Command) { + commands := []*Command{} +main: + for _, command := range c.commands { + for _, cmd := range cmds { + if command == cmd { + command.parent = nil + continue main + } + } + commands = append(commands, command) + } + c.commands = commands + // recompute all lengths + c.commandsMaxUseLen = 0 + c.commandsMaxCommandPathLen = 0 + c.commandsMaxNameLen = 0 + for _, command := range c.commands { + usageLen := len(command.Use) + if usageLen > c.commandsMaxUseLen { + c.commandsMaxUseLen = usageLen + } + commandPathLen := len(command.CommandPath()) + if commandPathLen > c.commandsMaxCommandPathLen { + c.commandsMaxCommandPathLen = commandPathLen + } + nameLen := len(command.Name()) + if nameLen > c.commandsMaxNameLen { + c.commandsMaxNameLen = nameLen + } + } +} + +// Print is a convenience method to Print to the defined output, fallback to Stderr if not set. +func (c *Command) Print(i ...interface{}) { + fmt.Fprint(c.OutOrStderr(), i...) +} + +// Println is a convenience method to Println to the defined output, fallback to Stderr if not set. +func (c *Command) Println(i ...interface{}) { + c.Print(fmt.Sprintln(i...)) +} + +// Printf is a convenience method to Printf to the defined output, fallback to Stderr if not set. +func (c *Command) Printf(format string, i ...interface{}) { + c.Print(fmt.Sprintf(format, i...)) +} + +// PrintErr is a convenience method to Print to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErr(i ...interface{}) { + fmt.Fprint(c.ErrOrStderr(), i...) +} + +// PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErrln(i ...interface{}) { + c.PrintErr(fmt.Sprintln(i...)) +} + +// PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set. +func (c *Command) PrintErrf(format string, i ...interface{}) { + c.PrintErr(fmt.Sprintf(format, i...)) +} + +// CommandPath returns the full path to this command. +func (c *Command) CommandPath() string { + if c.HasParent() { + return c.Parent().CommandPath() + " " + c.Name() + } + return c.Name() +} + +// UseLine puts out the full usage for a given command (including parents). +func (c *Command) UseLine() string { + var useline string + if c.HasParent() { + useline = c.parent.CommandPath() + " " + c.Use + } else { + useline = c.Use + } + if c.DisableFlagsInUseLine { + return useline + } + if c.HasAvailableFlags() && !strings.Contains(useline, "[flags]") { + useline += " [flags]" + } + return useline +} + +// DebugFlags used to determine which flags have been assigned to which commands +// and which persist. +func (c *Command) DebugFlags() { + c.Println("DebugFlags called on", c.Name()) + var debugflags func(*Command) + + debugflags = func(x *Command) { + if x.HasFlags() || x.HasPersistentFlags() { + c.Println(x.Name()) + } + if x.HasFlags() { + x.flags.VisitAll(func(f *flag.Flag) { + if x.HasPersistentFlags() && x.persistentFlag(f.Name) != nil { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [LP]") + } else { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [L]") + } + }) + } + if x.HasPersistentFlags() { + x.pflags.VisitAll(func(f *flag.Flag) { + if x.HasFlags() { + if x.flags.Lookup(f.Name) == nil { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [P]") + } + } else { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [P]") + } + }) + } + c.Println(x.flagErrorBuf) + if x.HasSubCommands() { + for _, y := range x.commands { + debugflags(y) + } + } + } + + debugflags(c) +} + +// Name returns the command's name: the first word in the use line. +func (c *Command) Name() string { + name := c.Use + i := strings.Index(name, " ") + if i >= 0 { + name = name[:i] + } + return name +} + +// HasAlias determines if a given string is an alias of the command. +func (c *Command) HasAlias(s string) bool { + for _, a := range c.Aliases { + if commandNameMatches(a, s) { + return true + } + } + return false +} + +// CalledAs returns the command name or alias that was used to invoke +// this command or an empty string if the command has not been called. +func (c *Command) CalledAs() string { + if c.commandCalledAs.called { + return c.commandCalledAs.name + } + return "" +} + +// hasNameOrAliasPrefix returns true if the Name or any of aliases start +// with prefix +func (c *Command) hasNameOrAliasPrefix(prefix string) bool { + if strings.HasPrefix(c.Name(), prefix) { + c.commandCalledAs.name = c.Name() + return true + } + for _, alias := range c.Aliases { + if strings.HasPrefix(alias, prefix) { + c.commandCalledAs.name = alias + return true + } + } + return false +} + +// NameAndAliases returns a list of the command name and all aliases +func (c *Command) NameAndAliases() string { + return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ") +} + +// HasExample determines if the command has example. +func (c *Command) HasExample() bool { + return len(c.Example) > 0 +} + +// Runnable determines if the command is itself runnable. +func (c *Command) Runnable() bool { + return c.Run != nil || c.RunE != nil +} + +// HasSubCommands determines if the command has children commands. +func (c *Command) HasSubCommands() bool { + return len(c.commands) > 0 +} + +// IsAvailableCommand determines if a command is available as a non-help command +// (this includes all non deprecated/hidden commands). +func (c *Command) IsAvailableCommand() bool { + if len(c.Deprecated) != 0 || c.Hidden { + return false + } + + if c.HasParent() && c.Parent().helpCommand == c { + return false + } + + if c.Runnable() || c.HasAvailableSubCommands() { + return true + } + + return false +} + +// IsAdditionalHelpTopicCommand determines if a command is an additional +// help topic command; additional help topic command is determined by the +// fact that it is NOT runnable/hidden/deprecated, and has no sub commands that +// are runnable/hidden/deprecated. +// Concrete example: https://github.com/spf13/cobra/issues/393#issuecomment-282741924. +func (c *Command) IsAdditionalHelpTopicCommand() bool { + // if a command is runnable, deprecated, or hidden it is not a 'help' command + if c.Runnable() || len(c.Deprecated) != 0 || c.Hidden { + return false + } + + // if any non-help sub commands are found, the command is not a 'help' command + for _, sub := range c.commands { + if !sub.IsAdditionalHelpTopicCommand() { + return false + } + } + + // the command either has no sub commands, or no non-help sub commands + return true +} + +// HasHelpSubCommands determines if a command has any available 'help' sub commands +// that need to be shown in the usage/help default template under 'additional help +// topics'. +func (c *Command) HasHelpSubCommands() bool { + // return true on the first found available 'help' sub command + for _, sub := range c.commands { + if sub.IsAdditionalHelpTopicCommand() { + return true + } + } + + // the command either has no sub commands, or no available 'help' sub commands + return false +} + +// HasAvailableSubCommands determines if a command has available sub commands that +// need to be shown in the usage/help default template under 'available commands'. +func (c *Command) HasAvailableSubCommands() bool { + // return true on the first found available (non deprecated/help/hidden) + // sub command + for _, sub := range c.commands { + if sub.IsAvailableCommand() { + return true + } + } + + // the command either has no sub commands, or no available (non deprecated/help/hidden) + // sub commands + return false +} + +// HasParent determines if the command is a child command. +func (c *Command) HasParent() bool { + return c.parent != nil +} + +// GlobalNormalizationFunc returns the global normalization function or nil if it doesn't exist. +func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) flag.NormalizedName { + return c.globNormFunc +} + +// Flags returns the complete FlagSet that applies +// to this command (local and persistent declared here and by all parents). +func (c *Command) Flags() *flag.FlagSet { + if c.flags == nil { + c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.flags.SetOutput(c.flagErrorBuf) + } + + return c.flags +} + +// LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands. +func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { + persistentFlags := c.PersistentFlags() + + out := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.LocalFlags().VisitAll(func(f *flag.Flag) { + if persistentFlags.Lookup(f.Name) == nil { + out.AddFlag(f) + } + }) + return out +} + +// LocalFlags returns the local FlagSet specifically set in the current command. +func (c *Command) LocalFlags() *flag.FlagSet { + c.mergePersistentFlags() + + if c.lflags == nil { + c.lflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.lflags.SetOutput(c.flagErrorBuf) + } + c.lflags.SortFlags = c.Flags().SortFlags + if c.globNormFunc != nil { + c.lflags.SetNormalizeFunc(c.globNormFunc) + } + + addToLocal := func(f *flag.Flag) { + // Add the flag if it is not a parent PFlag, or it shadows a parent PFlag + if c.lflags.Lookup(f.Name) == nil && f != c.parentsPflags.Lookup(f.Name) { + c.lflags.AddFlag(f) + } + } + c.Flags().VisitAll(addToLocal) + c.PersistentFlags().VisitAll(addToLocal) + return c.lflags +} + +// InheritedFlags returns all flags which were inherited from parent commands. +func (c *Command) InheritedFlags() *flag.FlagSet { + c.mergePersistentFlags() + + if c.iflags == nil { + c.iflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.iflags.SetOutput(c.flagErrorBuf) + } + + local := c.LocalFlags() + if c.globNormFunc != nil { + c.iflags.SetNormalizeFunc(c.globNormFunc) + } + + c.parentsPflags.VisitAll(func(f *flag.Flag) { + if c.iflags.Lookup(f.Name) == nil && local.Lookup(f.Name) == nil { + c.iflags.AddFlag(f) + } + }) + return c.iflags +} + +// NonInheritedFlags returns all flags which were not inherited from parent commands. +func (c *Command) NonInheritedFlags() *flag.FlagSet { + return c.LocalFlags() +} + +// PersistentFlags returns the persistent FlagSet specifically set in the current command. +func (c *Command) PersistentFlags() *flag.FlagSet { + if c.pflags == nil { + c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.pflags.SetOutput(c.flagErrorBuf) + } + return c.pflags +} + +// ResetFlags deletes all flags from command. +func (c *Command) ResetFlags() { + c.flagErrorBuf = new(bytes.Buffer) + c.flagErrorBuf.Reset() + c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.flags.SetOutput(c.flagErrorBuf) + c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.pflags.SetOutput(c.flagErrorBuf) + + c.lflags = nil + c.iflags = nil + c.parentsPflags = nil +} + +// HasFlags checks if the command contains any flags (local plus persistent from the entire structure). +func (c *Command) HasFlags() bool { + return c.Flags().HasFlags() +} + +// HasPersistentFlags checks if the command contains persistent flags. +func (c *Command) HasPersistentFlags() bool { + return c.PersistentFlags().HasFlags() +} + +// HasLocalFlags checks if the command has flags specifically declared locally. +func (c *Command) HasLocalFlags() bool { + return c.LocalFlags().HasFlags() +} + +// HasInheritedFlags checks if the command has flags inherited from its parent command. +func (c *Command) HasInheritedFlags() bool { + return c.InheritedFlags().HasFlags() +} + +// HasAvailableFlags checks if the command contains any flags (local plus persistent from the entire +// structure) which are not hidden or deprecated. +func (c *Command) HasAvailableFlags() bool { + return c.Flags().HasAvailableFlags() +} + +// HasAvailablePersistentFlags checks if the command contains persistent flags which are not hidden or deprecated. +func (c *Command) HasAvailablePersistentFlags() bool { + return c.PersistentFlags().HasAvailableFlags() +} + +// HasAvailableLocalFlags checks if the command has flags specifically declared locally which are not hidden +// or deprecated. +func (c *Command) HasAvailableLocalFlags() bool { + return c.LocalFlags().HasAvailableFlags() +} + +// HasAvailableInheritedFlags checks if the command has flags inherited from its parent command which are +// not hidden or deprecated. +func (c *Command) HasAvailableInheritedFlags() bool { + return c.InheritedFlags().HasAvailableFlags() +} + +// Flag climbs up the command tree looking for matching flag. +func (c *Command) Flag(name string) (flag *flag.Flag) { + flag = c.Flags().Lookup(name) + + if flag == nil { + flag = c.persistentFlag(name) + } + + return +} + +// Recursively find matching persistent flag. +func (c *Command) persistentFlag(name string) (flag *flag.Flag) { + if c.HasPersistentFlags() { + flag = c.PersistentFlags().Lookup(name) + } + + if flag == nil { + c.updateParentsPflags() + flag = c.parentsPflags.Lookup(name) + } + return +} + +// ParseFlags parses persistent flag tree and local flags. +func (c *Command) ParseFlags(args []string) error { + if c.DisableFlagParsing { + return nil + } + + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + beforeErrorBufLen := c.flagErrorBuf.Len() + c.mergePersistentFlags() + + // do it here after merging all flags and just before parse + c.Flags().ParseErrorsWhitelist = flag.ParseErrorsWhitelist(c.FParseErrWhitelist) + + err := c.Flags().Parse(args) + // Print warnings if they occurred (e.g. deprecated flag messages). + if c.flagErrorBuf.Len()-beforeErrorBufLen > 0 && err == nil { + c.Print(c.flagErrorBuf.String()) + } + + return err +} + +// Parent returns a commands parent command. +func (c *Command) Parent() *Command { + return c.parent +} + +// mergePersistentFlags merges c.PersistentFlags() to c.Flags() +// and adds missing persistent flags of all parents. +func (c *Command) mergePersistentFlags() { + c.updateParentsPflags() + c.Flags().AddFlagSet(c.PersistentFlags()) + c.Flags().AddFlagSet(c.parentsPflags) +} + +// updateParentsPflags updates c.parentsPflags by adding +// new persistent flags of all parents. +// If c.parentsPflags == nil, it makes new. +func (c *Command) updateParentsPflags() { + if c.parentsPflags == nil { + c.parentsPflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.parentsPflags.SetOutput(c.flagErrorBuf) + c.parentsPflags.SortFlags = false + } + + if c.globNormFunc != nil { + c.parentsPflags.SetNormalizeFunc(c.globNormFunc) + } + + c.Root().PersistentFlags().AddFlagSet(flag.CommandLine) + + c.VisitParents(func(parent *Command) { + c.parentsPflags.AddFlagSet(parent.PersistentFlags()) + }) +} + +// commandNameMatches checks if two command names are equal +// taking into account case sensitivity according to +// EnableCaseInsensitive global configuration. +func commandNameMatches(s string, t string) bool { + if EnableCaseInsensitive { + return strings.EqualFold(s, t) + } + + return s == t +} diff --git a/vendor/github.com/spf13/cobra/command_notwin.go b/vendor/github.com/spf13/cobra/command_notwin.go new file mode 100644 index 0000000000..307f0c127f --- /dev/null +++ b/vendor/github.com/spf13/cobra/command_notwin.go @@ -0,0 +1,20 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows +// +build !windows + +package cobra + +var preExecHookFn func(*Command) diff --git a/vendor/github.com/spf13/cobra/command_win.go b/vendor/github.com/spf13/cobra/command_win.go new file mode 100644 index 0000000000..adbef395c2 --- /dev/null +++ b/vendor/github.com/spf13/cobra/command_win.go @@ -0,0 +1,41 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows +// +build windows + +package cobra + +import ( + "fmt" + "os" + "time" + + "github.com/inconshreveable/mousetrap" +) + +var preExecHookFn = preExecHook + +func preExecHook(c *Command) { + if MousetrapHelpText != "" && mousetrap.StartedByExplorer() { + c.Print(MousetrapHelpText) + if MousetrapDisplayDuration > 0 { + time.Sleep(MousetrapDisplayDuration) + } else { + c.Println("Press return to continue...") + fmt.Scanln() + } + os.Exit(1) + } +} diff --git a/vendor/github.com/spf13/cobra/completions.go b/vendor/github.com/spf13/cobra/completions.go new file mode 100644 index 0000000000..ee38c4d0b8 --- /dev/null +++ b/vendor/github.com/spf13/cobra/completions.go @@ -0,0 +1,878 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/spf13/pflag" +) + +const ( + // ShellCompRequestCmd is the name of the hidden command that is used to request + // completion results from the program. It is used by the shell completion scripts. + ShellCompRequestCmd = "__complete" + // ShellCompNoDescRequestCmd is the name of the hidden command that is used to request + // completion results without their description. It is used by the shell completion scripts. + ShellCompNoDescRequestCmd = "__completeNoDesc" +) + +// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it. +var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){} + +// lock for reading and writing from flagCompletionFunctions +var flagCompletionMutex = &sync.RWMutex{} + +// ShellCompDirective is a bit map representing the different behaviors the shell +// can be instructed to have once completions have been provided. +type ShellCompDirective int + +type flagCompError struct { + subCommand string + flagName string +} + +func (e *flagCompError) Error() string { + return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'" +} + +const ( + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. + ShellCompDirectiveError ShellCompDirective = 1 << iota + + // ShellCompDirectiveNoSpace indicates that the shell should not add a space + // after the completion even if there is a single completion provided. + ShellCompDirectiveNoSpace + + // ShellCompDirectiveNoFileComp indicates that the shell should not provide + // file completion even when no completion is provided. + ShellCompDirectiveNoFileComp + + // ShellCompDirectiveFilterFileExt indicates that the provided completions + // should be used as file extension filters. + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() + // is a shortcut to using this directive explicitly. The BashCompFilenameExt + // annotation can also be used to obtain the same behavior for flags. + ShellCompDirectiveFilterFileExt + + // ShellCompDirectiveFilterDirs indicates that only directory names should + // be provided in file completion. To request directory names within another + // directory, the returned completions should specify the directory within + // which to search. The BashCompSubdirsInDir annotation can be used to + // obtain the same behavior but only for flags. + ShellCompDirectiveFilterDirs + + // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order + // in which the completions are provided + ShellCompDirectiveKeepOrder + + // =========================================================================== + + // All directives using iota should be above this one. + // For internal use. + shellCompDirectiveMaxValue + + // ShellCompDirectiveDefault indicates to let the shell perform its default + // behavior after completions have been provided. + // This one must be last to avoid messing up the iota count. + ShellCompDirectiveDefault ShellCompDirective = 0 +) + +const ( + // Constants for the completion command + compCmdName = "completion" + compCmdNoDescFlagName = "no-descriptions" + compCmdNoDescFlagDesc = "disable completion descriptions" + compCmdNoDescFlagDefault = false +) + +// CompletionOptions are the options to control shell completion +type CompletionOptions struct { + // DisableDefaultCmd prevents Cobra from creating a default 'completion' command + DisableDefaultCmd bool + // DisableNoDescFlag prevents Cobra from creating the '--no-descriptions' flag + // for shells that support completion descriptions + DisableNoDescFlag bool + // DisableDescriptions turns off all completion descriptions for shells + // that support them + DisableDescriptions bool + // HiddenDefaultCmd makes the default 'completion' command hidden + HiddenDefaultCmd bool +} + +// NoFileCompletions can be used to disable file completion for commands that should +// not trigger file completions. +func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return nil, ShellCompDirectiveNoFileComp +} + +// FixedCompletions can be used to create a completion function which always +// returns the same results. +func FixedCompletions(choices []string, directive ShellCompDirective) func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return choices, directive + } +} + +// RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag. +func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)) error { + flag := c.Flag(flagName) + if flag == nil { + return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName) + } + flagCompletionMutex.Lock() + defer flagCompletionMutex.Unlock() + + if _, exists := flagCompletionFunctions[flag]; exists { + return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName) + } + flagCompletionFunctions[flag] = f + return nil +} + +// Returns a string listing the different directive enabled in the specified parameter +func (d ShellCompDirective) string() string { + var directives []string + if d&ShellCompDirectiveError != 0 { + directives = append(directives, "ShellCompDirectiveError") + } + if d&ShellCompDirectiveNoSpace != 0 { + directives = append(directives, "ShellCompDirectiveNoSpace") + } + if d&ShellCompDirectiveNoFileComp != 0 { + directives = append(directives, "ShellCompDirectiveNoFileComp") + } + if d&ShellCompDirectiveFilterFileExt != 0 { + directives = append(directives, "ShellCompDirectiveFilterFileExt") + } + if d&ShellCompDirectiveFilterDirs != 0 { + directives = append(directives, "ShellCompDirectiveFilterDirs") + } + if d&ShellCompDirectiveKeepOrder != 0 { + directives = append(directives, "ShellCompDirectiveKeepOrder") + } + if len(directives) == 0 { + directives = append(directives, "ShellCompDirectiveDefault") + } + + if d >= shellCompDirectiveMaxValue { + return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d) + } + return strings.Join(directives, ", ") +} + +// initCompleteCmd adds a special hidden command that can be used to request custom completions. +func (c *Command) initCompleteCmd(args []string) { + completeCmd := &Command{ + Use: fmt.Sprintf("%s [command-line]", ShellCompRequestCmd), + Aliases: []string{ShellCompNoDescRequestCmd}, + DisableFlagsInUseLine: true, + Hidden: true, + DisableFlagParsing: true, + Args: MinimumNArgs(1), + Short: "Request shell completion choices for the specified command-line", + Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s", + "to request completion choices for the specified command-line.", ShellCompRequestCmd), + Run: func(cmd *Command, args []string) { + finalCmd, completions, directive, err := cmd.getCompletions(args) + if err != nil { + CompErrorln(err.Error()) + // Keep going for multiple reasons: + // 1- There could be some valid completions even though there was an error + // 2- Even without completions, we need to print the directive + } + + noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) + for _, comp := range completions { + if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable { + // Remove all activeHelp entries in this case + if strings.HasPrefix(comp, activeHelpMarker) { + continue + } + } + if noDescriptions { + // Remove any description that may be included following a tab character. + comp = strings.Split(comp, "\t")[0] + } + + // Make sure we only write the first line to the output. + // This is needed if a description contains a linebreak. + // Otherwise the shell scripts will interpret the other lines as new flags + // and could therefore provide a wrong completion. + comp = strings.Split(comp, "\n")[0] + + // Finally trim the completion. This is especially important to get rid + // of a trailing tab when there are no description following it. + // For example, a sub-command without a description should not be completed + // with a tab at the end (or else zsh will show a -- following it + // although there is no description). + comp = strings.TrimSpace(comp) + + // Print each possible completion to stdout for the completion script to consume. + fmt.Fprintln(finalCmd.OutOrStdout(), comp) + } + + // As the last printout, print the completion directive for the completion script to parse. + // The directive integer must be that last character following a single colon (:). + // The completion script expects : + fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive) + + // Print some helpful info to stderr for the user to understand. + // Output from stderr must be ignored by the completion script. + fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string()) + }, + } + c.AddCommand(completeCmd) + subCmd, _, err := c.Find(args) + if err != nil || subCmd.Name() != ShellCompRequestCmd { + // Only create this special command if it is actually being called. + // This reduces possible side-effects of creating such a command; + // for example, having this command would cause problems to a + // cobra program that only consists of the root command, since this + // command would cause the root command to suddenly have a subcommand. + c.RemoveCommand(completeCmd) + } +} + +func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) { + // The last argument, which is not completely typed by the user, + // should not be part of the list of arguments + toComplete := args[len(args)-1] + trimmedArgs := args[:len(args)-1] + + var finalCmd *Command + var finalArgs []string + var err error + // Find the real command for which completion must be performed + // check if we need to traverse here to parse local flags on parent commands + if c.Root().TraverseChildren { + finalCmd, finalArgs, err = c.Root().Traverse(trimmedArgs) + } else { + // For Root commands that don't specify any value for their Args fields, when we call + // Find(), if those Root commands don't have any sub-commands, they will accept arguments. + // However, because we have added the __complete sub-command in the current code path, the + // call to Find() -> legacyArgs() will return an error if there are any arguments. + // To avoid this, we first remove the __complete command to get back to having no sub-commands. + rootCmd := c.Root() + if len(rootCmd.Commands()) == 1 { + rootCmd.RemoveCommand(c) + } + + finalCmd, finalArgs, err = rootCmd.Find(trimmedArgs) + } + if err != nil { + // Unable to find the real command. E.g., someInvalidCmd + return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) + } + finalCmd.ctx = c.ctx + + // These flags are normally added when `execute()` is called on `finalCmd`, + // however, when doing completion, we don't call `finalCmd.execute()`. + // Let's add the --help and --version flag ourselves. + finalCmd.InitDefaultHelpFlag() + finalCmd.InitDefaultVersionFlag() + + // Check if we are doing flag value completion before parsing the flags. + // This is important because if we are completing a flag value, we need to also + // remove the flag name argument from the list of finalArgs or else the parsing + // could fail due to an invalid value (incomplete) for the flag. + flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete) + + // Check if interspersed is false or -- was set on a previous arg. + // This works by counting the arguments. Normally -- is not counted as arg but + // if -- was already set or interspersed is false and there is already one arg then + // the extra added -- is counted as arg. + flagCompletion := true + _ = finalCmd.ParseFlags(append(finalArgs, "--")) + newArgCount := finalCmd.Flags().NArg() + + // Parse the flags early so we can check if required flags are set + if err = finalCmd.ParseFlags(finalArgs); err != nil { + return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error()) + } + + realArgCount := finalCmd.Flags().NArg() + if newArgCount > realArgCount { + // don't do flag completion (see above) + flagCompletion = false + } + // Error while attempting to parse flags + if flagErr != nil { + // If error type is flagCompError and we don't want flagCompletion we should ignore the error + if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) { + return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr + } + } + + // Look for the --help or --version flags. If they are present, + // there should be no further completions. + if helpOrVersionFlagPresent(finalCmd) { + return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil + } + + // We only remove the flags from the arguments if DisableFlagParsing is not set. + // This is important for commands which have requested to do their own flag completion. + if !finalCmd.DisableFlagParsing { + finalArgs = finalCmd.Flags().Args() + } + + if flag != nil && flagCompletion { + // Check if we are completing a flag value subject to annotations + if validExts, present := flag.Annotations[BashCompFilenameExt]; present { + if len(validExts) != 0 { + // File completion filtered by extensions + return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil + } + + // The annotation requests simple file completion. There is no reason to do + // that since it is the default behavior anyway. Let's ignore this annotation + // in case the program also registered a completion function for this flag. + // Even though it is a mistake on the program's side, let's be nice when we can. + } + + if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present { + if len(subDir) == 1 { + // Directory completion from within a directory + return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil + } + // Directory completion + return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil + } + } + + var completions []string + var directive ShellCompDirective + + // Enforce flag groups before doing flag completions + finalCmd.enforceFlagGroupsForCompletion() + + // Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true; + // doing this allows for completion of persistent flag names even for commands that disable flag parsing. + // + // When doing completion of a flag name, as soon as an argument starts with + // a '-' we know it is a flag. We cannot use isFlagArg() here as it requires + // the flag name to be complete + if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion { + // First check for required flags + completions = completeRequireFlags(finalCmd, toComplete) + + // If we have not found any required flags, only then can we show regular flags + if len(completions) == 0 { + doCompleteFlags := func(flag *pflag.Flag) { + if !flag.Changed || + strings.Contains(flag.Value.Type(), "Slice") || + strings.Contains(flag.Value.Type(), "Array") { + // If the flag is not already present, or if it can be specified multiple times (Array or Slice) + // we suggest it as a completion + completions = append(completions, getFlagNameCompletions(flag, toComplete)...) + } + } + + // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands + // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and + // non-inherited flags. + finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { + doCompleteFlags(flag) + }) + finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + doCompleteFlags(flag) + }) + } + + directive = ShellCompDirectiveNoFileComp + if len(completions) == 1 && strings.HasSuffix(completions[0], "=") { + // If there is a single completion, the shell usually adds a space + // after the completion. We don't want that if the flag ends with an = + directive = ShellCompDirectiveNoSpace + } + + if !finalCmd.DisableFlagParsing { + // If DisableFlagParsing==false, we have completed the flags as known by Cobra; + // we can return what we found. + // If DisableFlagParsing==true, Cobra may not be aware of all flags, so we + // let the logic continue to see if ValidArgsFunction needs to be called. + return finalCmd, completions, directive, nil + } + } else { + directive = ShellCompDirectiveDefault + if flag == nil { + foundLocalNonPersistentFlag := false + // If TraverseChildren is true on the root command we don't check for + // local flags because we can use a local flag on a parent command + if !finalCmd.Root().TraverseChildren { + // Check if there are any local, non-persistent flags on the command-line + localNonPersistentFlags := finalCmd.LocalNonPersistentFlags() + finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed { + foundLocalNonPersistentFlag = true + } + }) + } + + // Complete subcommand names, including the help command + if len(finalArgs) == 0 && !foundLocalNonPersistentFlag { + // We only complete sub-commands if: + // - there are no arguments on the command-line and + // - there are no local, non-persistent flags on the command-line or TraverseChildren is true + for _, subCmd := range finalCmd.Commands() { + if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand { + if strings.HasPrefix(subCmd.Name(), toComplete) { + completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name(), subCmd.Short)) + } + directive = ShellCompDirectiveNoFileComp + } + } + } + + // Complete required flags even without the '-' prefix + completions = append(completions, completeRequireFlags(finalCmd, toComplete)...) + + // Always complete ValidArgs, even if we are completing a subcommand name. + // This is for commands that have both subcommands and ValidArgs. + if len(finalCmd.ValidArgs) > 0 { + if len(finalArgs) == 0 { + // ValidArgs are only for the first argument + for _, validArg := range finalCmd.ValidArgs { + if strings.HasPrefix(validArg, toComplete) { + completions = append(completions, validArg) + } + } + directive = ShellCompDirectiveNoFileComp + + // If no completions were found within commands or ValidArgs, + // see if there are any ArgAliases that should be completed. + if len(completions) == 0 { + for _, argAlias := range finalCmd.ArgAliases { + if strings.HasPrefix(argAlias, toComplete) { + completions = append(completions, argAlias) + } + } + } + } + + // If there are ValidArgs specified (even if they don't match), we stop completion. + // Only one of ValidArgs or ValidArgsFunction can be used for a single command. + return finalCmd, completions, directive, nil + } + + // Let the logic continue so as to add any ValidArgsFunction completions, + // even if we already found sub-commands. + // This is for commands that have subcommands but also specify a ValidArgsFunction. + } + } + + // Find the completion function for the flag or command + var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) + if flag != nil && flagCompletion { + flagCompletionMutex.RLock() + completionFn = flagCompletionFunctions[flag] + flagCompletionMutex.RUnlock() + } else { + completionFn = finalCmd.ValidArgsFunction + } + if completionFn != nil { + // Go custom completion defined for this flag or command. + // Call the registered completion function to get the completions. + var comps []string + comps, directive = completionFn(finalCmd, finalArgs, toComplete) + completions = append(completions, comps...) + } + + return finalCmd, completions, directive, nil +} + +func helpOrVersionFlagPresent(cmd *Command) bool { + if versionFlag := cmd.Flags().Lookup("version"); versionFlag != nil && + len(versionFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && versionFlag.Changed { + return true + } + if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil && + len(helpFlag.Annotations[FlagSetByCobraAnnotation]) > 0 && helpFlag.Changed { + return true + } + return false +} + +func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string { + if nonCompletableFlag(flag) { + return []string{} + } + + var completions []string + flagName := "--" + flag.Name + if strings.HasPrefix(flagName, toComplete) { + // Flag without the = + completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) + + // Why suggest both long forms: --flag and --flag= ? + // This forces the user to *always* have to type either an = or a space after the flag name. + // Let's be nice and avoid making users have to do that. + // Since boolean flags and shortname flags don't show the = form, let's go that route and never show it. + // The = form will still work, we just won't suggest it. + // This also makes the list of suggested flags shorter as we avoid all the = forms. + // + // if len(flag.NoOptDefVal) == 0 { + // // Flag requires a value, so it can be suffixed with = + // flagName += "=" + // completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) + // } + } + + flagName = "-" + flag.Shorthand + if len(flag.Shorthand) > 0 && strings.HasPrefix(flagName, toComplete) { + completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Usage)) + } + + return completions +} + +func completeRequireFlags(finalCmd *Command, toComplete string) []string { + var completions []string + + doCompleteRequiredFlags := func(flag *pflag.Flag) { + if _, present := flag.Annotations[BashCompOneRequiredFlag]; present { + if !flag.Changed { + // If the flag is not already present, we suggest it as a completion + completions = append(completions, getFlagNameCompletions(flag, toComplete)...) + } + } + } + + // We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands + // that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and + // non-inherited flags. + finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { + doCompleteRequiredFlags(flag) + }) + finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + doCompleteRequiredFlags(flag) + }) + + return completions +} + +func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) { + if finalCmd.DisableFlagParsing { + // We only do flag completion if we are allowed to parse flags + // This is important for commands which have requested to do their own flag completion. + return nil, args, lastArg, nil + } + + var flagName string + trimmedArgs := args + flagWithEqual := false + orgLastArg := lastArg + + // When doing completion of a flag name, as soon as an argument starts with + // a '-' we know it is a flag. We cannot use isFlagArg() here as that function + // requires the flag name to be complete + if len(lastArg) > 0 && lastArg[0] == '-' { + if index := strings.Index(lastArg, "="); index >= 0 { + // Flag with an = + if strings.HasPrefix(lastArg[:index], "--") { + // Flag has full name + flagName = lastArg[2:index] + } else { + // Flag is shorthand + // We have to get the last shorthand flag name + // e.g. `-asd` => d to provide the correct completion + // https://github.com/spf13/cobra/issues/1257 + flagName = lastArg[index-1 : index] + } + lastArg = lastArg[index+1:] + flagWithEqual = true + } else { + // Normal flag completion + return nil, args, lastArg, nil + } + } + + if len(flagName) == 0 { + if len(args) > 0 { + prevArg := args[len(args)-1] + if isFlagArg(prevArg) { + // Only consider the case where the flag does not contain an =. + // If the flag contains an = it means it has already been fully processed, + // so we don't need to deal with it here. + if index := strings.Index(prevArg, "="); index < 0 { + if strings.HasPrefix(prevArg, "--") { + // Flag has full name + flagName = prevArg[2:] + } else { + // Flag is shorthand + // We have to get the last shorthand flag name + // e.g. `-asd` => d to provide the correct completion + // https://github.com/spf13/cobra/issues/1257 + flagName = prevArg[len(prevArg)-1:] + } + // Remove the uncompleted flag or else there could be an error created + // for an invalid value for that flag + trimmedArgs = args[:len(args)-1] + } + } + } + } + + if len(flagName) == 0 { + // Not doing flag completion + return nil, trimmedArgs, lastArg, nil + } + + flag := findFlag(finalCmd, flagName) + if flag == nil { + // Flag not supported by this command, the interspersed option might be set so return the original args + return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name(), flagName: flagName} + } + + if !flagWithEqual { + if len(flag.NoOptDefVal) != 0 { + // We had assumed dealing with a two-word flag but the flag is a boolean flag. + // In that case, there is no value following it, so we are not really doing flag completion. + // Reset everything to do noun completion. + trimmedArgs = args + flag = nil + } + } + + return flag, trimmedArgs, lastArg, nil +} + +// InitDefaultCompletionCmd adds a default 'completion' command to c. +// This function will do nothing if any of the following is true: +// 1- the feature has been explicitly disabled by the program, +// 2- c has no subcommands (to avoid creating one), +// 3- c already has a 'completion' command provided by the program. +func (c *Command) InitDefaultCompletionCmd() { + if c.CompletionOptions.DisableDefaultCmd || !c.HasSubCommands() { + return + } + + for _, cmd := range c.commands { + if cmd.Name() == compCmdName || cmd.HasAlias(compCmdName) { + // A completion command is already available + return + } + } + + haveNoDescFlag := !c.CompletionOptions.DisableNoDescFlag && !c.CompletionOptions.DisableDescriptions + + completionCmd := &Command{ + Use: compCmdName, + Short: "Generate the autocompletion script for the specified shell", + Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell. +See each sub-command's help for details on how to use the generated script. +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + Hidden: c.CompletionOptions.HiddenDefaultCmd, + GroupID: c.completionCommandGroupID, + } + c.AddCommand(completionCmd) + + out := c.OutOrStdout() + noDesc := c.CompletionOptions.DisableDescriptions + shortDesc := "Generate the autocompletion script for %s" + bash := &Command{ + Use: "bash", + Short: fmt.Sprintf(shortDesc, "bash"), + Long: fmt.Sprintf(`Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(%[1]s completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + %[1]s completion bash > /etc/bash_completion.d/%[1]s + +#### macOS: + + %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s + +You will need to start a new shell for this setup to take effect. +`, c.Root().Name()), + Args: NoArgs, + DisableFlagsInUseLine: true, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + return cmd.Root().GenBashCompletionV2(out, !noDesc) + }, + } + if haveNoDescFlag { + bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + } + + zsh := &Command{ + Use: "zsh", + Short: fmt.Sprintf(shortDesc, "zsh"), + Long: fmt.Sprintf(`Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(%[1]s completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + %[1]s completion zsh > "${fpath[1]}/_%[1]s" + +#### macOS: + + %[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s + +You will need to start a new shell for this setup to take effect. +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + if noDesc { + return cmd.Root().GenZshCompletionNoDesc(out) + } + return cmd.Root().GenZshCompletion(out) + }, + } + if haveNoDescFlag { + zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + } + + fish := &Command{ + Use: "fish", + Short: fmt.Sprintf(shortDesc, "fish"), + Long: fmt.Sprintf(`Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + %[1]s completion fish | source + +To load completions for every new session, execute once: + + %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish + +You will need to start a new shell for this setup to take effect. +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + return cmd.Root().GenFishCompletion(out, !noDesc) + }, + } + if haveNoDescFlag { + fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + } + + powershell := &Command{ + Use: "powershell", + Short: fmt.Sprintf(shortDesc, "powershell"), + Long: fmt.Sprintf(`Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + %[1]s completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + if noDesc { + return cmd.Root().GenPowerShellCompletion(out) + } + return cmd.Root().GenPowerShellCompletionWithDesc(out) + + }, + } + if haveNoDescFlag { + powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + } + + completionCmd.AddCommand(bash, zsh, fish, powershell) +} + +func findFlag(cmd *Command, name string) *pflag.Flag { + flagSet := cmd.Flags() + if len(name) == 1 { + // First convert the short flag into a long flag + // as the cmd.Flag() search only accepts long flags + if short := flagSet.ShorthandLookup(name); short != nil { + name = short.Name + } else { + set := cmd.InheritedFlags() + if short = set.ShorthandLookup(name); short != nil { + name = short.Name + } else { + return nil + } + } + } + return cmd.Flag(name) +} + +// CompDebug prints the specified string to the same file as where the +// completion script prints its logs. +// Note that completion printouts should never be on stdout as they would +// be wrongly interpreted as actual completion choices by the completion script. +func CompDebug(msg string, printToStdErr bool) { + msg = fmt.Sprintf("[Debug] %s", msg) + + // Such logs are only printed when the user has set the environment + // variable BASH_COMP_DEBUG_FILE to the path of some file to be used. + if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" { + f, err := os.OpenFile(path, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err == nil { + defer f.Close() + WriteStringAndCheck(f, msg) + } + } + + if printToStdErr { + // Must print to stderr for this not to be read by the completion script. + fmt.Fprint(os.Stderr, msg) + } +} + +// CompDebugln prints the specified string with a newline at the end +// to the same file as where the completion script prints its logs. +// Such logs are only printed when the user has set the environment +// variable BASH_COMP_DEBUG_FILE to the path of some file to be used. +func CompDebugln(msg string, printToStdErr bool) { + CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr) +} + +// CompError prints the specified completion message to stderr. +func CompError(msg string) { + msg = fmt.Sprintf("[Error] %s", msg) + CompDebug(msg, true) +} + +// CompErrorln prints the specified completion message to stderr with a newline at the end. +func CompErrorln(msg string) { + CompError(fmt.Sprintf("%s\n", msg)) +} diff --git a/vendor/github.com/spf13/cobra/fish_completions.go b/vendor/github.com/spf13/cobra/fish_completions.go new file mode 100644 index 0000000000..12ca0d2b11 --- /dev/null +++ b/vendor/github.com/spf13/cobra/fish_completions.go @@ -0,0 +1,292 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" +) + +func genFishComp(buf io.StringWriter, name string, includeDesc bool) { + // Variables should not contain a '-' or ':' character + nameForVar := name + nameForVar = strings.ReplaceAll(nameForVar, "-", "_") + nameForVar = strings.ReplaceAll(nameForVar, ":", "_") + + compCmd := ShellCompRequestCmd + if !includeDesc { + compCmd = ShellCompNoDescRequestCmd + } + WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name)) + WriteStringAndCheck(buf, fmt.Sprintf(` +function __%[1]s_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __%[1]s_perform_completion + __%[1]s_debug "Starting __%[1]s_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __%[1]s_debug "args: $args" + __%[1]s_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "%[10]s=0 $args[1] %[3]s $args[2..-1] $lastArg" + + __%[1]s_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __%[1]s_debug "Comps: $comps" + __%[1]s_debug "DirectiveLine: $directiveLine" + __%[1]s_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%%s%%s\n" "$flagPrefix" "$comp" + end + + printf "%%s\n" "$directiveLine" +end + +# this function limits calls to __%[1]s_perform_completion, by caching the result behind $__%[1]s_perform_completion_once_result +function __%[1]s_perform_completion_once + __%[1]s_debug "Starting __%[1]s_perform_completion_once" + + if test -n "$__%[1]s_perform_completion_once_result" + __%[1]s_debug "Seems like a valid result already exists, skipping __%[1]s_perform_completion" + return 0 + end + + set --global __%[1]s_perform_completion_once_result (__%[1]s_perform_completion) + if test -z "$__%[1]s_perform_completion_once_result" + __%[1]s_debug "No completions, probably due to a failure" + return 1 + end + + __%[1]s_debug "Performed completions and set __%[1]s_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__%[1]s_perform_completion_once_result variable after completions are run +function __%[1]s_clear_perform_completion_once_result + __%[1]s_debug "" + __%[1]s_debug "========= clearing previously set __%[1]s_perform_completion_once_result variable ==========" + set --erase __%[1]s_perform_completion_once_result + __%[1]s_debug "Succesfully erased the variable __%[1]s_perform_completion_once_result" +end + +function __%[1]s_requires_order_preservation + __%[1]s_debug "" + __%[1]s_debug "========= checking if order preservation is required ==========" + + __%[1]s_perform_completion_once + if test -z "$__%[1]s_perform_completion_once_result" + __%[1]s_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) + __%[1]s_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder %[9]d + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) %% 2) + __%[1]s_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __%[1]s_debug "This does require order preservation" + return 0 + end + + __%[1]s_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __%[1]s_comp_results +# - Return false if file completion should be performed +function __%[1]s_prepare_completions + __%[1]s_debug "" + __%[1]s_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __%[1]s_comp_results + + __%[1]s_perform_completion_once + __%[1]s_debug "Completion results: $__%[1]s_perform_completion_once_result" + + if test -z "$__%[1]s_perform_completion_once_result" + __%[1]s_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) + set --global __%[1]s_comp_results $__%[1]s_perform_completion_once_result[1..-2] + + __%[1]s_debug "Completions are: $__%[1]s_comp_results" + __%[1]s_debug "Directive is: $directive" + + set -l shellCompDirectiveError %[4]d + set -l shellCompDirectiveNoSpace %[5]d + set -l shellCompDirectiveNoFileComp %[6]d + set -l shellCompDirectiveFilterFileExt %[7]d + set -l shellCompDirectiveFilterDirs %[8]d + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) + if test $compErr -eq 1 + __%[1]s_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __%[1]s_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) + + __%[1]s_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __%[1]s_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results) + set --global __%[1]s_comp_results $completions + __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__%[1]s_comp_results) + __%[1]s_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __%[1]s_debug "Adding second completion to perform nospace directive" + set --global __%[1]s_comp_results $split[1] $split[1]. + __%[1]s_debug "Completions are now: $__%[1]s_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __%[1]s_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "%[2]s" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "%[2]s " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c %[2]s -e + +# this will get called after the two calls below and clear the $__%[1]s_perform_completion_once_result global +complete -c %[2]s -n '__%[1]s_clear_perform_completion_once_result' +# The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c %[2]s -n 'not __%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' +# otherwise we use the -k flag +complete -k -c %[2]s -n '__%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' +`, nameForVar, name, compCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) +} + +// GenFishCompletion generates fish completion file and writes to the passed writer. +func (c *Command) GenFishCompletion(w io.Writer, includeDesc bool) error { + buf := new(bytes.Buffer) + genFishComp(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) + return err +} + +// GenFishCompletionFile generates fish completion file. +func (c *Command) GenFishCompletionFile(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenFishCompletion(outFile, includeDesc) +} diff --git a/vendor/github.com/spf13/cobra/fish_completions.md b/vendor/github.com/spf13/cobra/fish_completions.md new file mode 100644 index 0000000000..19b2ed1293 --- /dev/null +++ b/vendor/github.com/spf13/cobra/fish_completions.md @@ -0,0 +1,4 @@ +## Generating Fish Completions For Your cobra.Command + +Please refer to [Shell Completions](shell_completions.md) for details. + diff --git a/vendor/github.com/spf13/cobra/flag_groups.go b/vendor/github.com/spf13/cobra/flag_groups.go new file mode 100644 index 0000000000..b35fde1554 --- /dev/null +++ b/vendor/github.com/spf13/cobra/flag_groups.go @@ -0,0 +1,224 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "fmt" + "sort" + "strings" + + flag "github.com/spf13/pflag" +) + +const ( + requiredAsGroup = "cobra_annotation_required_if_others_set" + mutuallyExclusive = "cobra_annotation_mutually_exclusive" +) + +// MarkFlagsRequiredTogether marks the given flags with annotations so that Cobra errors +// if the command is invoked with a subset (but not all) of the given flags. +func (c *Command) MarkFlagsRequiredTogether(flagNames ...string) { + c.mergePersistentFlags() + for _, v := range flagNames { + f := c.Flags().Lookup(v) + if f == nil { + panic(fmt.Sprintf("Failed to find flag %q and mark it as being required in a flag group", v)) + } + if err := c.Flags().SetAnnotation(v, requiredAsGroup, append(f.Annotations[requiredAsGroup], strings.Join(flagNames, " "))); err != nil { + // Only errs if the flag isn't found. + panic(err) + } + } +} + +// MarkFlagsMutuallyExclusive marks the given flags with annotations so that Cobra errors +// if the command is invoked with more than one flag from the given set of flags. +func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { + c.mergePersistentFlags() + for _, v := range flagNames { + f := c.Flags().Lookup(v) + if f == nil { + panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a mutually exclusive flag group", v)) + } + // Each time this is called is a single new entry; this allows it to be a member of multiple groups if needed. + if err := c.Flags().SetAnnotation(v, mutuallyExclusive, append(f.Annotations[mutuallyExclusive], strings.Join(flagNames, " "))); err != nil { + panic(err) + } + } +} + +// ValidateFlagGroups validates the mutuallyExclusive/requiredAsGroup logic and returns the +// first error encountered. +func (c *Command) ValidateFlagGroups() error { + if c.DisableFlagParsing { + return nil + } + + flags := c.Flags() + + // groupStatus format is the list of flags as a unique ID, + // then a map of each flag name and whether it is set or not. + groupStatus := map[string]map[string]bool{} + mutuallyExclusiveGroupStatus := map[string]map[string]bool{} + flags.VisitAll(func(pflag *flag.Flag) { + processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) + processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) + }) + + if err := validateRequiredFlagGroups(groupStatus); err != nil { + return err + } + if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil { + return err + } + return nil +} + +func hasAllFlags(fs *flag.FlagSet, flagnames ...string) bool { + for _, fname := range flagnames { + f := fs.Lookup(fname) + if f == nil { + return false + } + } + return true +} + +func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annotation string, groupStatus map[string]map[string]bool) { + groupInfo, found := pflag.Annotations[annotation] + if found { + for _, group := range groupInfo { + if groupStatus[group] == nil { + flagnames := strings.Split(group, " ") + + // Only consider this flag group at all if all the flags are defined. + if !hasAllFlags(flags, flagnames...) { + continue + } + + groupStatus[group] = map[string]bool{} + for _, name := range flagnames { + groupStatus[group][name] = false + } + } + + groupStatus[group][pflag.Name] = pflag.Changed + } + } +} + +func validateRequiredFlagGroups(data map[string]map[string]bool) error { + keys := sortedKeys(data) + for _, flagList := range keys { + flagnameAndStatus := data[flagList] + + unset := []string{} + for flagname, isSet := range flagnameAndStatus { + if !isSet { + unset = append(unset, flagname) + } + } + if len(unset) == len(flagnameAndStatus) || len(unset) == 0 { + continue + } + + // Sort values, so they can be tested/scripted against consistently. + sort.Strings(unset) + return fmt.Errorf("if any flags in the group [%v] are set they must all be set; missing %v", flagList, unset) + } + + return nil +} + +func validateExclusiveFlagGroups(data map[string]map[string]bool) error { + keys := sortedKeys(data) + for _, flagList := range keys { + flagnameAndStatus := data[flagList] + var set []string + for flagname, isSet := range flagnameAndStatus { + if isSet { + set = append(set, flagname) + } + } + if len(set) == 0 || len(set) == 1 { + continue + } + + // Sort values, so they can be tested/scripted against consistently. + sort.Strings(set) + return fmt.Errorf("if any flags in the group [%v] are set none of the others can be; %v were all set", flagList, set) + } + return nil +} + +func sortedKeys(m map[string]map[string]bool) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} + +// enforceFlagGroupsForCompletion will do the following: +// - when a flag in a group is present, other flags in the group will be marked required +// - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden +// This allows the standard completion logic to behave appropriately for flag groups +func (c *Command) enforceFlagGroupsForCompletion() { + if c.DisableFlagParsing { + return + } + + flags := c.Flags() + groupStatus := map[string]map[string]bool{} + mutuallyExclusiveGroupStatus := map[string]map[string]bool{} + c.Flags().VisitAll(func(pflag *flag.Flag) { + processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) + processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) + }) + + // If a flag that is part of a group is present, we make all the other flags + // of that group required so that the shell completion suggests them automatically + for flagList, flagnameAndStatus := range groupStatus { + for _, isSet := range flagnameAndStatus { + if isSet { + // One of the flags of the group is set, mark the other ones as required + for _, fName := range strings.Split(flagList, " ") { + _ = c.MarkFlagRequired(fName) + } + } + } + } + + // If a flag that is mutually exclusive to others is present, we hide the other + // flags of that group so the shell completion does not suggest them + for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus { + for flagName, isSet := range flagnameAndStatus { + if isSet { + // One of the flags of the mutually exclusive group is set, mark the other ones as hidden + // Don't mark the flag that is already set as hidden because it may be an + // array or slice flag and therefore must continue being suggested + for _, fName := range strings.Split(flagList, " ") { + if fName != flagName { + flag := c.Flags().Lookup(fName) + flag.Hidden = true + } + } + } + } + } +} diff --git a/vendor/github.com/spf13/cobra/powershell_completions.go b/vendor/github.com/spf13/cobra/powershell_completions.go new file mode 100644 index 0000000000..177d2755f2 --- /dev/null +++ b/vendor/github.com/spf13/cobra/powershell_completions.go @@ -0,0 +1,325 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The generated scripts require PowerShell v5.0+ (which comes Windows 10, but +// can be downloaded separately for windows 7 or 8.1). + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" +) + +func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) { + // Variables should not contain a '-' or ':' character + nameForVar := name + nameForVar = strings.Replace(nameForVar, "-", "_", -1) + nameForVar = strings.Replace(nameForVar, ":", "_", -1) + + compCmd := ShellCompRequestCmd + if !includeDesc { + compCmd = ShellCompNoDescRequestCmd + } + WriteStringAndCheck(buf, fmt.Sprintf(`# powershell completion for %-36[1]s -*- shell-script -*- + +function __%[1]s_debug { + if ($env:BASH_COMP_DEBUG_FILE) { + "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" + } +} + +filter __%[1]s_escapeStringWithSpecialChars { +`+" $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|\"|`|\\||<|>|&','`$&'"+` +} + +[scriptblock]$__%[2]sCompleterBlock = { + param( + $WordToComplete, + $CommandAst, + $CursorPosition + ) + + # Get the current command line and convert into a string + $Command = $CommandAst.CommandElements + $Command = "$Command" + + __%[1]s_debug "" + __%[1]s_debug "========= starting completion logic ==========" + __%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CursorPosition location, so we need + # to truncate the command-line ($Command) up to the $CursorPosition location. + # Make sure the $Command is longer then the $CursorPosition before we truncate. + # This happens because the $Command does not include the last space. + if ($Command.Length -gt $CursorPosition) { + $Command=$Command.Substring(0,$CursorPosition) + } + __%[1]s_debug "Truncated command: $Command" + + $ShellCompDirectiveError=%[4]d + $ShellCompDirectiveNoSpace=%[5]d + $ShellCompDirectiveNoFileComp=%[6]d + $ShellCompDirectiveFilterFileExt=%[7]d + $ShellCompDirectiveFilterDirs=%[8]d + $ShellCompDirectiveKeepOrder=%[9]d + + # Prepare the command to request completions for the program. + # Split the command at the first space to separate the program and arguments. + $Program,$Arguments = $Command.Split(" ",2) + + $RequestComp="$Program %[3]s $Arguments" + __%[1]s_debug "RequestComp: $RequestComp" + + # we cannot use $WordToComplete because it + # has the wrong values if the cursor was moved + # so use the last argument + if ($WordToComplete -ne "" ) { + $WordToComplete = $Arguments.Split(" ")[-1] + } + __%[1]s_debug "New WordToComplete: $WordToComplete" + + + # Check for flag with equal sign + $IsEqualFlag = ($WordToComplete -Like "--*=*" ) + if ( $IsEqualFlag ) { + __%[1]s_debug "Completing equal sign flag" + # Remove the flag part + $Flag,$WordToComplete = $WordToComplete.Split("=",2) + } + + if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __%[1]s_debug "Adding extra empty parameter" + # PowerShell 7.2+ changed the way how the arguments are passed to executables, + # so for pre-7.2 or when Legacy argument passing is enabled we need to use +`+" # `\"`\" to pass an empty argument, a \"\" or '' does not work!!!"+` + if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or + ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or + (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and + $PSNativeCommandArgumentPassing -eq 'Legacy')) { +`+" $RequestComp=\"$RequestComp\" + ' `\"`\"'"+` + } else { + $RequestComp="$RequestComp" + ' ""' + } + } + + __%[1]s_debug "Calling $RequestComp" + # First disable ActiveHelp which is not supported for Powershell + $env:%[10]s=0 + + #call the command store the output in $out and redirect stderr and stdout to null + # $Out is an array contains each line per element + Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null + + # get directive from last line + [int]$Directive = $Out[-1].TrimStart(':') + if ($Directive -eq "") { + # There is no directive specified + $Directive = 0 + } + __%[1]s_debug "The completion directive is: $Directive" + + # remove directive (last element) from out + $Out = $Out | Where-Object { $_ -ne $Out[-1] } + __%[1]s_debug "The completions are: $Out" + + if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { + # Error code. No completion. + __%[1]s_debug "Received error from custom completion go code" + return + } + + $Longest = 0 + [Array]$Values = $Out | ForEach-Object { + #Split the output in name and description +`+" $Name, $Description = $_.Split(\"`t\",2)"+` + __%[1]s_debug "Name: $Name Description: $Description" + + # Look for the longest completion so that we can format things nicely + if ($Longest -lt $Name.Length) { + $Longest = $Name.Length + } + + # Set the description to a one space string if there is none set. + # This is needed because the CompletionResult does not accept an empty string as argument + if (-Not $Description) { + $Description = " " + } + @{Name="$Name";Description="$Description"} + } + + + $Space = " " + if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { + # remove the space here + __%[1]s_debug "ShellCompDirectiveNoSpace is called" + $Space = "" + } + + if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or + (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { + __%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" + + # return here to prevent the completion of the extensions + return + } + + $Values = $Values | Where-Object { + # filter the result + $_.Name -like "$WordToComplete*" + + # Join the flag back if we have an equal sign flag + if ( $IsEqualFlag ) { + __%[1]s_debug "Join the equal sign flag back to the completion value" + $_.Name = $Flag + "=" + $_.Name + } + } + + # we sort the values in ascending order by name if keep order isn't passed + if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { + $Values = $Values | Sort-Object -Property Name + } + + if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { + __%[1]s_debug "ShellCompDirectiveNoFileComp is called" + + if ($Values.Length -eq 0) { + # Just print an empty string here so the + # shell does not start to complete paths. + # We cannot use CompletionResult here because + # it does not accept an empty string as argument. + "" + return + } + } + + # Get the current mode + $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function + __%[1]s_debug "Mode: $Mode" + + $Values | ForEach-Object { + + # store temporary because switch will overwrite $_ + $comp = $_ + + # PowerShell supports three different completion modes + # - TabCompleteNext (default windows style - on each key press the next option is displayed) + # - Complete (works like bash) + # - MenuComplete (works like zsh) + # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function + + # CompletionResult Arguments: + # 1) CompletionText text to be used as the auto completion result + # 2) ListItemText text to be displayed in the suggestion list + # 3) ResultType type of completion result + # 4) ToolTip text for the tooltip with details about the object + + switch ($Mode) { + + # bash like + "Complete" { + + if ($Values.Length -eq 1) { + __%[1]s_debug "Only one completion left" + + # insert space after value + [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + + } else { + # Add the proper number of spaces to align the descriptions + while($comp.Name.Length -lt $Longest) { + $comp.Name = $comp.Name + " " + } + + # Check for empty description and only add parentheses if needed + if ($($comp.Description) -eq " " ) { + $Description = "" + } else { + $Description = " ($($comp.Description))" + } + + [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") + } + } + + # zsh like + "MenuComplete" { + # insert space after value + # MenuComplete will automatically show the ToolTip of + # the highlighted value at the bottom of the suggestions. + [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } + + # TabCompleteNext and in case we get something unknown + Default { + # Like MenuComplete but we don't want to add a space here because + # the user need to press space anyway to get the completion. + # Description will not be shown because that's not possible with TabCompleteNext + [System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } + } + + } +} + +Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock $__%[2]sCompleterBlock +`, name, nameForVar, compCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, activeHelpEnvVar(name))) +} + +func (c *Command) genPowerShellCompletion(w io.Writer, includeDesc bool) error { + buf := new(bytes.Buffer) + genPowerShellComp(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) + return err +} + +func (c *Command) genPowerShellCompletionFile(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.genPowerShellCompletion(outFile, includeDesc) +} + +// GenPowerShellCompletionFile generates powershell completion file without descriptions. +func (c *Command) GenPowerShellCompletionFile(filename string) error { + return c.genPowerShellCompletionFile(filename, false) +} + +// GenPowerShellCompletion generates powershell completion file without descriptions +// and writes it to the passed writer. +func (c *Command) GenPowerShellCompletion(w io.Writer) error { + return c.genPowerShellCompletion(w, false) +} + +// GenPowerShellCompletionFileWithDesc generates powershell completion file with descriptions. +func (c *Command) GenPowerShellCompletionFileWithDesc(filename string) error { + return c.genPowerShellCompletionFile(filename, true) +} + +// GenPowerShellCompletionWithDesc generates powershell completion file with descriptions +// and writes it to the passed writer. +func (c *Command) GenPowerShellCompletionWithDesc(w io.Writer) error { + return c.genPowerShellCompletion(w, true) +} diff --git a/vendor/github.com/spf13/cobra/powershell_completions.md b/vendor/github.com/spf13/cobra/powershell_completions.md new file mode 100644 index 0000000000..c449f1e5c0 --- /dev/null +++ b/vendor/github.com/spf13/cobra/powershell_completions.md @@ -0,0 +1,3 @@ +# Generating PowerShell Completions For Your Own cobra.Command + +Please refer to [Shell Completions](shell_completions.md#powershell-completions) for details. diff --git a/vendor/github.com/spf13/cobra/projects_using_cobra.md b/vendor/github.com/spf13/cobra/projects_using_cobra.md new file mode 100644 index 0000000000..8a291eb20e --- /dev/null +++ b/vendor/github.com/spf13/cobra/projects_using_cobra.md @@ -0,0 +1,64 @@ +## Projects using Cobra + +- [Allero](https://github.com/allero-io/allero) +- [Arewefastyet](https://benchmark.vitess.io) +- [Arduino CLI](https://github.com/arduino/arduino-cli) +- [Bleve](https://blevesearch.com/) +- [Cilium](https://cilium.io/) +- [CloudQuery](https://github.com/cloudquery/cloudquery) +- [CockroachDB](https://www.cockroachlabs.com/) +- [Constellation](https://github.com/edgelesssys/constellation) +- [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) +- [Datree](https://github.com/datreeio/datree) +- [Delve](https://github.com/derekparker/delve) +- [Docker (distribution)](https://github.com/docker/distribution) +- [Etcd](https://etcd.io/) +- [Gardener](https://github.com/gardener/gardenctl) +- [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl) +- [Git Bump](https://github.com/erdaltsksn/git-bump) +- [GitHub CLI](https://github.com/cli/cli) +- [GitHub Labeler](https://github.com/erdaltsksn/gh-label) +- [Golangci-lint](https://golangci-lint.run) +- [GopherJS](https://github.com/gopherjs/gopherjs) +- [GoReleaser](https://goreleaser.com) +- [Helm](https://helm.sh) +- [Hugo](https://gohugo.io) +- [Infracost](https://github.com/infracost/infracost) +- [Istio](https://istio.io) +- [Kool](https://github.com/kool-dev/kool) +- [Kubernetes](https://kubernetes.io/) +- [Kubescape](https://github.com/kubescape/kubescape) +- [KubeVirt](https://github.com/kubevirt/kubevirt) +- [Linkerd](https://linkerd.io/) +- [Mattermost-server](https://github.com/mattermost/mattermost-server) +- [Mercure](https://mercure.rocks/) +- [Meroxa CLI](https://github.com/meroxa/cli) +- [Metal Stack CLI](https://github.com/metal-stack/metalctl) +- [Moby (former Docker)](https://github.com/moby/moby) +- [Moldy](https://github.com/Moldy-Community/moldy) +- [Multi-gitter](https://github.com/lindell/multi-gitter) +- [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack) +- [nFPM](https://nfpm.goreleaser.com) +- [Okteto](https://github.com/okteto/okteto) +- [OpenShift](https://www.openshift.com/) +- [Ory Hydra](https://github.com/ory/hydra) +- [Ory Kratos](https://github.com/ory/kratos) +- [Pixie](https://github.com/pixie-io/pixie) +- [Polygon Edge](https://github.com/0xPolygon/polygon-edge) +- [Pouch](https://github.com/alibaba/pouch) +- [ProjectAtomic (enterprise)](https://www.projectatomic.io/) +- [Prototool](https://github.com/uber/prototool) +- [Pulumi](https://www.pulumi.com) +- [QRcp](https://github.com/claudiodangelis/qrcp) +- [Random](https://github.com/erdaltsksn/random) +- [Rclone](https://rclone.org/) +- [Scaleway CLI](https://github.com/scaleway/scaleway-cli) +- [Sia](https://github.com/SiaFoundation/siad) +- [Skaffold](https://skaffold.dev/) +- [Tendermint](https://github.com/tendermint/tendermint) +- [Twitch CLI](https://github.com/twitchdev/twitch-cli) +- [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli) +- [Vitess](https://vitess.io) +- VMware's [Tanzu Community Edition](https://github.com/vmware-tanzu/community-edition) & [Tanzu Framework](https://github.com/vmware-tanzu/tanzu-framework) +- [Werf](https://werf.io/) +- [ZITADEL](https://github.com/zitadel/zitadel) diff --git a/vendor/github.com/spf13/cobra/shell_completions.go b/vendor/github.com/spf13/cobra/shell_completions.go new file mode 100644 index 0000000000..b035742d39 --- /dev/null +++ b/vendor/github.com/spf13/cobra/shell_completions.go @@ -0,0 +1,98 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "github.com/spf13/pflag" +) + +// MarkFlagRequired instructs the various shell completion implementations to +// prioritize the named flag when performing completion, +// and causes your command to report an error if invoked without the flag. +func (c *Command) MarkFlagRequired(name string) error { + return MarkFlagRequired(c.Flags(), name) +} + +// MarkPersistentFlagRequired instructs the various shell completion implementations to +// prioritize the named persistent flag when performing completion, +// and causes your command to report an error if invoked without the flag. +func (c *Command) MarkPersistentFlagRequired(name string) error { + return MarkFlagRequired(c.PersistentFlags(), name) +} + +// MarkFlagRequired instructs the various shell completion implementations to +// prioritize the named flag when performing completion, +// and causes your command to report an error if invoked without the flag. +func MarkFlagRequired(flags *pflag.FlagSet, name string) error { + return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) +} + +// MarkFlagFilename instructs the various shell completion implementations to +// limit completions for the named flag to the specified file extensions. +func (c *Command) MarkFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(c.Flags(), name, extensions...) +} + +// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. +// The bash completion script will call the bash function f for the flag. +// +// This will only work for bash completion. +// It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows +// to register a Go function which will work across all shells. +func (c *Command) MarkFlagCustom(name string, f string) error { + return MarkFlagCustom(c.Flags(), name, f) +} + +// MarkPersistentFlagFilename instructs the various shell completion +// implementations to limit completions for the named persistent flag to the +// specified file extensions. +func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(c.PersistentFlags(), name, extensions...) +} + +// MarkFlagFilename instructs the various shell completion implementations to +// limit completions for the named flag to the specified file extensions. +func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { + return flags.SetAnnotation(name, BashCompFilenameExt, extensions) +} + +// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. +// The bash completion script will call the bash function f for the flag. +// +// This will only work for bash completion. +// It is recommended to instead use c.RegisterFlagCompletionFunc(...) which allows +// to register a Go function which will work across all shells. +func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { + return flags.SetAnnotation(name, BashCompCustom, []string{f}) +} + +// MarkFlagDirname instructs the various shell completion implementations to +// limit completions for the named flag to directory names. +func (c *Command) MarkFlagDirname(name string) error { + return MarkFlagDirname(c.Flags(), name) +} + +// MarkPersistentFlagDirname instructs the various shell completion +// implementations to limit completions for the named persistent flag to +// directory names. +func (c *Command) MarkPersistentFlagDirname(name string) error { + return MarkFlagDirname(c.PersistentFlags(), name) +} + +// MarkFlagDirname instructs the various shell completion implementations to +// limit completions for the named flag to directory names. +func MarkFlagDirname(flags *pflag.FlagSet, name string) error { + return flags.SetAnnotation(name, BashCompSubdirsInDir, []string{}) +} diff --git a/vendor/github.com/spf13/cobra/shell_completions.md b/vendor/github.com/spf13/cobra/shell_completions.md new file mode 100644 index 0000000000..065c0621d4 --- /dev/null +++ b/vendor/github.com/spf13/cobra/shell_completions.md @@ -0,0 +1,576 @@ +# Generating shell completions + +Cobra can generate shell completions for multiple shells. +The currently supported shells are: +- Bash +- Zsh +- fish +- PowerShell + +Cobra will automatically provide your program with a fully functional `completion` command, +similarly to how it provides the `help` command. + +## Creating your own completion command + +If you do not wish to use the default `completion` command, you can choose to +provide your own, which will take precedence over the default one. (This also provides +backwards-compatibility with programs that already have their own `completion` command.) + +If you are using the `cobra-cli` generator, +which can be found at [spf13/cobra-cli](https://github.com/spf13/cobra-cli), +you can create a completion command by running + +```bash +cobra-cli add completion +``` +and then modifying the generated `cmd/completion.go` file to look something like this +(writing the shell script to stdout allows the most flexible use): + +```go +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: fmt.Sprintf(`To load completions: + +Bash: + + $ source <(%[1]s completion bash) + + # To load completions for each session, execute once: + # Linux: + $ %[1]s completion bash > /etc/bash_completion.d/%[1]s + # macOS: + $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s + +Zsh: + + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" + + # You will need to start a new shell for this setup to take effect. + +fish: + + $ %[1]s completion fish | source + + # To load completions for each session, execute once: + $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish + +PowerShell: + + PS> %[1]s completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> %[1]s completion powershell > %[1]s.ps1 + # and source this file from your PowerShell profile. +`,cmd.Root().Name()), + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + }, +} +``` + +**Note:** The cobra generator may include messages printed to stdout, for example, if the config file is loaded; this will break the auto-completion script so must be removed. + +## Adapting the default completion command + +Cobra provides a few options for the default `completion` command. To configure such options you must set +the `CompletionOptions` field on the *root* command. + +To tell Cobra *not* to provide the default `completion` command: +``` +rootCmd.CompletionOptions.DisableDefaultCmd = true +``` + +To tell Cobra to mark the default `completion` command as *hidden*: +``` +rootCmd.CompletionOptions.HiddenDefaultCmd = true +``` + +To tell Cobra *not* to provide the user with the `--no-descriptions` flag to the completion sub-commands: +``` +rootCmd.CompletionOptions.DisableNoDescFlag = true +``` + +To tell Cobra to completely disable descriptions for completions: +``` +rootCmd.CompletionOptions.DisableDescriptions = true +``` + +# Customizing completions + +The generated completion scripts will automatically handle completing commands and flags. However, you can make your completions much more powerful by providing information to complete your program's nouns and flag values. + +## Completion of nouns + +### Static completion of nouns + +Cobra allows you to provide a pre-defined list of completion choices for your nouns using the `ValidArgs` field. +For example, if you want `kubectl get [tab][tab]` to show a list of valid "nouns" you have to set them. +Some simplified code from `kubectl get` looks like: + +```go +validArgs = []string{ "pod", "node", "service", "replicationcontroller" } + +cmd := &cobra.Command{ + Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)", + Short: "Display one or many resources", + Long: get_long, + Example: get_example, + Run: func(cmd *cobra.Command, args []string) { + cobra.CheckErr(RunGet(f, out, cmd, args)) + }, + ValidArgs: validArgs, +} +``` + +Notice we put the `ValidArgs` field on the `get` sub-command. Doing so will give results like: + +```bash +$ kubectl get [tab][tab] +node pod replicationcontroller service +``` + +#### Aliases for nouns + +If your nouns have aliases, you can define them alongside `ValidArgs` using `ArgAliases`: + +```go +argAliases = []string { "pods", "nodes", "services", "svc", "replicationcontrollers", "rc" } + +cmd := &cobra.Command{ + ... + ValidArgs: validArgs, + ArgAliases: argAliases +} +``` + +The aliases are shown to the user on tab completion only if no completions were found within sub-commands or `ValidArgs`. + +### Dynamic completion of nouns + +In some cases it is not possible to provide a list of completions in advance. Instead, the list of completions must be determined at execution-time. In a similar fashion as for static completions, you can use the `ValidArgsFunction` field to provide a Go function that Cobra will execute when it needs the list of completion choices for the nouns of a command. Note that either `ValidArgs` or `ValidArgsFunction` can be used for a single cobra command, but not both. +Simplified code from `helm status` looks like: + +```go +cmd := &cobra.Command{ + Use: "status RELEASE_NAME", + Short: "Display the status of the named release", + Long: status_long, + RunE: func(cmd *cobra.Command, args []string) { + RunGet(args[0]) + }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getReleasesFromCluster(toComplete), cobra.ShellCompDirectiveNoFileComp + }, +} +``` +Where `getReleasesFromCluster()` is a Go function that obtains the list of current Helm releases running on the Kubernetes cluster. +Notice we put the `ValidArgsFunction` on the `status` sub-command. Let's assume the Helm releases on the cluster are: `harbor`, `notary`, `rook` and `thanos` then this dynamic completion will give results like: + +```bash +$ helm status [tab][tab] +harbor notary rook thanos +``` +You may have noticed the use of `cobra.ShellCompDirective`. These directives are bit fields allowing to control some shell completion behaviors for your particular completion. You can combine them with the bit-or operator such as `cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp` +```go +// Indicates that the shell will perform its default behavior after completions +// have been provided (this implies none of the other directives). +ShellCompDirectiveDefault + +// Indicates an error occurred and completions should be ignored. +ShellCompDirectiveError + +// Indicates that the shell should not add a space after the completion, +// even if there is a single completion provided. +ShellCompDirectiveNoSpace + +// Indicates that the shell should not provide file completion even when +// no completion is provided. +ShellCompDirectiveNoFileComp + +// Indicates that the returned completions should be used as file extension filters. +// For example, to complete only files of the form *.json or *.yaml: +// return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt +// For flags, using MarkFlagFilename() and MarkPersistentFlagFilename() +// is a shortcut to using this directive explicitly. +// +ShellCompDirectiveFilterFileExt + +// Indicates that only directory names should be provided in file completion. +// For example: +// return nil, ShellCompDirectiveFilterDirs +// For flags, using MarkFlagDirname() is a shortcut to using this directive explicitly. +// +// To request directory names within another directory, the returned completions +// should specify a single directory name within which to search. For example, +// to complete directories within "themes/": +// return []string{"themes"}, ShellCompDirectiveFilterDirs +// +ShellCompDirectiveFilterDirs + +// ShellCompDirectiveKeepOrder indicates that the shell should preserve the order +// in which the completions are provided +ShellCompDirectiveKeepOrder +``` + +***Note***: When using the `ValidArgsFunction`, Cobra will call your registered function after having parsed all flags and arguments provided in the command-line. You therefore don't need to do this parsing yourself. For example, when a user calls `helm status --namespace my-rook-ns [tab][tab]`, Cobra will call your registered `ValidArgsFunction` after having parsed the `--namespace` flag, as it would have done when calling the `RunE` function. + +#### Debugging + +Cobra achieves dynamic completion through the use of a hidden command called by the completion script. To debug your Go completion code, you can call this hidden command directly: +```bash +$ helm __complete status har +harbor +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp # This is on stderr +``` +***Important:*** If the noun to complete is empty (when the user has not yet typed any letters of that noun), you must pass an empty parameter to the `__complete` command: +```bash +$ helm __complete status "" +harbor +notary +rook +thanos +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp # This is on stderr +``` +Calling the `__complete` command directly allows you to run the Go debugger to troubleshoot your code. You can also add printouts to your code; Cobra provides the following functions to use for printouts in Go completion code: +```go +// Prints to the completion script debug file (if BASH_COMP_DEBUG_FILE +// is set to a file path) and optionally prints to stderr. +cobra.CompDebug(msg string, printToStdErr bool) { +cobra.CompDebugln(msg string, printToStdErr bool) + +// Prints to the completion script debug file (if BASH_COMP_DEBUG_FILE +// is set to a file path) and to stderr. +cobra.CompError(msg string) +cobra.CompErrorln(msg string) +``` +***Important:*** You should **not** leave traces that print directly to stdout in your completion code as they will be interpreted as completion choices by the completion script. Instead, use the cobra-provided debugging traces functions mentioned above. + +## Completions for flags + +### Mark flags as required + +Most of the time completions will only show sub-commands. But if a flag is required to make a sub-command work, you probably want it to show up when the user types [tab][tab]. You can mark a flag as 'Required' like so: + +```go +cmd.MarkFlagRequired("pod") +cmd.MarkFlagRequired("container") +``` + +and you'll get something like + +```bash +$ kubectl exec [tab][tab] +-c --container= -p --pod= +``` + +### Specify dynamic flag completion + +As for nouns, Cobra provides a way of defining dynamic completion of flags. To provide a Go function that Cobra will execute when it needs the list of completion choices for a flag, you must register the function using the `command.RegisterFlagCompletionFunc()` function. + +```go +flagName := "output" +cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"json", "table", "yaml"}, cobra.ShellCompDirectiveDefault +}) +``` +Notice that calling `RegisterFlagCompletionFunc()` is done through the `command` with which the flag is associated. In our example this dynamic completion will give results like so: + +```bash +$ helm status --output [tab][tab] +json table yaml +``` + +#### Debugging + +You can also easily debug your Go completion code for flags: +```bash +$ helm __complete status --output "" +json +table +yaml +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp # This is on stderr +``` +***Important:*** You should **not** leave traces that print to stdout in your completion code as they will be interpreted as completion choices by the completion script. Instead, use the cobra-provided debugging traces functions mentioned further above. + +### Specify valid filename extensions for flags that take a filename + +To limit completions of flag values to file names with certain extensions you can either use the different `MarkFlagFilename()` functions or a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterFileExt`, like so: +```go +flagName := "output" +cmd.MarkFlagFilename(flagName, "yaml", "json") +``` +or +```go +flagName := "output" +cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"yaml", "json"}, ShellCompDirectiveFilterFileExt}) +``` + +### Limit flag completions to directory names + +To limit completions of flag values to directory names you can either use the `MarkFlagDirname()` functions or a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterDirs`, like so: +```go +flagName := "output" +cmd.MarkFlagDirname(flagName) +``` +or +```go +flagName := "output" +cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveFilterDirs +}) +``` +To limit completions of flag values to directory names *within another directory* you can use a combination of `RegisterFlagCompletionFunc()` and `ShellCompDirectiveFilterDirs` like so: +```go +flagName := "output" +cmd.RegisterFlagCompletionFunc(flagName, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"themes"}, cobra.ShellCompDirectiveFilterDirs +}) +``` +### Descriptions for completions + +Cobra provides support for completion descriptions. Such descriptions are supported for each shell +(however, for bash, it is only available in the [completion V2 version](#bash-completion-v2)). +For commands and flags, Cobra will provide the descriptions automatically, based on usage information. +For example, using zsh: +``` +$ helm s[tab] +search -- search for a keyword in charts +show -- show information of a chart +status -- displays the status of the named release +``` +while using fish: +``` +$ helm s[tab] +search (search for a keyword in charts) show (show information of a chart) status (displays the status of the named release) +``` + +Cobra allows you to add descriptions to your own completions. Simply add the description text after each completion, following a `\t` separator. This technique applies to completions returned by `ValidArgs`, `ValidArgsFunction` and `RegisterFlagCompletionFunc()`. For example: +```go +ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"harbor\tAn image registry", "thanos\tLong-term metrics"}, cobra.ShellCompDirectiveNoFileComp +}} +``` +or +```go +ValidArgs: []string{"bash\tCompletions for bash", "zsh\tCompletions for zsh"} +``` + +If you don't want to show descriptions in the completions, you can add `--no-descriptions` to the default `completion` command to disable them, like: + +```bash +$ source <(helm completion bash) +$ helm completion [tab][tab] +bash (generate autocompletion script for bash) powershell (generate autocompletion script for powershell) +fish (generate autocompletion script for fish) zsh (generate autocompletion script for zsh) + +$ source <(helm completion bash --no-descriptions) +$ helm completion [tab][tab] +bash fish powershell zsh +``` +## Bash completions + +### Dependencies + +The bash completion script generated by Cobra requires the `bash_completion` package. You should update the help text of your completion command to show how to install the `bash_completion` package ([Kubectl docs](https://kubernetes.io/docs/tasks/tools/install-kubectl/#enabling-shell-autocompletion)) + +### Aliases + +You can also configure `bash` aliases for your program and they will also support completions. + +```bash +alias aliasname=origcommand +complete -o default -F __start_origcommand aliasname + +# and now when you run `aliasname` completion will make +# suggestions as it did for `origcommand`. + +$ aliasname +completion firstcommand secondcommand +``` +### Bash legacy dynamic completions + +For backward compatibility, Cobra still supports its bash legacy dynamic completion solution. +Please refer to [Bash Completions](bash_completions.md) for details. + +### Bash completion V2 + +Cobra provides two versions for bash completion. The original bash completion (which started it all!) can be used by calling +`GenBashCompletion()` or `GenBashCompletionFile()`. + +A new V2 bash completion version is also available. This version can be used by calling `GenBashCompletionV2()` or +`GenBashCompletionFileV2()`. The V2 version does **not** support the legacy dynamic completion +(see [Bash Completions](bash_completions.md)) but instead works only with the Go dynamic completion +solution described in this document. +Unless your program already uses the legacy dynamic completion solution, it is recommended that you use the bash +completion V2 solution which provides the following extra features: +- Supports completion descriptions (like the other shells) +- Small completion script of less than 300 lines (v1 generates scripts of thousands of lines; `kubectl` for example has a bash v1 completion script of over 13K lines) +- Streamlined user experience thanks to a completion behavior aligned with the other shells + +`Bash` completion V2 supports descriptions for completions. When calling `GenBashCompletionV2()` or `GenBashCompletionFileV2()` +you must provide these functions with a parameter indicating if the completions should be annotated with a description; Cobra +will provide the description automatically based on usage information. You can choose to make this option configurable by +your users. + +``` +# With descriptions +$ helm s[tab][tab] +search (search for a keyword in charts) status (display the status of the named release) +show (show information of a chart) + +# Without descriptions +$ helm s[tab][tab] +search show status +``` +**Note**: Cobra's default `completion` command uses bash completion V2. If for some reason you need to use bash completion V1, you will need to implement your own `completion` command. +## Zsh completions + +Cobra supports native zsh completion generated from the root `cobra.Command`. +The generated completion script should be put somewhere in your `$fpath` and be named +`_`. You will need to start a new shell for the completions to become available. + +Zsh supports descriptions for completions. Cobra will provide the description automatically, +based on usage information. Cobra provides a way to completely disable such descriptions by +using `GenZshCompletionNoDesc()` or `GenZshCompletionFileNoDesc()`. You can choose to make +this a configurable option to your users. +``` +# With descriptions +$ helm s[tab] +search -- search for a keyword in charts +show -- show information of a chart +status -- displays the status of the named release + +# Without descriptions +$ helm s[tab] +search show status +``` +*Note*: Because of backward-compatibility requirements, we were forced to have a different API to disable completion descriptions between `zsh` and `fish`. + +### Limitations + +* Custom completions implemented in Bash scripting (legacy) are not supported and will be ignored for `zsh` (including the use of the `BashCompCustom` flag annotation). + * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). +* The function `MarkFlagCustom()` is not supported and will be ignored for `zsh`. + * You should instead use `RegisterFlagCompletionFunc()`. + +### Zsh completions standardization + +Cobra 1.1 standardized its zsh completion support to align it with its other shell completions. Although the API was kept backward-compatible, some small changes in behavior were introduced. +Please refer to [Zsh Completions](zsh_completions.md) for details. + +## fish completions + +Cobra supports native fish completions generated from the root `cobra.Command`. You can use the `command.GenFishCompletion()` or `command.GenFishCompletionFile()` functions. You must provide these functions with a parameter indicating if the completions should be annotated with a description; Cobra will provide the description automatically based on usage information. You can choose to make this option configurable by your users. +``` +# With descriptions +$ helm s[tab] +search (search for a keyword in charts) show (show information of a chart) status (displays the status of the named release) + +# Without descriptions +$ helm s[tab] +search show status +``` +*Note*: Because of backward-compatibility requirements, we were forced to have a different API to disable completion descriptions between `zsh` and `fish`. + +### Limitations + +* Custom completions implemented in bash scripting (legacy) are not supported and will be ignored for `fish` (including the use of the `BashCompCustom` flag annotation). + * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). +* The function `MarkFlagCustom()` is not supported and will be ignored for `fish`. + * You should instead use `RegisterFlagCompletionFunc()`. +* The following flag completion annotations are not supported and will be ignored for `fish`: + * `BashCompFilenameExt` (filtering by file extension) + * `BashCompSubdirsInDir` (filtering by directory) +* The functions corresponding to the above annotations are consequently not supported and will be ignored for `fish`: + * `MarkFlagFilename()` and `MarkPersistentFlagFilename()` (filtering by file extension) + * `MarkFlagDirname()` and `MarkPersistentFlagDirname()` (filtering by directory) +* Similarly, the following completion directives are not supported and will be ignored for `fish`: + * `ShellCompDirectiveFilterFileExt` (filtering by file extension) + * `ShellCompDirectiveFilterDirs` (filtering by directory) + +## PowerShell completions + +Cobra supports native PowerShell completions generated from the root `cobra.Command`. You can use the `command.GenPowerShellCompletion()` or `command.GenPowerShellCompletionFile()` functions. To include descriptions use `command.GenPowerShellCompletionWithDesc()` and `command.GenPowerShellCompletionFileWithDesc()`. Cobra will provide the description automatically based on usage information. You can choose to make this option configurable by your users. + +The script is designed to support all three PowerShell completion modes: + +* TabCompleteNext (default windows style - on each key press the next option is displayed) +* Complete (works like bash) +* MenuComplete (works like zsh) + +You set the mode with `Set-PSReadLineKeyHandler -Key Tab -Function `. Descriptions are only displayed when using the `Complete` or `MenuComplete` mode. + +Users need PowerShell version 5.0 or above, which comes with Windows 10 and can be downloaded separately for Windows 7 or 8.1. They can then write the completions to a file and source this file from their PowerShell profile, which is referenced by the `$Profile` environment variable. See `Get-Help about_Profiles` for more info about PowerShell profiles. + +``` +# With descriptions and Mode 'Complete' +$ helm s[tab] +search (search for a keyword in charts) show (show information of a chart) status (displays the status of the named release) + +# With descriptions and Mode 'MenuComplete' The description of the current selected value will be displayed below the suggestions. +$ helm s[tab] +search show status + +search for a keyword in charts + +# Without descriptions +$ helm s[tab] +search show status +``` +### Aliases + +You can also configure `powershell` aliases for your program and they will also support completions. + +``` +$ sal aliasname origcommand +$ Register-ArgumentCompleter -CommandName 'aliasname' -ScriptBlock $__origcommandCompleterBlock + +# and now when you run `aliasname` completion will make +# suggestions as it did for `origcommand`. + +$ aliasname +completion firstcommand secondcommand +``` +The name of the completer block variable is of the form `$__CompleterBlock` where every `-` and `:` in the program name have been replaced with `_`, to respect powershell naming syntax. + +### Limitations + +* Custom completions implemented in bash scripting (legacy) are not supported and will be ignored for `powershell` (including the use of the `BashCompCustom` flag annotation). + * You should instead use `ValidArgsFunction` and `RegisterFlagCompletionFunc()` which are portable to the different shells (`bash`, `zsh`, `fish`, `powershell`). +* The function `MarkFlagCustom()` is not supported and will be ignored for `powershell`. + * You should instead use `RegisterFlagCompletionFunc()`. +* The following flag completion annotations are not supported and will be ignored for `powershell`: + * `BashCompFilenameExt` (filtering by file extension) + * `BashCompSubdirsInDir` (filtering by directory) +* The functions corresponding to the above annotations are consequently not supported and will be ignored for `powershell`: + * `MarkFlagFilename()` and `MarkPersistentFlagFilename()` (filtering by file extension) + * `MarkFlagDirname()` and `MarkPersistentFlagDirname()` (filtering by directory) +* Similarly, the following completion directives are not supported and will be ignored for `powershell`: + * `ShellCompDirectiveFilterFileExt` (filtering by file extension) + * `ShellCompDirectiveFilterDirs` (filtering by directory) diff --git a/vendor/github.com/spf13/cobra/user_guide.md b/vendor/github.com/spf13/cobra/user_guide.md new file mode 100644 index 0000000000..85201d840c --- /dev/null +++ b/vendor/github.com/spf13/cobra/user_guide.md @@ -0,0 +1,726 @@ +# User Guide + +While you are welcome to provide your own organization, typically a Cobra-based +application will follow the following organizational structure: + +``` + ▾ appName/ + ▾ cmd/ + add.go + your.go + commands.go + here.go + main.go +``` + +In a Cobra app, typically the main.go file is very bare. It serves one purpose: initializing Cobra. + +```go +package main + +import ( + "{pathToYourApp}/cmd" +) + +func main() { + cmd.Execute() +} +``` + +## Using the Cobra Generator + +Cobra-CLI is its own program that will create your application and add any +commands you want. It's the easiest way to incorporate Cobra into your application. + +For complete details on using the Cobra generator, please refer to [The Cobra-CLI Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md) + +## Using the Cobra Library + +To manually implement Cobra you need to create a bare main.go file and a rootCmd file. +You will optionally provide additional commands as you see fit. + +### Create rootCmd + +Cobra doesn't require any special constructors. Simply create your commands. + +Ideally you place this in app/cmd/root.go: + +```go +var rootCmd = &cobra.Command{ + Use: "hugo", + Short: "Hugo is a very fast static site generator", + Long: `A Fast and Flexible Static Site Generator built with + love by spf13 and friends in Go. + Complete documentation is available at https://gohugo.io/documentation/`, + Run: func(cmd *cobra.Command, args []string) { + // Do Stuff Here + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +``` + +You will additionally define flags and handle configuration in your init() function. + +For example cmd/root.go: + +```go +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + // Used for flags. + cfgFile string + userLicense string + + rootCmd = &cobra.Command{ + Use: "cobra-cli", + Short: "A generator for Cobra based Applications", + Long: `Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + } +) + +// Execute executes the root command. +func Execute() error { + return rootCmd.Execute() +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)") + rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution") + rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project") + rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") + viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author")) + viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper")) + viper.SetDefault("author", "NAME HERE ") + viper.SetDefault("license", "apache") + + rootCmd.AddCommand(addCmd) + rootCmd.AddCommand(initCmd) +} + +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".cobra" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".cobra") + } + + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} +``` + +### Create your main.go + +With the root command you need to have your main function execute it. +Execute should be run on the root for clarity, though it can be called on any command. + +In a Cobra app, typically the main.go file is very bare. It serves one purpose: to initialize Cobra. + +```go +package main + +import ( + "{pathToYourApp}/cmd" +) + +func main() { + cmd.Execute() +} +``` + +### Create additional commands + +Additional commands can be defined and typically are each given their own file +inside of the cmd/ directory. + +If you wanted to create a version command you would create cmd/version.go and +populate it with the following: + +```go +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(versionCmd) +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Hugo", + Long: `All software has versions. This is Hugo's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Hugo Static Site Generator v0.9 -- HEAD") + }, +} +``` + +### Organizing subcommands + +A command may have subcommands which in turn may have other subcommands. This is achieved by using +`AddCommand`. In some cases, especially in larger applications, each subcommand may be defined in +its own go package. + +The suggested approach is for the parent command to use `AddCommand` to add its most immediate +subcommands. For example, consider the following directory structure: + +```text +├── cmd +│   ├── root.go +│   └── sub1 +│   ├── sub1.go +│   └── sub2 +│   ├── leafA.go +│   ├── leafB.go +│   └── sub2.go +└── main.go +``` + +In this case: + +* The `init` function of `root.go` adds the command defined in `sub1.go` to the root command. +* The `init` function of `sub1.go` adds the command defined in `sub2.go` to the sub1 command. +* The `init` function of `sub2.go` adds the commands defined in `leafA.go` and `leafB.go` to the + sub2 command. + +This approach ensures the subcommands are always included at compile time while avoiding cyclic +references. + +### Returning and handling errors + +If you wish to return an error to the caller of a command, `RunE` can be used. + +```go +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(tryCmd) +} + +var tryCmd = &cobra.Command{ + Use: "try", + Short: "Try and possibly fail at something", + RunE: func(cmd *cobra.Command, args []string) error { + if err := someFunc(); err != nil { + return err + } + return nil + }, +} +``` + +The error can then be caught at the execute function call. + +## Working with Flags + +Flags provide modifiers to control how the action command operates. + +### Assign flags to a command + +Since the flags are defined and used in different locations, we need to +define a variable outside with the correct scope to assign the flag to +work with. + +```go +var Verbose bool +var Source string +``` + +There are two different approaches to assign a flag. + +### Persistent Flags + +A flag can be 'persistent', meaning that this flag will be available to the +command it's assigned to as well as every command under that command. For +global flags, assign a flag as a persistent flag on the root. + +```go +rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") +``` + +### Local Flags + +A flag can also be assigned locally, which will only apply to that specific command. + +```go +localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from") +``` + +### Local Flag on Parent Commands + +By default, Cobra only parses local flags on the target command, and any local flags on +parent commands are ignored. By enabling `Command.TraverseChildren`, Cobra will +parse local flags on each command before executing the target command. + +```go +command := cobra.Command{ + Use: "print [OPTIONS] [COMMANDS]", + TraverseChildren: true, +} +``` + +### Bind Flags with Config + +You can also bind your flags with [viper](https://github.com/spf13/viper): +```go +var author string + +func init() { + rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution") + viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author")) +} +``` + +In this example, the persistent flag `author` is bound with `viper`. +**Note**: the variable `author` will not be set to the value from config, +when the `--author` flag is provided by user. + +More in [viper documentation](https://github.com/spf13/viper#working-with-flags). + +### Required flags + +Flags are optional by default. If instead you wish your command to report an error +when a flag has not been set, mark it as required: +```go +rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)") +rootCmd.MarkFlagRequired("region") +``` + +Or, for persistent flags: +```go +rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)") +rootCmd.MarkPersistentFlagRequired("region") +``` + +### Flag Groups + +If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then +Cobra can enforce that requirement: +```go +rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)") +rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)") +rootCmd.MarkFlagsRequiredTogether("username", "password") +``` + +You can also prevent different flags from being provided together if they represent mutually +exclusive options such as specifying an output format as either `--json` or `--yaml` but never both: +```go +rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") +rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") +rootCmd.MarkFlagsMutuallyExclusive("json", "yaml") +``` + +In both of these cases: + - both local and persistent flags can be used + - **NOTE:** the group is only enforced on commands where every flag is defined + - a flag may appear in multiple groups + - a group may contain any number of flags + +## Positional and Custom Arguments + +Validation of positional arguments can be specified using the `Args` field of `Command`. +The following validators are built in: + +- Number of arguments: + - `NoArgs` - report an error if there are any positional args. + - `ArbitraryArgs` - accept any number of args. + - `MinimumNArgs(int)` - report an error if less than N positional args are provided. + - `MaximumNArgs(int)` - report an error if more than N positional args are provided. + - `ExactArgs(int)` - report an error if there are not exactly N positional args. + - `RangeArgs(min, max)` - report an error if the number of args is not between `min` and `max`. +- Content of the arguments: + - `OnlyValidArgs` - report an error if there are any positional args not specified in the `ValidArgs` field of `Command`, which can optionally be set to a list of valid values for positional args. + +If `Args` is undefined or `nil`, it defaults to `ArbitraryArgs`. + +Moreover, `MatchAll(pargs ...PositionalArgs)` enables combining existing checks with arbitrary other checks. +For instance, if you want to report an error if there are not exactly N positional args OR if there are any positional +args that are not in the `ValidArgs` field of `Command`, you can call `MatchAll` on `ExactArgs` and `OnlyValidArgs`, as +shown below: + +```go +var cmd = &cobra.Command{ + Short: "hello", + Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Hello, World!") + }, +} +``` + +It is possible to set any custom validator that satisfies `func(cmd *cobra.Command, args []string) error`. +For example: + +```go +var cmd = &cobra.Command{ + Short: "hello", + Args: func(cmd *cobra.Command, args []string) error { + // Optionally run one of the validators provided by cobra + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return err + } + // Run the custom validation logic + if myapp.IsValidColor(args[0]) { + return nil + } + return fmt.Errorf("invalid color specified: %s", args[0]) + }, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Hello, World!") + }, +} +``` + +## Example + +In the example below, we have defined three commands. Two are at the top level +and one (cmdTimes) is a child of one of the top commands. In this case the root +is not executable, meaning that a subcommand is required. This is accomplished +by not providing a 'Run' for the 'rootCmd'. + +We have only defined one flag for a single command. + +More documentation about flags is available at https://github.com/spf13/pflag + +```go +package main + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +func main() { + var echoTimes int + + var cmdPrint = &cobra.Command{ + Use: "print [string to print]", + Short: "Print anything to the screen", + Long: `print is for printing anything back to the screen. +For many years people have printed back to the screen.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Print: " + strings.Join(args, " ")) + }, + } + + var cmdEcho = &cobra.Command{ + Use: "echo [string to echo]", + Short: "Echo anything to the screen", + Long: `echo is for echoing anything back. +Echo works a lot like print, except it has a child command.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Echo: " + strings.Join(args, " ")) + }, + } + + var cmdTimes = &cobra.Command{ + Use: "times [string to echo]", + Short: "Echo anything to the screen more times", + Long: `echo things multiple times back to the user by providing +a count and a string.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + for i := 0; i < echoTimes; i++ { + fmt.Println("Echo: " + strings.Join(args, " ")) + } + }, + } + + cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input") + + var rootCmd = &cobra.Command{Use: "app"} + rootCmd.AddCommand(cmdPrint, cmdEcho) + cmdEcho.AddCommand(cmdTimes) + rootCmd.Execute() +} +``` + +For a more complete example of a larger application, please checkout [Hugo](https://gohugo.io/). + +## Help Command + +Cobra automatically adds a help command to your application when you have subcommands. +This will be called when a user runs 'app help'. Additionally, help will also +support all other commands as input. Say, for instance, you have a command called +'create' without any additional configuration; Cobra will work when 'app help +create' is called. Every command will automatically have the '--help' flag added. + +### Example + +The following output is automatically generated by Cobra. Nothing beyond the +command and flag definitions are needed. + + $ cobra-cli help + + Cobra is a CLI library for Go that empowers applications. + This application is a tool to generate the needed files + to quickly create a Cobra application. + + Usage: + cobra-cli [command] + + Available Commands: + add Add a command to a Cobra Application + completion Generate the autocompletion script for the specified shell + help Help about any command + init Initialize a Cobra Application + + Flags: + -a, --author string author name for copyright attribution (default "YOUR NAME") + --config string config file (default is $HOME/.cobra.yaml) + -h, --help help for cobra-cli + -l, --license string name of license for the project + --viper use Viper for configuration + + Use "cobra-cli [command] --help" for more information about a command. + + +Help is just a command like any other. There is no special logic or behavior +around it. In fact, you can provide your own if you want. + +### Grouping commands in help + +Cobra supports grouping of available commands in the help output. To group commands, each group must be explicitly +defined using `AddGroup()` on the parent command. Then a subcommand can be added to a group using the `GroupID` element +of that subcommand. The groups will appear in the help output in the same order as they are defined using different +calls to `AddGroup()`. If you use the generated `help` or `completion` commands, you can set their group ids using +`SetHelpCommandGroupId()` and `SetCompletionCommandGroupId()` on the root command, respectively. + +### Defining your own help + +You can provide your own Help command or your own template for the default command to use +with the following functions: + +```go +cmd.SetHelpCommand(cmd *Command) +cmd.SetHelpFunc(f func(*Command, []string)) +cmd.SetHelpTemplate(s string) +``` + +The latter two will also apply to any children commands. + +## Usage Message + +When the user provides an invalid flag or invalid command, Cobra responds by +showing the user the 'usage'. + +### Example +You may recognize this from the help above. That's because the default help +embeds the usage as part of its output. + + $ cobra-cli --invalid + Error: unknown flag: --invalid + Usage: + cobra-cli [command] + + Available Commands: + add Add a command to a Cobra Application + completion Generate the autocompletion script for the specified shell + help Help about any command + init Initialize a Cobra Application + + Flags: + -a, --author string author name for copyright attribution (default "YOUR NAME") + --config string config file (default is $HOME/.cobra.yaml) + -h, --help help for cobra-cli + -l, --license string name of license for the project + --viper use Viper for configuration + + Use "cobra [command] --help" for more information about a command. + +### Defining your own usage +You can provide your own usage function or template for Cobra to use. +Like help, the function and template are overridable through public methods: + +```go +cmd.SetUsageFunc(f func(*Command) error) +cmd.SetUsageTemplate(s string) +``` + +## Version Flag + +Cobra adds a top-level '--version' flag if the Version field is set on the root command. +Running an application with the '--version' flag will print the version to stdout using +the version template. The template can be customized using the +`cmd.SetVersionTemplate(s string)` function. + +## PreRun and PostRun Hooks + +It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. These functions are run in the following order: + +- `PersistentPreRun` +- `PreRun` +- `Run` +- `PostRun` +- `PersistentPostRun` + +An example of two commands which use all of these features is below. When the subcommand is executed, it will run the root command's `PersistentPreRun` but not the root command's `PersistentPostRun`: + +```go +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func main() { + + var rootCmd = &cobra.Command{ + Use: "root [sub]", + Short: "My root command", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args) + }, + PreRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd PreRun with args: %v\n", args) + }, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd Run with args: %v\n", args) + }, + PostRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd PostRun with args: %v\n", args) + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args) + }, + } + + var subCmd = &cobra.Command{ + Use: "sub [no options!]", + Short: "My subcommand", + PreRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside subCmd PreRun with args: %v\n", args) + }, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside subCmd Run with args: %v\n", args) + }, + PostRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside subCmd PostRun with args: %v\n", args) + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args) + }, + } + + rootCmd.AddCommand(subCmd) + + rootCmd.SetArgs([]string{""}) + rootCmd.Execute() + fmt.Println() + rootCmd.SetArgs([]string{"sub", "arg1", "arg2"}) + rootCmd.Execute() +} +``` + +Output: +``` +Inside rootCmd PersistentPreRun with args: [] +Inside rootCmd PreRun with args: [] +Inside rootCmd Run with args: [] +Inside rootCmd PostRun with args: [] +Inside rootCmd PersistentPostRun with args: [] + +Inside rootCmd PersistentPreRun with args: [arg1 arg2] +Inside subCmd PreRun with args: [arg1 arg2] +Inside subCmd Run with args: [arg1 arg2] +Inside subCmd PostRun with args: [arg1 arg2] +Inside subCmd PersistentPostRun with args: [arg1 arg2] +``` + +## Suggestions when "unknown command" happens + +Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behave similarly to the `git` command when a typo happens. For example: + +``` +$ hugo srever +Error: unknown command "srever" for "hugo" + +Did you mean this? + server + +Run 'hugo --help' for usage. +``` + +Suggestions are automatically generated based on existing subcommands and use an implementation of [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance). Every registered command that matches a minimum distance of 2 (ignoring case) will be displayed as a suggestion. + +If you need to disable suggestions or tweak the string distance in your command, use: + +```go +command.DisableSuggestions = true +``` + +or + +```go +command.SuggestionsMinimumDistance = 1 +``` + +You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which +you don't want aliases. Example: + +``` +$ kubectl remove +Error: unknown command "remove" for "kubectl" + +Did you mean this? + delete + +Run 'kubectl help' for usage. +``` + +## Generating documentation for your command + +Cobra can generate documentation based on subcommands, flags, etc. Read more about it in the [docs generation documentation](doc/README.md). + +## Generating shell completions + +Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md). + +## Providing Active Help + +Cobra makes use of the shell-completion system to define a framework allowing you to provide Active Help to your users. Active Help are messages (hints, warnings, etc) printed as the program is being used. Read more about it in [Active Help](active_help.md). diff --git a/vendor/github.com/spf13/cobra/zsh_completions.go b/vendor/github.com/spf13/cobra/zsh_completions.go new file mode 100644 index 0000000000..1856e4c7f6 --- /dev/null +++ b/vendor/github.com/spf13/cobra/zsh_completions.go @@ -0,0 +1,308 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// GenZshCompletionFile generates zsh completion file including descriptions. +func (c *Command) GenZshCompletionFile(filename string) error { + return c.genZshCompletionFile(filename, true) +} + +// GenZshCompletion generates zsh completion file including descriptions +// and writes it to the passed writer. +func (c *Command) GenZshCompletion(w io.Writer) error { + return c.genZshCompletion(w, true) +} + +// GenZshCompletionFileNoDesc generates zsh completion file without descriptions. +func (c *Command) GenZshCompletionFileNoDesc(filename string) error { + return c.genZshCompletionFile(filename, false) +} + +// GenZshCompletionNoDesc generates zsh completion file without descriptions +// and writes it to the passed writer. +func (c *Command) GenZshCompletionNoDesc(w io.Writer) error { + return c.genZshCompletion(w, false) +} + +// MarkZshCompPositionalArgumentFile only worked for zsh and its behavior was +// not consistent with Bash completion. It has therefore been disabled. +// Instead, when no other completion is specified, file completion is done by +// default for every argument. One can disable file completion on a per-argument +// basis by using ValidArgsFunction and ShellCompDirectiveNoFileComp. +// To achieve file extension filtering, one can use ValidArgsFunction and +// ShellCompDirectiveFilterFileExt. +// +// Deprecated +func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error { + return nil +} + +// MarkZshCompPositionalArgumentWords only worked for zsh. It has therefore +// been disabled. +// To achieve the same behavior across all shells, one can use +// ValidArgs (for the first argument only) or ValidArgsFunction for +// any argument (can include the first one also). +// +// Deprecated +func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error { + return nil +} + +func (c *Command) genZshCompletionFile(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.genZshCompletion(outFile, includeDesc) +} + +func (c *Command) genZshCompletion(w io.Writer, includeDesc bool) error { + buf := new(bytes.Buffer) + genZshComp(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) + return err +} + +func genZshComp(buf io.StringWriter, name string, includeDesc bool) { + compCmd := ShellCompRequestCmd + if !includeDesc { + compCmd = ShellCompNoDescRequestCmd + } + WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s +compdef _%[1]s %[1]s + +# zsh completion for %-36[1]s -*- shell-script -*- + +__%[1]s_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_%[1]s() +{ + local shellCompDirectiveError=%[3]d + local shellCompDirectiveNoSpace=%[4]d + local shellCompDirectiveNoFileComp=%[5]d + local shellCompDirectiveFilterFileExt=%[6]d + local shellCompDirectiveFilterDirs=%[7]d + local shellCompDirectiveKeepOrder=%[8]d + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __%[1]s_debug "\n========= starting completion logic ==========" + __%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __%[1]s_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., %[1]s -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} %[2]s ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __%[1]s_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __%[1]s_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __%[1]s_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%%s\n" "${out[@]}") + __%[1]s_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __%[1]s_debug "No directive found. Setting do default" + directive=0 + fi + + __%[1]s_debug "directive: ${directive}" + __%[1]s_debug "completions: ${out}" + __%[1]s_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __%[1]s_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="%[9]s" + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __%[1]s_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __%[1]s_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __%[1]s_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __%[1]s_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __%[1]s_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __%[1]s_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __%[1]s_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __%[1]s_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __%[1]s_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __%[1]s_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __%[1]s_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __%[1]s_debug "_describe did not find completions." + __%[1]s_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __%[1]s_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __%[1]s_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_%[1]s" ]; then + _%[1]s +fi +`, name, compCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, + activeHelpMarker)) +} diff --git a/vendor/github.com/spf13/cobra/zsh_completions.md b/vendor/github.com/spf13/cobra/zsh_completions.md new file mode 100644 index 0000000000..7cff61787f --- /dev/null +++ b/vendor/github.com/spf13/cobra/zsh_completions.md @@ -0,0 +1,48 @@ +## Generating Zsh Completion For Your cobra.Command + +Please refer to [Shell Completions](shell_completions.md) for details. + +## Zsh completions standardization + +Cobra 1.1 standardized its zsh completion support to align it with its other shell completions. Although the API was kept backwards-compatible, some small changes in behavior were introduced. + +### Deprecation summary + +See further below for more details on these deprecations. + +* `cmd.MarkZshCompPositionalArgumentFile(pos, []string{})` is no longer needed. It is therefore **deprecated** and silently ignored. +* `cmd.MarkZshCompPositionalArgumentFile(pos, glob[])` is **deprecated** and silently ignored. + * Instead use `ValidArgsFunction` with `ShellCompDirectiveFilterFileExt`. +* `cmd.MarkZshCompPositionalArgumentWords()` is **deprecated** and silently ignored. + * Instead use `ValidArgsFunction`. + +### Behavioral changes + +**Noun completion** +|Old behavior|New behavior| +|---|---| +|No file completion by default (opposite of bash)|File completion by default; use `ValidArgsFunction` with `ShellCompDirectiveNoFileComp` to turn off file completion on a per-argument basis| +|Completion of flag names without the `-` prefix having been typed|Flag names are only completed if the user has typed the first `-`| +`cmd.MarkZshCompPositionalArgumentFile(pos, []string{})` used to turn on file completion on a per-argument position basis|File completion for all arguments by default; `cmd.MarkZshCompPositionalArgumentFile()` is **deprecated** and silently ignored| +|`cmd.MarkZshCompPositionalArgumentFile(pos, glob[])` used to turn on file completion **with glob filtering** on a per-argument position basis (zsh-specific)|`cmd.MarkZshCompPositionalArgumentFile()` is **deprecated** and silently ignored; use `ValidArgsFunction` with `ShellCompDirectiveFilterFileExt` for file **extension** filtering (not full glob filtering)| +|`cmd.MarkZshCompPositionalArgumentWords(pos, words[])` used to provide completion choices on a per-argument position basis (zsh-specific)|`cmd.MarkZshCompPositionalArgumentWords()` is **deprecated** and silently ignored; use `ValidArgsFunction` to achieve the same behavior| + +**Flag-value completion** + +|Old behavior|New behavior| +|---|---| +|No file completion by default (opposite of bash)|File completion by default; use `RegisterFlagCompletionFunc()` with `ShellCompDirectiveNoFileComp` to turn off file completion| +|`cmd.MarkFlagFilename(flag, []string{})` and similar used to turn on file completion|File completion by default; `cmd.MarkFlagFilename(flag, []string{})` no longer needed in this context and silently ignored| +|`cmd.MarkFlagFilename(flag, glob[])` used to turn on file completion **with glob filtering** (syntax of `[]string{"*.yaml", "*.yml"}` incompatible with bash)|Will continue to work, however, support for bash syntax is added and should be used instead so as to work for all shells (`[]string{"yaml", "yml"}`)| +|`cmd.MarkFlagDirname(flag)` only completes directories (zsh-specific)|Has been added for all shells| +|Completion of a flag name does not repeat, unless flag is of type `*Array` or `*Slice` (not supported by bash)|Retained for `zsh` and added to `fish`| +|Completion of a flag name does not provide the `=` form (unlike bash)|Retained for `zsh` and added to `fish`| + +**Improvements** + +* Custom completion support (`ValidArgsFunction` and `RegisterFlagCompletionFunc()`) +* File completion by default if no other completions found +* Handling of required flags +* File extension filtering no longer mutually exclusive with bash usage +* Completion of directory names *within* another directory +* Support for `=` form of flags diff --git a/vendor/modules.txt b/vendor/modules.txt index 631ddb0060..8b53c44286 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,6 +121,9 @@ github.com/go-openapi/jsonreference/internal # github.com/go-openapi/swag v0.22.3 ## explicit; go 1.18 github.com/go-openapi/swag +# github.com/gobuffalo/flect v0.3.0 +## explicit; go 1.16 +github.com/gobuffalo/flect # github.com/gogo/protobuf v1.3.2 ## explicit; go 1.15 github.com/gogo/protobuf/gogoproto @@ -207,6 +210,9 @@ github.com/hashicorp/serf/coordinate # github.com/imdario/mergo v0.3.13 ## explicit; go 1.13 github.com/imdario/mergo +# github.com/inconshreveable/mousetrap v1.1.0 +## explicit; go 1.18 +github.com/inconshreveable/mousetrap # github.com/jmespath/go-jmespath v0.4.0 ## explicit; go 1.14 github.com/jmespath/go-jmespath @@ -357,6 +363,9 @@ github.com/russross/blackfriday/v2 # github.com/sirupsen/logrus v1.9.3 ## explicit; go 1.13 github.com/sirupsen/logrus +# github.com/spf13/cobra v1.7.0 +## explicit; go 1.15 +github.com/spf13/cobra # github.com/spf13/pflag v1.0.5 ## explicit; go 1.12 github.com/spf13/pflag @@ -1027,6 +1036,22 @@ sigs.k8s.io/controller-runtime/pkg/webhook sigs.k8s.io/controller-runtime/pkg/webhook/admission sigs.k8s.io/controller-runtime/pkg/webhook/conversion sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics +# sigs.k8s.io/controller-tools v0.11.3 +## explicit; go 1.19 +sigs.k8s.io/controller-tools/cmd/controller-gen +sigs.k8s.io/controller-tools/pkg/crd +sigs.k8s.io/controller-tools/pkg/crd/markers +sigs.k8s.io/controller-tools/pkg/deepcopy +sigs.k8s.io/controller-tools/pkg/genall +sigs.k8s.io/controller-tools/pkg/genall/help +sigs.k8s.io/controller-tools/pkg/genall/help/pretty +sigs.k8s.io/controller-tools/pkg/loader +sigs.k8s.io/controller-tools/pkg/markers +sigs.k8s.io/controller-tools/pkg/rbac +sigs.k8s.io/controller-tools/pkg/schemapatcher +sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml +sigs.k8s.io/controller-tools/pkg/version +sigs.k8s.io/controller-tools/pkg/webhook # sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd ## explicit; go 1.18 sigs.k8s.io/json diff --git a/vendor/sigs.k8s.io/controller-tools/LICENSE b/vendor/sigs.k8s.io/controller-tools/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go b/vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go new file mode 100644 index 0000000000..c27bcda42c --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go @@ -0,0 +1,263 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "sigs.k8s.io/controller-tools/pkg/crd" + "sigs.k8s.io/controller-tools/pkg/deepcopy" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/genall/help" + prettyhelp "sigs.k8s.io/controller-tools/pkg/genall/help/pretty" + "sigs.k8s.io/controller-tools/pkg/markers" + "sigs.k8s.io/controller-tools/pkg/rbac" + "sigs.k8s.io/controller-tools/pkg/schemapatcher" + "sigs.k8s.io/controller-tools/pkg/version" + "sigs.k8s.io/controller-tools/pkg/webhook" +) + +//go:generate go run ../helpgen/main.go paths=../../pkg/... generate:headerFile=../../boilerplate.go.txt,year=2019 + +// Options are specified to controller-gen by turning generators and output rules into +// markers, and then parsing them using the standard registry logic (without the "+"). +// Each marker and output rule should thus be usable as a marker target. + +var ( + // allGenerators maintains the list of all known generators, giving + // them names for use on the command line. + // each turns into a command line option, + // and has options for output forms. + allGenerators = map[string]genall.Generator{ + "crd": crd.Generator{}, + "rbac": rbac.Generator{}, + "object": deepcopy.Generator{}, + "webhook": webhook.Generator{}, + "schemapatch": schemapatcher.Generator{}, + } + + // allOutputRules defines the list of all known output rules, giving + // them names for use on the command line. + // Each output rule turns into two command line options: + // - output::
(per-generator output) + // - output: (default output) + allOutputRules = map[string]genall.OutputRule{ + "dir": genall.OutputToDirectory(""), + "none": genall.OutputToNothing, + "stdout": genall.OutputToStdout, + "artifacts": genall.OutputArtifacts{}, + } + + // optionsRegistry contains all the marker definitions used to process command line options + optionsRegistry = &markers.Registry{} +) + +func init() { + for genName, gen := range allGenerators { + // make the generator options marker itself + defn := markers.Must(markers.MakeDefinition(genName, markers.DescribesPackage, gen)) + if err := optionsRegistry.Register(defn); err != nil { + panic(err) + } + if helpGiver, hasHelp := gen.(genall.HasHelp); hasHelp { + if help := helpGiver.Help(); help != nil { + optionsRegistry.AddHelp(defn, help) + } + } + + // make per-generation output rule markers + for ruleName, rule := range allOutputRules { + ruleMarker := markers.Must(markers.MakeDefinition(fmt.Sprintf("output:%s:%s", genName, ruleName), markers.DescribesPackage, rule)) + if err := optionsRegistry.Register(ruleMarker); err != nil { + panic(err) + } + if helpGiver, hasHelp := rule.(genall.HasHelp); hasHelp { + if help := helpGiver.Help(); help != nil { + optionsRegistry.AddHelp(ruleMarker, help) + } + } + } + } + + // make "default output" output rule markers + for ruleName, rule := range allOutputRules { + ruleMarker := markers.Must(markers.MakeDefinition("output:"+ruleName, markers.DescribesPackage, rule)) + if err := optionsRegistry.Register(ruleMarker); err != nil { + panic(err) + } + if helpGiver, hasHelp := rule.(genall.HasHelp); hasHelp { + if help := helpGiver.Help(); help != nil { + optionsRegistry.AddHelp(ruleMarker, help) + } + } + } + + // add in the common options markers + if err := genall.RegisterOptionsMarkers(optionsRegistry); err != nil { + panic(err) + } +} + +// noUsageError suppresses usage printing when it occurs +// (since cobra doesn't provide a good way to avoid printing +// out usage in only certain situations). +type noUsageError struct{ error } + +func main() { + helpLevel := 0 + whichLevel := 0 + showVersion := false + + cmd := &cobra.Command{ + Use: "controller-gen", + Short: "Generate Kubernetes API extension resources and code.", + Long: "Generate Kubernetes API extension resources and code.", + Example: ` # Generate RBAC manifests and crds for all types under apis/, + # outputting crds to /tmp/crds and everything else to stdout + controller-gen rbac:roleName= crd paths=./apis/... output:crd:dir=/tmp/crds output:stdout + + # Generate deepcopy/runtime.Object implementations for a particular file + controller-gen object paths=./apis/v1beta1/some_types.go + + # Generate OpenAPI v3 schemas for API packages and merge them into existing CRD manifests + controller-gen schemapatch:manifests=./manifests output:dir=./manifests paths=./pkg/apis/... + + # Run all the generators for a given project + controller-gen paths=./apis/... + + # Explain the markers for generating CRDs, and their arguments + controller-gen crd -ww +`, + RunE: func(c *cobra.Command, rawOpts []string) error { + // print version if asked for it + if showVersion { + version.Print() + return nil + } + + // print the help if we asked for it (since we've got a different help flag :-/), then bail + if helpLevel > 0 { + return c.Usage() + } + + // print the marker docs if we asked for them, then bail + if whichLevel > 0 { + return printMarkerDocs(c, rawOpts, whichLevel) + } + + // otherwise, set up the runtime for actually running the generators + rt, err := genall.FromOptions(optionsRegistry, rawOpts) + if err != nil { + return err + } + if len(rt.Generators) == 0 { + return fmt.Errorf("no generators specified") + } + + if hadErrs := rt.Run(); hadErrs { + // don't obscure the actual error with a bunch of usage + return noUsageError{fmt.Errorf("not all generators ran successfully")} + } + return nil + }, + SilenceUsage: true, // silence the usage, then print it out ourselves if it wasn't suppressed + } + cmd.Flags().CountVarP(&whichLevel, "which-markers", "w", "print out all markers available with the requested generators\n(up to -www for the most detailed output, or -wwww for json output)") + cmd.Flags().CountVarP(&helpLevel, "detailed-help", "h", "print out more detailed help\n(up to -hhh for the most detailed output, or -hhhh for json output)") + cmd.Flags().BoolVar(&showVersion, "version", false, "show version") + cmd.Flags().Bool("help", false, "print out usage and a summary of options") + oldUsage := cmd.UsageFunc() + cmd.SetUsageFunc(func(c *cobra.Command) error { + if err := oldUsage(c); err != nil { + return err + } + if helpLevel == 0 { + helpLevel = summaryHelp + } + fmt.Fprintf(c.OutOrStderr(), "\n\nOptions\n\n") + return helpForLevels(c.OutOrStdout(), c.OutOrStderr(), helpLevel, optionsRegistry, help.SortByOption) + }) + + if err := cmd.Execute(); err != nil { + if _, noUsage := err.(noUsageError); !noUsage { + // print the usage unless we suppressed it + if err := cmd.Usage(); err != nil { + panic(err) + } + } + fmt.Fprintf(cmd.OutOrStderr(), "run `%[1]s %[2]s -w` to see all available markers, or `%[1]s %[2]s -h` for usage\n", cmd.CalledAs(), strings.Join(os.Args[1:], " ")) + os.Exit(1) + } +} + +// printMarkerDocs prints out marker help for the given generators specified in +// the rawOptions, at the given level. +func printMarkerDocs(c *cobra.Command, rawOptions []string, whichLevel int) error { + // just grab a registry so we don't lag while trying to load roots + // (like we'd do if we just constructed the full runtime). + reg, err := genall.RegistryFromOptions(optionsRegistry, rawOptions) + if err != nil { + return err + } + + return helpForLevels(c.OutOrStdout(), c.OutOrStderr(), whichLevel, reg, help.SortByCategory) +} + +func helpForLevels(mainOut io.Writer, errOut io.Writer, whichLevel int, reg *markers.Registry, sorter help.SortGroup) error { + helpInfo := help.ByCategory(reg, sorter) + switch whichLevel { + case jsonHelp: + if err := json.NewEncoder(mainOut).Encode(helpInfo); err != nil { + return err + } + case detailedHelp, fullHelp: + fullDetail := whichLevel == fullHelp + for _, cat := range helpInfo { + if cat.Category == "" { + continue + } + contents := prettyhelp.MarkersDetails(fullDetail, cat.Category, cat.Markers) + if err := contents.WriteTo(errOut); err != nil { + return err + } + } + case summaryHelp: + for _, cat := range helpInfo { + if cat.Category == "" { + continue + } + contents := prettyhelp.MarkersSummary(cat.Category, cat.Markers) + if err := contents.WriteTo(errOut); err != nil { + return err + } + } + } + return nil +} + +const ( + _ = iota + summaryHelp + detailedHelp + fullHelp + jsonHelp +) diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/conv.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/conv.go new file mode 100644 index 0000000000..374e02c373 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/conv.go @@ -0,0 +1,43 @@ +package crd + +import ( + "fmt" + + apiextinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + conversionScheme = runtime.NewScheme() +) + +func init() { + if err := apiextinternal.AddToScheme(conversionScheme); err != nil { + panic("must be able to add internal apiextensions to the CRD conversion Scheme") + } + if err := apiext.AddToScheme(conversionScheme); err != nil { + panic("must be able to add apiextensions/v1 to the CRD conversion Scheme") + } +} + +// AsVersion converts a CRD from the canonical internal form (currently v1) to some external form. +func AsVersion(original apiext.CustomResourceDefinition, gv schema.GroupVersion) (runtime.Object, error) { + // TODO: Do we need to keep maintaining this conversion function + // post 1.22 when only CRDv1 is served by the apiserver? + if gv == apiextv1beta1.SchemeGroupVersion { + return nil, fmt.Errorf("apiVersion %q is not supported", gv.String()) + } + // We can use the internal versions an existing conversions from kubernetes, since they're not in k/k itself. + // This punts the problem of conversion down the road for a future maintainer (or future instance of @directxman12) + // when we have to support older versions that get removed, or when API machinery decides to yell at us for this + // questionable decision. + intVer, err := conversionScheme.ConvertToVersion(&original, apiextinternal.SchemeGroupVersion) + if err != nil { + return nil, fmt.Errorf("unable to convert to internal CRD version: %w", err) + } + + return conversionScheme.ConvertToVersion(intVer, gv) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/desc_visitor.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/desc_visitor.go new file mode 100644 index 0000000000..cba36c46cb --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/desc_visitor.go @@ -0,0 +1,78 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + "strings" + "unicode" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +// TruncateDescription truncates the description of fields in given schema if it +// exceeds maxLen. +// It tries to chop off the description at the closest sentence boundary. +func TruncateDescription(schema *apiext.JSONSchemaProps, maxLen int) { + EditSchema(schema, descVisitor{maxLen: maxLen}) +} + +// descVisitor recursively visits all fields in the schema and truncates the +// description of the fields to specified maxLen. +type descVisitor struct { + // maxLen is the maximum allowed length for decription of a field + maxLen int +} + +func (v descVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor { + if schema == nil { + return v + } + if v.maxLen < 0 { + return nil /* no further work to be done for this schema */ + } + if v.maxLen == 0 { + schema.Description = "" + return v + } + if len(schema.Description) > v.maxLen { + schema.Description = truncateString(schema.Description, v.maxLen) + return v + } + return v +} + +// truncateString truncates given desc string if it exceeds maxLen. It may +// return string with length less than maxLen even in cases where original desc +// exceeds maxLen because it tries to chop off the desc at the closest sentence +// boundary to avoid incomplete sentences. +func truncateString(desc string, maxLen int) string { + desc = desc[0:maxLen] + + // Trying to chop off at closest sentence boundary. + if n := strings.LastIndexFunc(desc, isSentenceTerminal); n > 0 { + return desc[0 : n+1] + } + // TODO(droot): Improve the logic to chop off at closest word boundary + // or add elipses (...) to indicate that it's chopped incase no closest + // sentence found within maxLen. + return desc +} + +// helper function to determine if given rune is a sentence terminal or not. +func isSentenceTerminal(r rune) bool { + return unicode.Is(unicode.STerm, r) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/doc.go new file mode 100644 index 0000000000..914af94d91 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/doc.go @@ -0,0 +1,63 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package crd contains utilities for generating CustomResourceDefinitions and +// their corresponding OpenAPI validation schemata. +// +// # Markers +// +// Markers live under the markers subpackage. Two types of markers exist: +// those that modify schema generation (for validation), and those that modify +// the rest of the CRD. See the subpackage for more information and all +// supported markers. +// +// # Collecting Types and Generating CRDs +// +// The Parser is the entrypoint for collecting the information required to +// generate CRDs. Like loader and collector, its methods are idemptotent, not +// doing extra work if called multiple times. +// +// Parser's method start with Need. Calling NeedXYZ indicates that XYZ should +// be made present in the eqivalent field in the Parser, where it can then be +// loaded from. Each Need method will in turn call Need on anything it needs. +// +// In general, root packages should first be loaded into the Parser with +// NeedPackage. Then, CRDs can be generated with NeedCRDFor. +// +// Errors are generally attached directly to the relevant Package with +// AddError. +// +// # Known Packages +// +// There are a few types from Kubernetes that have special meaning, but don't +// have validation markers attached. Those specific types have overrides +// listed in KnownPackages that can be added as overrides to any parser. +// +// # Flattening +// +// Once schemata are generated, they can be used directly by external tooling +// (like JSONSchema validators), but must first be "flattened" to not contain +// references before use in a CRD (Kubernetes doesn't allow references in the +// CRD's validation schema). +// +// The Flattener built in to the Parser takes care of flattening out references +// when requesting the CRDs, but can be invoked manually. It will not modify +// the input schemata. +// +// Flattened schemata may further be passed to FlattenEmbedded to remove the +// use of AllOf (which is used to describe embedded struct fields when +// references are in use). This done automatically when fetching CRDs. +package crd diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/flatten.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/flatten.go new file mode 100644 index 0000000000..9224c26b27 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/flatten.go @@ -0,0 +1,445 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + "fmt" + "reflect" + "sort" + "strings" + "sync" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// ErrorRecorder knows how to record errors. It wraps the part of +// pkg/loader.Package that we need to record errors in places were it might not +// make sense to have a loader.Package +type ErrorRecorder interface { + // AddError records that the given error occurred. + // See the documentation on loader.Package.AddError for more information. + AddError(error) +} + +// isOrNil checks if val is nil if val is of a nillable type, otherwise, +// it compares val to valInt (which should probably be the zero value). +func isOrNil(val reflect.Value, valInt interface{}, zeroInt interface{}) bool { + switch valKind := val.Kind(); valKind { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return val.IsNil() + default: + return valInt == zeroInt + } +} + +// flattenAllOfInto copies properties from src to dst, then copies the properties +// of each item in src's allOf to dst's properties as well. +func flattenAllOfInto(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps, errRec ErrorRecorder) { + if len(src.AllOf) > 0 { + for _, embedded := range src.AllOf { + flattenAllOfInto(dst, embedded, errRec) + } + } + + dstVal := reflect.Indirect(reflect.ValueOf(dst)) + srcVal := reflect.ValueOf(src) + typ := dstVal.Type() + + srcRemainder := apiext.JSONSchemaProps{} + srcRemVal := reflect.Indirect(reflect.ValueOf(&srcRemainder)) + dstRemainder := apiext.JSONSchemaProps{} + dstRemVal := reflect.Indirect(reflect.ValueOf(&dstRemainder)) + hoisted := false + + for i := 0; i < srcVal.NumField(); i++ { + fieldName := typ.Field(i).Name + switch fieldName { + case "AllOf": + // don't merge because we deal with it above + continue + case "Title", "Description", "Example", "ExternalDocs": + // don't merge because we pre-merge to properly preserve field docs + continue + } + srcField := srcVal.Field(i) + fldTyp := srcField.Type() + zeroVal := reflect.Zero(fldTyp) + zeroInt := zeroVal.Interface() + srcInt := srcField.Interface() + + if isOrNil(srcField, srcInt, zeroInt) { + // nothing to copy from src, continue + continue + } + + dstField := dstVal.Field(i) + dstInt := dstField.Interface() + if isOrNil(dstField, dstInt, zeroInt) { + // dst is empty, continue + dstField.Set(srcField) + continue + } + + if fldTyp.Comparable() && srcInt == dstInt { + // same value, continue + continue + } + + // resolve conflict + switch fieldName { + case "Properties": + // merge if possible, use all of otherwise + srcMap := srcInt.(map[string]apiext.JSONSchemaProps) + dstMap := dstInt.(map[string]apiext.JSONSchemaProps) + + for k, v := range srcMap { + dstProp, exists := dstMap[k] + if !exists { + dstMap[k] = v + continue + } + flattenAllOfInto(&dstProp, v, errRec) + dstMap[k] = dstProp + } + case "Required": + // merge + dstField.Set(reflect.AppendSlice(dstField, srcField)) + case "Type": + if srcInt != dstInt { + // TODO(directxman12): figure out how to attach this back to a useful point in the Go source or in the schema + errRec.AddError(fmt.Errorf("conflicting types in allOf branches in schema: %s vs %s", dstInt, srcInt)) + } + // keep the destination value, for now + // TODO(directxman12): Default -- use field? + // TODO(directxman12): + // - Dependencies: if field x is present, then either schema validates or all props are present + // - AdditionalItems: like AdditionalProperties + // - Definitions: common named validation sets that can be references (merge, bail if duplicate) + case "AdditionalProperties": + // as of the time of writing, `allows: false` is not allowed, so we don't have to handle it + srcProps := srcInt.(*apiext.JSONSchemaPropsOrBool) + if srcProps.Schema == nil { + // nothing to merge + continue + } + dstProps := dstInt.(*apiext.JSONSchemaPropsOrBool) + if dstProps.Schema == nil { + dstProps.Schema = &apiext.JSONSchemaProps{} + } + flattenAllOfInto(dstProps.Schema, *srcProps.Schema, errRec) + case "XPreserveUnknownFields": + dstField.Set(srcField) + case "XMapType": + dstField.Set(srcField) + // NB(directxman12): no need to explicitly handle nullable -- false is considered to be the zero value + // TODO(directxman12): src isn't necessarily the field value -- it's just the most recent allOf entry + default: + // hoist into allOf... + hoisted = true + + srcRemVal.Field(i).Set(srcField) + dstRemVal.Field(i).Set(dstField) + // ...and clear the original + dstField.Set(zeroVal) + } + } + + if hoisted { + dst.AllOf = append(dst.AllOf, dstRemainder, srcRemainder) + } + + // dedup required + if len(dst.Required) > 0 { + reqUniq := make(map[string]struct{}) + for _, req := range dst.Required { + reqUniq[req] = struct{}{} + } + dst.Required = make([]string, 0, len(reqUniq)) + for req := range reqUniq { + dst.Required = append(dst.Required, req) + } + // be deterministic + sort.Strings(dst.Required) + } +} + +// allOfVisitor recursively visits allOf fields in the schema, +// merging nested allOf properties into the root schema. +type allOfVisitor struct { + // errRec is used to record errors while flattening (like two conflicting + // field values used in an allOf) + errRec ErrorRecorder +} + +func (v *allOfVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor { + if schema == nil { + return v + } + + // clear this now so that we can safely preserve edits made my flattenAllOfInto + origAllOf := schema.AllOf + schema.AllOf = nil + + for _, embedded := range origAllOf { + flattenAllOfInto(schema, embedded, v.errRec) + } + return v +} + +// NB(directxman12): FlattenEmbedded is separate from Flattener because +// some tooling wants to flatten out embedded fields, but only actually +// flatten a few specific types first. + +// FlattenEmbedded flattens embedded fields (represented via AllOf) which have +// already had their references resolved into simple properties in the containing +// schema. +func FlattenEmbedded(schema *apiext.JSONSchemaProps, errRec ErrorRecorder) *apiext.JSONSchemaProps { + outSchema := schema.DeepCopy() + EditSchema(outSchema, &allOfVisitor{errRec: errRec}) + return outSchema +} + +// Flattener knows how to take a root type, and flatten all references in it +// into a single, flat type. Flattened types are cached, so it's relatively +// cheap to make repeated calls with the same type. +type Flattener struct { + // Parser is used to lookup package and type details, and parse in new packages. + Parser *Parser + + LookupReference func(ref string, contextPkg *loader.Package) (TypeIdent, error) + + // flattenedTypes hold the flattened version of each seen type for later reuse. + flattenedTypes map[TypeIdent]apiext.JSONSchemaProps + initOnce sync.Once +} + +func (f *Flattener) init() { + f.initOnce.Do(func() { + f.flattenedTypes = make(map[TypeIdent]apiext.JSONSchemaProps) + if f.LookupReference == nil { + f.LookupReference = identFromRef + } + }) +} + +// cacheType saves the flattened version of the given type for later reuse +func (f *Flattener) cacheType(typ TypeIdent, schema apiext.JSONSchemaProps) { + f.init() + f.flattenedTypes[typ] = schema +} + +// loadUnflattenedSchema fetches a fresh, unflattened schema from the parser. +func (f *Flattener) loadUnflattenedSchema(typ TypeIdent) (*apiext.JSONSchemaProps, error) { + f.Parser.NeedSchemaFor(typ) + + baseSchema, found := f.Parser.Schemata[typ] + if !found { + return nil, fmt.Errorf("unable to locate schema for type %s", typ) + } + return &baseSchema, nil +} + +// FlattenType flattens the given pre-loaded type, removing any references from it. +// It deep-copies the schema first, so it won't affect the parser's version of the schema. +func (f *Flattener) FlattenType(typ TypeIdent) *apiext.JSONSchemaProps { + f.init() + if cachedSchema, isCached := f.flattenedTypes[typ]; isCached { + return &cachedSchema + } + baseSchema, err := f.loadUnflattenedSchema(typ) + if err != nil { + typ.Package.AddError(err) + return nil + } + resSchema := f.FlattenSchema(*baseSchema, typ.Package) + f.cacheType(typ, *resSchema) + return resSchema +} + +// FlattenSchema flattens the given schema, removing any references. +// It deep-copies the schema first, so the input schema won't be affected. +func (f *Flattener) FlattenSchema(baseSchema apiext.JSONSchemaProps, currentPackage *loader.Package) *apiext.JSONSchemaProps { + resSchema := baseSchema.DeepCopy() + EditSchema(resSchema, &flattenVisitor{ + Flattener: f, + currentPackage: currentPackage, + }) + + return resSchema +} + +// RefParts splits a reference produced by the schema generator into its component +// type name and package name (if it's a cross-package reference). Note that +// referenced packages *must* be looked up relative to the current package. +func RefParts(ref string) (typ string, pkgName string, err error) { + if !strings.HasPrefix(ref, defPrefix) { + return "", "", fmt.Errorf("non-standard reference link %q", ref) + } + ref = ref[len(defPrefix):] + // decode the json pointer encodings + ref = strings.Replace(ref, "~1", "/", -1) + ref = strings.Replace(ref, "~0", "~", -1) + nameParts := strings.SplitN(ref, "~", 2) + + if len(nameParts) == 1 { + // local reference + return nameParts[0], "", nil + } + // cross-package reference + return nameParts[1], nameParts[0], nil +} + +// identFromRef converts the given schema ref from the given package back +// into the TypeIdent that it represents. +func identFromRef(ref string, contextPkg *loader.Package) (TypeIdent, error) { + typ, pkgName, err := RefParts(ref) + if err != nil { + return TypeIdent{}, err + } + + if pkgName == "" { + // a local reference + return TypeIdent{ + Name: typ, + Package: contextPkg, + }, nil + } + + // an external reference + return TypeIdent{ + Name: typ, + Package: contextPkg.Imports()[pkgName], + }, nil +} + +// preserveFields copies documentation fields from src into dst, preserving +// field-level documentation when flattening, and preserving field-level validation +// as allOf entries. +func preserveFields(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps) { + srcDesc := src.Description + srcTitle := src.Title + srcExDoc := src.ExternalDocs + srcEx := src.Example + + src.Description, src.Title, src.ExternalDocs, src.Example = "", "", nil, nil + + src.Ref = nil + *dst = apiext.JSONSchemaProps{ + AllOf: []apiext.JSONSchemaProps{*dst, src}, + + // keep these, in case the source field doesn't specify anything useful + Description: dst.Description, + Title: dst.Title, + ExternalDocs: dst.ExternalDocs, + Example: dst.Example, + } + + if srcDesc != "" { + dst.Description = srcDesc + } + if srcTitle != "" { + dst.Title = srcTitle + } + if srcExDoc != nil { + dst.ExternalDocs = srcExDoc + } + if srcEx != nil { + dst.Example = srcEx + } +} + +// flattenVisitor visits each node in the schema, recursively flattening references. +type flattenVisitor struct { + *Flattener + + currentPackage *loader.Package + currentType *TypeIdent + currentSchema *apiext.JSONSchemaProps + originalField apiext.JSONSchemaProps +} + +func (f *flattenVisitor) Visit(baseSchema *apiext.JSONSchemaProps) SchemaVisitor { + if baseSchema == nil { + // end-of-node marker, cache the results + if f.currentType != nil { + f.cacheType(*f.currentType, *f.currentSchema) + // preserve field information *after* caching so that we don't + // accidentally cache field-level information onto the schema for + // the type in general. + preserveFields(f.currentSchema, f.originalField) + } + return f + } + + // if we get a type that's just a ref, resolve it + if baseSchema.Ref != nil && len(*baseSchema.Ref) > 0 { + // resolve this ref + refIdent, err := f.LookupReference(*baseSchema.Ref, f.currentPackage) + if err != nil { + f.currentPackage.AddError(err) + return nil + } + + // load and potentially flatten the schema + + // check the cache first... + if refSchemaCached, isCached := f.flattenedTypes[refIdent]; isCached { + // shallow copy is fine, it's just to avoid overwriting the doc fields + preserveFields(&refSchemaCached, *baseSchema) + *baseSchema = refSchemaCached + return nil // don't recurse, we're done + } + + // ...otherwise, we need to flatten + refSchema, err := f.loadUnflattenedSchema(refIdent) + if err != nil { + f.currentPackage.AddError(err) + return nil + } + refSchema = refSchema.DeepCopy() + + // keep field around to preserve field-level validation, docs, etc + origField := *baseSchema + *baseSchema = *refSchema + + // avoid loops (which shouldn't exist, but just in case) + // by marking a nil cached pointer before we start recursing + f.cacheType(refIdent, apiext.JSONSchemaProps{}) + + return &flattenVisitor{ + Flattener: f.Flattener, + + currentPackage: refIdent.Package, + currentType: &refIdent, + currentSchema: baseSchema, + originalField: origField, + } + } + + // otherwise, continue recursing... + if f.currentType != nil { + // ...but don't accidentally end this node early (for caching purposes) + return &flattenVisitor{ + Flattener: f.Flattener, + currentPackage: f.currentPackage, + } + } + + return f +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/gen.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/gen.go new file mode 100644 index 0000000000..c6c5f88b8d --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/gen.go @@ -0,0 +1,307 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + "fmt" + "go/ast" + "go/types" + "sort" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" + "sigs.k8s.io/controller-tools/pkg/version" +) + +// The identifier for v1 CustomResourceDefinitions. +const v1 = "v1" + +// The default CustomResourceDefinition version to generate. +const defaultVersion = v1 + +// +controllertools:marker:generateHelp + +// Generator generates CustomResourceDefinition objects. +type Generator struct { + // IgnoreUnexportedFields indicates that we should skip unexported fields. + // + // Left unspecified, the default is false. + IgnoreUnexportedFields *bool `marker:",optional"` + + // AllowDangerousTypes allows types which are usually omitted from CRD generation + // because they are not recommended. + // + // Currently the following additional types are allowed when this is true: + // float32 + // float64 + // + // Left unspecified, the default is false + AllowDangerousTypes *bool `marker:",optional"` + + // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema. + // + // 0 indicates drop the description for all fields completely. + // n indicates limit the description to at most n characters and truncate the description to + // closest sentence boundary if it exceeds n characters. + MaxDescLen *int `marker:",optional"` + + // CRDVersions specifies the target API versions of the CRD type itself to + // generate. Defaults to v1. + // + // Currently, the only supported value is v1. + // + // The first version listed will be assumed to be the "default" version and + // will not get a version suffix in the output filename. + // + // You'll need to use "v1" to get support for features like defaulting, + // along with an API server that supports it (Kubernetes 1.16+). + CRDVersions []string `marker:"crdVersions,optional"` + + // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated + GenerateEmbeddedObjectMeta *bool `marker:",optional"` +} + +func (Generator) CheckFilter() loader.NodeFilter { + return filterTypesForCRDs +} +func (Generator) RegisterMarkers(into *markers.Registry) error { + return crdmarkers.Register(into) +} + +// transformRemoveCRDStatus ensures we do not write the CRD status field. +func transformRemoveCRDStatus(obj map[string]interface{}) error { + delete(obj, "status") + return nil +} + +func (g Generator) Generate(ctx *genall.GenerationContext) error { + parser := &Parser{ + Collector: ctx.Collector, + Checker: ctx.Checker, + // Perform defaulting here to avoid ambiguity later + IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields == true, + AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true, + // Indicates the parser on whether to register the ObjectMeta type or not + GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true, + } + + AddKnownTypes(parser) + for _, root := range ctx.Roots { + parser.NeedPackage(root) + } + + metav1Pkg := FindMetav1(ctx.Roots) + if metav1Pkg == nil { + // no objects in the roots, since nothing imported metav1 + return nil + } + + // TODO: allow selecting a specific object + kubeKinds := FindKubeKinds(parser, metav1Pkg) + if len(kubeKinds) == 0 { + // no objects in the roots + return nil + } + + crdVersions := g.CRDVersions + + if len(crdVersions) == 0 { + crdVersions = []string{defaultVersion} + } + + for _, groupKind := range kubeKinds { + parser.NeedCRDFor(groupKind, g.MaxDescLen) + crdRaw := parser.CustomResourceDefinitions[groupKind] + addAttribution(&crdRaw) + + // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments + FixTopLevelMetadata(crdRaw) + + versionedCRDs := make([]interface{}, len(crdVersions)) + for i, ver := range crdVersions { + conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver}) + if err != nil { + return err + } + versionedCRDs[i] = conv + } + + for i, crd := range versionedCRDs { + removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition)) + var fileName string + if i == 0 { + fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) + } else { + fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) + } + if err := ctx.WriteYAML(fileName, []interface{}{crd}, genall.WithTransform(transformRemoveCRDStatus)); err != nil { + return err + } + } + } + + return nil +} + +func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) { + for _, versionSpec := range crd.Spec.Versions { + if versionSpec.Schema != nil { + removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema) + } + } +} + +func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) { + if m, ok := v.Properties["metadata"]; ok { + meta := &m + if meta.Description != "" { + meta.Description = "" + v.Properties["metadata"] = m + + } + } +} + +// FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation +func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) { + for _, v := range crd.Spec.Versions { + if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil { + schemaProperties := v.Schema.OpenAPIV3Schema.Properties + if _, ok := schemaProperties["metadata"]; ok { + schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"} + } + } + } +} + +// addAttribution adds attribution info to indicate controller-gen tool was used +// to generate this CRD definition along with the version info. +func addAttribution(crd *apiext.CustomResourceDefinition) { + if crd.ObjectMeta.Annotations == nil { + crd.ObjectMeta.Annotations = map[string]string{} + } + crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version() +} + +// FindMetav1 locates the actual package representing metav1 amongst +// the imports of the roots. +func FindMetav1(roots []*loader.Package) *loader.Package { + for _, root := range roots { + pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] + if pkg != nil { + return pkg + } + } + return nil +} + +// FindKubeKinds locates all types that contain TypeMeta and ObjectMeta +// (and thus may be a Kubernetes object), and returns the corresponding +// group-kinds. +func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind { + // TODO(directxman12): technically, we should be finding metav1 per-package + kubeKinds := map[schema.GroupKind]struct{}{} + for typeIdent, info := range parser.Types { + hasObjectMeta := false + hasTypeMeta := false + + pkg := typeIdent.Package + pkg.NeedTypesInfo() + typesInfo := pkg.TypesInfo + + for _, field := range info.Fields { + if field.Name != "" { + // type and object meta are embedded, + // so they can't be this + continue + } + + fieldType := typesInfo.TypeOf(field.RawField.Type) + namedField, isNamed := fieldType.(*types.Named) + if !isNamed { + // ObjectMeta and TypeMeta are named types + continue + } + if namedField.Obj().Pkg() == nil { + // Embedded non-builtin universe type (specifically, it's probably `error`), + // so it can't be ObjectMeta or TypeMeta + continue + } + fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path()) + fieldPkg := pkg.Imports()[fieldPkgPath] + + // Compare the metav1 package by ID and not by the actual instance + // of the object. The objects in memory could be different due to + // loading from different root paths, even when they both refer to + // the same metav1 package. + if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID { + continue + } + + switch namedField.Obj().Name() { + case "ObjectMeta": + hasObjectMeta = true + case "TypeMeta": + hasTypeMeta = true + } + } + + if !hasObjectMeta || !hasTypeMeta { + continue + } + + groupKind := schema.GroupKind{ + Group: parser.GroupVersions[pkg].Group, + Kind: typeIdent.Name, + } + kubeKinds[groupKind] = struct{}{} + } + + groupKindList := make([]schema.GroupKind, 0, len(kubeKinds)) + for groupKind := range kubeKinds { + groupKindList = append(groupKindList, groupKind) + } + sort.Slice(groupKindList, func(i, j int) bool { + return groupKindList[i].String() < groupKindList[j].String() + }) + + return groupKindList +} + +// filterTypesForCRDs filters out all nodes that aren't used in CRD generation, +// like interfaces and struct fields without JSON tag. +func filterTypesForCRDs(node ast.Node) bool { + switch node := node.(type) { + case *ast.InterfaceType: + // skip interfaces, we never care about references in them + return false + case *ast.StructType: + return true + case *ast.Field: + _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json") + // fields without JSON tags mean we have custom serialization, + // so only visit fields with tags. + return hasTag + default: + return true + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/known_types.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/known_types.go new file mode 100644 index 0000000000..9e8decb286 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/known_types.go @@ -0,0 +1,180 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package crd + +import ( + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// KnownPackages overrides types in some comment packages that have custom validation +// but don't have validation markers on them (since they're from core Kubernetes). +var KnownPackages = map[string]PackageOverride{ + "k8s.io/api/core/v1": func(p *Parser, pkg *loader.Package) { + // Explicit defaulting for the corev1.Protocol type in lieu of https://github.com/kubernetes/enhancements/pull/1928 + p.Schemata[TypeIdent{Name: "Protocol", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "string", + Default: &apiext.JSON{Raw: []byte(`"TCP"`)}, + } + p.AddPackage(pkg) + }, + + "k8s.io/apimachinery/pkg/apis/meta/v1": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "ObjectMeta", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "object", + } + p.Schemata[TypeIdent{Name: "Time", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "string", + Format: "date-time", + } + p.Schemata[TypeIdent{Name: "MicroTime", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "string", + Format: "date-time", + } + p.Schemata[TypeIdent{Name: "Duration", Package: pkg}] = apiext.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + Type: "string", + } + p.Schemata[TypeIdent{Name: "Fields", Package: pkg}] = apiext.JSONSchemaProps{ + // this is a recursive structure that can't be flattened or, for that matter, properly generated. + // so just treat it as an arbitrary map + Type: "object", + AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Allows: true}, + } + p.AddPackage(pkg) // get the rest of the types + }, + + "k8s.io/apimachinery/pkg/api/resource": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "Quantity", Package: pkg}] = apiext.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + XIntOrString: true, + AnyOf: []apiext.JSONSchemaProps{ + {Type: "integer"}, + {Type: "string"}, + }, + Pattern: "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + } + // No point in calling AddPackage, this is the sole inhabitant + }, + + "k8s.io/apimachinery/pkg/runtime": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "RawExtension", Package: pkg}] = apiext.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + Type: "object", + XPreserveUnknownFields: boolPtr(true), + } + p.AddPackage(pkg) // get the rest of the types + }, + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "Unstructured", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "object", + } + p.AddPackage(pkg) // get the rest of the types + }, + + "k8s.io/apimachinery/pkg/util/intstr": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "IntOrString", Package: pkg}] = apiext.JSONSchemaProps{ + XIntOrString: true, + AnyOf: []apiext.JSONSchemaProps{ + {Type: "integer"}, + {Type: "string"}, + }, + } + // No point in calling AddPackage, this is the sole inhabitant + }, + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "JSON", Package: pkg}] = apiext.JSONSchemaProps{ + XPreserveUnknownFields: boolPtr(true), + } + p.AddPackage(pkg) // get the rest of the types + }, + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1": func(p *Parser, pkg *loader.Package) { + p.Schemata[TypeIdent{Name: "JSON", Package: pkg}] = apiext.JSONSchemaProps{ + XPreserveUnknownFields: boolPtr(true), + } + p.AddPackage(pkg) // get the rest of the types + }, +} + +// ObjectMetaPackages overrides the ObjectMeta in all types +var ObjectMetaPackages = map[string]PackageOverride{ + "k8s.io/apimachinery/pkg/apis/meta/v1": func(p *Parser, pkg *loader.Package) { + // execute the KnowPackages for `k8s.io/apimachinery/pkg/apis/meta/v1` if any + if f, ok := KnownPackages["k8s.io/apimachinery/pkg/apis/meta/v1"]; ok { + f(p, pkg) + } + // This is an allow-listed set of properties of ObjectMeta, other runtime properties are not part of this list + // See more discussion: https://github.com/kubernetes-sigs/controller-tools/pull/395#issuecomment-691919433 + p.Schemata[TypeIdent{Name: "ObjectMeta", Package: pkg}] = apiext.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiext.JSONSchemaProps{ + "name": { + Type: "string", + }, + "namespace": { + Type: "string", + }, + "annotations": { + Type: "object", + AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ + Schema: &apiext.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "labels": { + Type: "object", + AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ + Schema: &apiext.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "finalizers": { + Type: "array", + Items: &apiext.JSONSchemaPropsOrArray{ + Schema: &apiext.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + } + }, +} + +func boolPtr(b bool) *bool { + return &b +} + +// AddKnownTypes registers the packages overrides in KnownPackages with the given parser. +func AddKnownTypes(parser *Parser) { + // ensure everything is there before adding to PackageOverrides + // TODO(directxman12): this is a bit of a hack, maybe just use constructors? + parser.init() + for pkgName, override := range KnownPackages { + parser.PackageOverrides[pkgName] = override + } + // if we want to generate the embedded ObjectMeta in the CRD we need to add the ObjectMetaPackages + if parser.GenerateEmbeddedObjectMeta { + for pkgName, override := range ObjectMetaPackages { + parser.PackageOverrides[pkgName] = override + } + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/crd.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/crd.go new file mode 100644 index 0000000000..0c637c773c --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/crd.go @@ -0,0 +1,387 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "fmt" + "strings" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// CRDMarkers lists all markers that directly modify the CRD (not validation +// schemas). +var CRDMarkers = []*definitionWithHelp{ + // TODO(directxman12): more detailed help + must(markers.MakeDefinition("kubebuilder:subresource:status", markers.DescribesType, SubresourceStatus{})). + WithHelp(SubresourceStatus{}.Help()), + + must(markers.MakeDefinition("kubebuilder:subresource:scale", markers.DescribesType, SubresourceScale{})). + WithHelp(SubresourceScale{}.Help()), + + must(markers.MakeDefinition("kubebuilder:printcolumn", markers.DescribesType, PrintColumn{})). + WithHelp(PrintColumn{}.Help()), + + must(markers.MakeDefinition("kubebuilder:resource", markers.DescribesType, Resource{})). + WithHelp(Resource{}.Help()), + + must(markers.MakeDefinition("kubebuilder:storageversion", markers.DescribesType, StorageVersion{})). + WithHelp(StorageVersion{}.Help()), + + must(markers.MakeDefinition("kubebuilder:skipversion", markers.DescribesType, SkipVersion{})). + WithHelp(SkipVersion{}.Help()), + + must(markers.MakeDefinition("kubebuilder:unservedversion", markers.DescribesType, UnservedVersion{})). + WithHelp(UnservedVersion{}.Help()), + + must(markers.MakeDefinition("kubebuilder:deprecatedversion", markers.DescribesType, DeprecatedVersion{})). + WithHelp(DeprecatedVersion{}.Help()), + + must(markers.MakeDefinition("kubebuilder:metadata", markers.DescribesType, Metadata{})). + WithHelp(Metadata{}.Help()), +} + +// TODO: categories and singular used to be annotations types +// TODO: doc + +func init() { + AllDefinitions = append(AllDefinitions, CRDMarkers...) +} + +// +controllertools:marker:generateHelp:category=CRD + +// SubresourceStatus enables the "/status" subresource on a CRD. +type SubresourceStatus struct{} + +func (s SubresourceStatus) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + var subresources *apiext.CustomResourceSubresources + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + if ver.Subresources == nil { + ver.Subresources = &apiext.CustomResourceSubresources{} + } + subresources = ver.Subresources + break + } + if subresources == nil { + return fmt.Errorf("status subresource applied to version %q not in CRD", version) + } + subresources.Status = &apiext.CustomResourceSubresourceStatus{} + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// SubresourceScale enables the "/scale" subresource on a CRD. +type SubresourceScale struct { + // marker names are leftover legacy cruft + + // SpecPath specifies the jsonpath to the replicas field for the scale's spec. + SpecPath string `marker:"specpath"` + + // StatusPath specifies the jsonpath to the replicas field for the scale's status. + StatusPath string `marker:"statuspath"` + + // SelectorPath specifies the jsonpath to the pod label selector field for the scale's status. + // + // The selector field must be the *string* form (serialized form) of a selector. + // Setting a pod label selector is necessary for your type to work with the HorizontalPodAutoscaler. + SelectorPath *string `marker:"selectorpath"` +} + +func (s SubresourceScale) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + var subresources *apiext.CustomResourceSubresources + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + if ver.Subresources == nil { + ver.Subresources = &apiext.CustomResourceSubresources{} + } + subresources = ver.Subresources + break + } + if subresources == nil { + return fmt.Errorf("scale subresource applied to version %q not in CRD", version) + } + subresources.Scale = &apiext.CustomResourceSubresourceScale{ + SpecReplicasPath: s.SpecPath, + StatusReplicasPath: s.StatusPath, + LabelSelectorPath: s.SelectorPath, + } + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// StorageVersion marks this version as the "storage version" for the CRD for conversion. +// +// When conversion is enabled for a CRD (i.e. it's not a trivial-versions/single-version CRD), +// one version is set as the "storage version" to be stored in etcd. Attempting to store any +// other version will result in conversion to the storage version via a conversion webhook. +type StorageVersion struct{} + +func (s StorageVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + if version == "" { + // single-version, do nothing + return nil + } + // multi-version + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + ver.Storage = true + break + } + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// SkipVersion removes the particular version of the CRD from the CRDs spec. +// +// This is useful if you need to skip generating and listing version entries +// for 'internal' resource versions, which typically exist if using the +// Kubernetes upstream conversion-gen tool. +type SkipVersion struct{} + +func (s SkipVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + if version == "" { + // single-version, this is an invalid state + return fmt.Errorf("cannot skip a version if there is only a single version") + } + var versions []apiext.CustomResourceDefinitionVersion + // multi-version + for i := range crd.Versions { + ver := crd.Versions[i] + if ver.Name == version { + // skip the skipped version + continue + } + versions = append(versions, ver) + } + crd.Versions = versions + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// PrintColumn adds a column to "kubectl get" output for this CRD. +type PrintColumn struct { + // Name specifies the name of the column. + Name string + + // Type indicates the type of the column. + // + // It may be any OpenAPI data type listed at + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types. + Type string + + // JSONPath specifies the jsonpath expression used to extract the value of the column. + JSONPath string `marker:"JSONPath"` // legacy cruft + + // Description specifies the help/description for this column. + Description string `marker:",optional"` + + // Format specifies the format of the column. + // + // It may be any OpenAPI data format corresponding to the type, listed at + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types. + Format string `marker:",optional"` + + // Priority indicates how important it is that this column be displayed. + // + // Lower priority (*higher* numbered) columns will be hidden if the terminal + // width is too small. + Priority int32 `marker:",optional"` +} + +func (s PrintColumn) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + var columns *[]apiext.CustomResourceColumnDefinition + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + if ver.Subresources == nil { + ver.Subresources = &apiext.CustomResourceSubresources{} + } + columns = &ver.AdditionalPrinterColumns + break + } + if columns == nil { + return fmt.Errorf("printer columns applied to version %q not in CRD", version) + } + + *columns = append(*columns, apiext.CustomResourceColumnDefinition{ + Name: s.Name, + Type: s.Type, + JSONPath: s.JSONPath, + Description: s.Description, + Format: s.Format, + Priority: s.Priority, + }) + + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// Resource configures naming and scope for a CRD. +type Resource struct { + // Path specifies the plural "resource" for this CRD. + // + // It generally corresponds to a plural, lower-cased version of the Kind. + // See https://book.kubebuilder.io/cronjob-tutorial/gvks.html. + Path string `marker:",optional"` + + // ShortName specifies aliases for this CRD. + // + // Short names are often used when people have work with your resource + // over and over again. For instance, "rs" for "replicaset" or + // "crd" for customresourcedefinition. + ShortName []string `marker:",optional"` + + // Categories specifies which group aliases this resource is part of. + // + // Group aliases are used to work with groups of resources at once. + // The most common one is "all" which covers about a third of the base + // resources in Kubernetes, and is generally used for "user-facing" resources. + Categories []string `marker:",optional"` + + // Singular overrides the singular form of your resource. + // + // The singular form is otherwise defaulted off the plural (path). + Singular string `marker:",optional"` + + // Scope overrides the scope of the CRD (Cluster vs Namespaced). + // + // Scope defaults to "Namespaced". Cluster-scoped ("Cluster") resources + // don't exist in namespaces. + Scope string `marker:",optional"` +} + +func (s Resource) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + if s.Path != "" { + crd.Names.Plural = s.Path + } + if s.Singular != "" { + crd.Names.Singular = s.Singular + } + crd.Names.ShortNames = s.ShortName + crd.Names.Categories = s.Categories + + switch s.Scope { + case "": + crd.Scope = apiext.NamespaceScoped + default: + crd.Scope = apiext.ResourceScope(s.Scope) + } + + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// UnservedVersion does not serve this version. +// +// This is useful if you need to drop support for a version in favor of a newer version. +type UnservedVersion struct{} + +func (s UnservedVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + ver.Served = false + break + } + return nil +} + +// NB(directxman12): singular was historically distinct, so we keep it here for backwards compat + +// +controllertools:marker:generateHelp:category=CRD + +// DeprecatedVersion marks this version as deprecated. +type DeprecatedVersion struct { + // Warning message to be shown on the deprecated version + Warning *string `marker:",optional"` +} + +func (s DeprecatedVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error { + if version == "" { + // single-version, do nothing + return nil + } + // multi-version + for i := range crd.Versions { + ver := &crd.Versions[i] + if ver.Name != version { + continue + } + ver.Deprecated = true + ver.DeprecationWarning = s.Warning + break + } + return nil +} + +// +controllertools:marker:generateHelp:category=CRD + +// Metadata configures the additional annotations or labels for this CRD. +// For example adding annotation "api-approved.kubernetes.io" for a CRD with Kubernetes groups, +// or annotation "cert-manager.io/inject-ca-from-secret" for a CRD that needs CA injection. +type Metadata struct { + // Annotations will be added into the annotations of this CRD. + Annotations []string `marker:",optional"` + // Labels will be added into the labels of this CRD. + Labels []string `marker:",optional"` +} + +func (s Metadata) ApplyToCRD(crd *apiext.CustomResourceDefinition, version string) error { + if len(s.Annotations) > 0 { + if crd.Annotations == nil { + crd.Annotations = map[string]string{} + } + for _, str := range s.Annotations { + kv := strings.SplitN(str, "=", 2) + crd.Annotations[kv[0]] = kv[1] + } + } + + if len(s.Labels) > 0 { + if crd.Labels == nil { + crd.Labels = map[string]string{} + } + for _, str := range s.Labels { + kv := strings.SplitN(str, "=", 2) + crd.Labels[kv[0]] = kv[1] + } + } + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/doc.go new file mode 100644 index 0000000000..995af44b37 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/doc.go @@ -0,0 +1,46 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package markers defines markers for generating schema valiation +// and CRD structure. +// +// All markers related to CRD generation live in AllDefinitions. +// +// # Validation Markers +// +// Validation markers have values that implement ApplyToSchema +// (crd.SchemaMarker). Any marker implementing this will automatically +// be run after the rest of a given schema node has been generated. +// Markers that need to be run before any other markers can also +// implement ApplyFirst, but this is discouraged and may change +// in the future. +// +// All validation markers start with "+kubebuilder:validation", and +// have the same name as their type name. +// +// # CRD Markers +// +// Markers that modify anything in the CRD itself *except* for the schema +// implement ApplyToCRD (crd.CRDMarker). They are expected to detect whether +// they should apply themselves to a specific version in the CRD (as passed to +// them), or to the root-level CRD for legacy cases. They are applied *after* +// the rest of the CRD is computed. +// +// # Misc +// +// This package also defines the "+groupName" and "+versionName" package-level +// markers, for defining package<->group-version mappings. +package markers diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/package.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/package.go new file mode 100644 index 0000000000..cebe8fa4b1 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/package.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func init() { + AllDefinitions = append(AllDefinitions, + must(markers.MakeDefinition("groupName", markers.DescribesPackage, "")). + WithHelp(markers.SimpleHelp("CRD", "specifies the API group name for this package.")), + + must(markers.MakeDefinition("versionName", markers.DescribesPackage, "")). + WithHelp(markers.SimpleHelp("CRD", "overrides the API group version for this package (defaults to the package name).")), + + must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesPackage, struct{}{})). + WithHelp(markers.SimpleHelp("CRD validation", "specifies that all fields in this package are optional by default.")), + + must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesPackage, struct{}{})). + WithHelp(markers.SimpleHelp("CRD validation", "specifies that all fields in this package are required by default.")), + + must(markers.MakeDefinition("kubebuilder:skip", markers.DescribesPackage, struct{}{})). + WithHelp(markers.SimpleHelp("CRD", "don't consider this package as an API version.")), + ) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/register.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/register.go new file mode 100644 index 0000000000..0e7c426942 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/register.go @@ -0,0 +1,83 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "reflect" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +type definitionWithHelp struct { + *markers.Definition + Help *markers.DefinitionHelp +} + +func (d *definitionWithHelp) WithHelp(help *markers.DefinitionHelp) *definitionWithHelp { + d.Help = help + return d +} + +func (d *definitionWithHelp) Register(reg *markers.Registry) error { + if err := reg.Register(d.Definition); err != nil { + return err + } + if d.Help != nil { + reg.AddHelp(d.Definition, d.Help) + } + return nil +} + +func must(def *markers.Definition, err error) *definitionWithHelp { + return &definitionWithHelp{ + Definition: markers.Must(def, err), + } +} + +// AllDefinitions contains all marker definitions for this package. +var AllDefinitions []*definitionWithHelp + +type hasHelp interface { + Help() *markers.DefinitionHelp +} + +// mustMakeAllWithPrefix converts each object into a marker definition using +// the object's type's with the prefix to form the marker name. +func mustMakeAllWithPrefix(prefix string, target markers.TargetType, objs ...interface{}) []*definitionWithHelp { + defs := make([]*definitionWithHelp, len(objs)) + for i, obj := range objs { + name := prefix + ":" + reflect.TypeOf(obj).Name() + def, err := markers.MakeDefinition(name, target, obj) + if err != nil { + panic(err) + } + defs[i] = &definitionWithHelp{Definition: def, Help: obj.(hasHelp).Help()} + } + + return defs +} + +// Register registers all definitions for CRD generation to the given registry. +func Register(reg *markers.Registry) error { + for _, def := range AllDefinitions { + if err := def.Register(reg); err != nil { + return err + } + } + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/topology.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/topology.go new file mode 100644 index 0000000000..a92995c802 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/topology.go @@ -0,0 +1,163 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "fmt" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// TopologyMarkers specify topology markers (i.e. markers that describe if a +// list behaves as an associative-list or a set, if a map is atomic or not). +var TopologyMarkers = []*definitionWithHelp{ + must(markers.MakeDefinition("listMapKey", markers.DescribesField, ListMapKey(""))). + WithHelp(ListMapKey("").Help()), + must(markers.MakeDefinition("listMapKey", markers.DescribesType, ListMapKey(""))). + WithHelp(ListMapKey("").Help()), + must(markers.MakeDefinition("listType", markers.DescribesField, ListType(""))). + WithHelp(ListType("").Help()), + must(markers.MakeDefinition("listType", markers.DescribesType, ListType(""))). + WithHelp(ListType("").Help()), + must(markers.MakeDefinition("mapType", markers.DescribesField, MapType(""))). + WithHelp(MapType("").Help()), + must(markers.MakeDefinition("mapType", markers.DescribesType, MapType(""))). + WithHelp(MapType("").Help()), + must(markers.MakeDefinition("structType", markers.DescribesField, StructType(""))). + WithHelp(StructType("").Help()), + must(markers.MakeDefinition("structType", markers.DescribesType, StructType(""))). + WithHelp(StructType("").Help()), +} + +func init() { + AllDefinitions = append(AllDefinitions, TopologyMarkers...) +} + +// +controllertools:marker:generateHelp:category="CRD processing" + +// ListType specifies the type of data-structure that the list +// represents (map, set, atomic). +// +// Possible data-structure types of a list are: +// +// - "map": it needs to have a key field, which will be used to build an +// associative list. A typical example is a the pod container list, +// which is indexed by the container name. +// +// - "set": Fields need to be "scalar", and there can be only one +// occurrence of each. +// +// - "atomic": All the fields in the list are treated as a single value, +// are typically manipulated together by the same actor. +type ListType string + +// +controllertools:marker:generateHelp:category="CRD processing" + +// ListMapKey specifies the keys to map listTypes. +// +// It indicates the index of a map list. They can be repeated if multiple keys +// must be used. It can only be used when ListType is set to map, and the keys +// should be scalar types. +type ListMapKey string + +// +controllertools:marker:generateHelp:category="CRD processing" + +// MapType specifies the level of atomicity of the map; +// i.e. whether each item in the map is independent of the others, +// or all fields are treated as a single unit. +// +// Possible values: +// +// - "granular": items in the map are independent of each other, +// and can be manipulated by different actors. +// This is the default behavior. +// +// - "atomic": all fields are treated as one unit. +// Any changes have to replace the entire map. +type MapType string + +// +controllertools:marker:generateHelp:category="CRD processing" + +// StructType specifies the level of atomicity of the struct; +// i.e. whether each field in the struct is independent of the others, +// or all fields are treated as a single unit. +// +// Possible values: +// +// - "granular": fields in the struct are independent of each other, +// and can be manipulated by different actors. +// This is the default behavior. +// +// - "atomic": all fields are treated as one unit. +// Any changes have to replace the entire struct. +type StructType string + +func (l ListType) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "array" { + return fmt.Errorf("must apply listType to an array, found %s", schema.Type) + } + if l != "map" && l != "atomic" && l != "set" { + return fmt.Errorf(`ListType must be either "map", "set" or "atomic"`) + } + p := string(l) + schema.XListType = &p + return nil +} + +func (l ListType) ApplyFirst() {} + +func (l ListMapKey) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "array" { + return fmt.Errorf("must apply listMapKey to an array, found %s", schema.Type) + } + if schema.XListType == nil || *schema.XListType != "map" { + return fmt.Errorf("must apply listMapKey to an associative-list") + } + schema.XListMapKeys = append(schema.XListMapKeys, string(l)) + return nil +} + +func (m MapType) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "object" { + return fmt.Errorf("must apply mapType to an object") + } + + if m != "atomic" && m != "granular" { + return fmt.Errorf(`MapType must be either "granular" or "atomic"`) + } + + p := string(m) + schema.XMapType = &p + + return nil +} + +func (s StructType) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "object" && schema.Type != "" { + return fmt.Errorf("must apply structType to an object; either explicitly set or defaulted through an empty schema type") + } + + if s != "atomic" && s != "granular" { + return fmt.Errorf(`StructType must be either "granular" or "atomic"`) + } + + p := string(s) + schema.XMapType = &p + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/validation.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/validation.go new file mode 100644 index 0000000000..67522c9d75 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/validation.go @@ -0,0 +1,520 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "encoding/json" + "fmt" + "math" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +const ( + SchemalessName = "kubebuilder:validation:Schemaless" +) + +// ValidationMarkers lists all available markers that affect CRD schema generation, +// except for the few that don't make sense as type-level markers (see FieldOnlyMarkers). +// All markers start with `+kubebuilder:validation:`, and continue with their type name. +// A copy is produced of all markers that describes types as well, for making types +// reusable and writing complex validations on slice items. +var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField, + + // numeric markers + + Maximum(0), + Minimum(0), + ExclusiveMaximum(false), + ExclusiveMinimum(false), + MultipleOf(0), + MinProperties(0), + MaxProperties(0), + + // string markers + + MaxLength(0), + MinLength(0), + Pattern(""), + + // slice markers + + MaxItems(0), + MinItems(0), + UniqueItems(false), + + // general markers + + Enum(nil), + Format(""), + Type(""), + XPreserveUnknownFields{}, + XEmbeddedResource{}, + XIntOrString{}, + XValidation{}, +) + +// FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make +// sense on a type, and thus aren't in ValidationMarkers). +var FieldOnlyMarkers = []*definitionWithHelp{ + must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})). + WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")), + must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})). + WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), + must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})). + WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), + + must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})). + WithHelp(Nullable{}.Help()), + + must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})). + WithHelp(Default{}.Help()), + + must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})). + WithHelp(Example{}.Help()), + + must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})). + WithHelp(XEmbeddedResource{}.Help()), + + must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})). + WithHelp(Schemaless{}.Help()), +} + +// ValidationIshMarkers are field-and-type markers that don't fall under the +// :validation: prefix, and/or don't have a name that directly matches their +// type. +var ValidationIshMarkers = []*definitionWithHelp{ + must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})). + WithHelp(XPreserveUnknownFields{}.Help()), + must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})). + WithHelp(XPreserveUnknownFields{}.Help()), +} + +func init() { + AllDefinitions = append(AllDefinitions, ValidationMarkers...) + + for _, def := range ValidationMarkers { + newDef := *def.Definition + // copy both parts so we don't change the definition + typDef := definitionWithHelp{ + Definition: &newDef, + Help: def.Help, + } + typDef.Target = markers.DescribesType + AllDefinitions = append(AllDefinitions, &typDef) + } + + AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...) + AllDefinitions = append(AllDefinitions, ValidationIshMarkers...) +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Maximum specifies the maximum numeric value that this field can have. +type Maximum float64 + +func (m Maximum) Value() float64 { + return float64(m) +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported. +type Minimum float64 + +func (m Minimum) Value() float64 { + return float64(m) +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// ExclusiveMinimum indicates that the minimum is "up to" but not including that value. +type ExclusiveMinimum bool + +// +controllertools:marker:generateHelp:category="CRD validation" +// ExclusiveMaximum indicates that the maximum is "up to" but not including that value. +type ExclusiveMaximum bool + +// +controllertools:marker:generateHelp:category="CRD validation" +// MultipleOf specifies that this field must have a numeric value that's a multiple of this one. +type MultipleOf float64 + +func (m MultipleOf) Value() float64 { + return float64(m) +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// MaxLength specifies the maximum length for this string. +type MaxLength int + +// +controllertools:marker:generateHelp:category="CRD validation" +// MinLength specifies the minimum length for this string. +type MinLength int + +// +controllertools:marker:generateHelp:category="CRD validation" +// Pattern specifies that this string must match the given regular expression. +type Pattern string + +// +controllertools:marker:generateHelp:category="CRD validation" +// MaxItems specifies the maximum length for this list. +type MaxItems int + +// +controllertools:marker:generateHelp:category="CRD validation" +// MinItems specifies the minimun length for this list. +type MinItems int + +// +controllertools:marker:generateHelp:category="CRD validation" +// UniqueItems specifies that all items in this list must be unique. +type UniqueItems bool + +// +controllertools:marker:generateHelp:category="CRD validation" +// MaxProperties restricts the number of keys in an object +type MaxProperties int + +// +controllertools:marker:generateHelp:category="CRD validation" +// MinProperties restricts the number of keys in an object +type MinProperties int + +// +controllertools:marker:generateHelp:category="CRD validation" +// Enum specifies that this (scalar) field is restricted to the *exact* values specified here. +type Enum []interface{} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Format specifies additional "complex" formatting for this field. +// +// For example, a date-time field would be marked as "type: string" and +// "format: date-time". +type Format string + +// +controllertools:marker:generateHelp:category="CRD validation" +// Type overrides the type for this field (which defaults to the equivalent of the Go type). +// +// This generally must be paired with custom serialization. For example, the +// metav1.Time field would be marked as "type: string" and "format: date-time". +type Type string + +// +controllertools:marker:generateHelp:category="CRD validation" +// Nullable marks this field as allowing the "null" value. +// +// This is often not necessary, but may be helpful with custom serialization. +type Nullable struct{} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Default sets the default value for this field. +// +// A default value will be accepted as any value valid for the +// field. Formatting for common types include: boolean: `true`, string: +// `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: +// "delete"}`). Defaults should be defined in pruned form, and only best-effort +// validation will be performed. Full validation of a default requires +// submission of the containing CRD to an apiserver. +type Default struct { + Value interface{} +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Example sets the example value for this field. +// +// An example value will be accepted as any value valid for the +// field. Formatting for common types include: boolean: `true`, string: +// `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: +// "delete"}`). Examples should be defined in pruned form, and only best-effort +// validation will be performed. Full validation of an example requires +// submission of the containing CRD to an apiserver. +type Example struct { + Value interface{} +} + +// +controllertools:marker:generateHelp:category="CRD processing" +// PreserveUnknownFields stops the apiserver from pruning fields which are not specified. +// +// By default the apiserver drops unknown fields from the request payload +// during the decoding step. This marker stops the API server from doing so. +// It affects fields recursively, but switches back to normal pruning behaviour +// if nested properties or additionalProperties are specified in the schema. +// This can either be true or undefined. False +// is forbidden. +// +// NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated +// in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function +// identically. +type XPreserveUnknownFields struct{} + +// +controllertools:marker:generateHelp:category="CRD validation" +// EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. +// +// An embedded resource is a value that has apiVersion, kind and metadata fields. +// They are validated implicitly according to the semantics of the currently +// running apiserver. It is not necessary to add any additional schema for these +// field, yet it is possible. This can be combined with PreserveUnknownFields. +type XEmbeddedResource struct{} + +// +controllertools:marker:generateHelp:category="CRD validation" +// IntOrString marks a fields as an IntOrString. +// +// This is required when applying patterns or other validations to an IntOrString +// field. Knwon information about the type is applied during the collapse phase +// and as such is not normally available during marker application. +type XIntOrString struct{} + +// +controllertools:marker:generateHelp:category="CRD validation" +// Schemaless marks a field as being a schemaless object. +// +// Schemaless objects are not introspected, so you must provide +// any type and validation information yourself. One use for this +// tag is for embedding fields that hold JSONSchema typed objects. +// Because this field disables all type checking, it is recommended +// to be used only as a last resort. +type Schemaless struct{} + +func hasNumericType(schema *apiext.JSONSchemaProps) bool { + return schema.Type == "integer" || schema.Type == "number" +} + +func isIntegral(value float64) bool { + return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0) +} + +// +controllertools:marker:generateHelp:category="CRD validation" +// XValidation marks a field as requiring a value for which a given +// expression evaluates to true. +// +// This marker may be repeated to specify multiple expressions, all of +// which must evaluate to true. +type XValidation struct { + Rule string + Message string `marker:",optional"` +} + +func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if !hasNumericType(schema) { + return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type) + } + + if schema.Type == "integer" && !isIntegral(m.Value()) { + return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value()) + } + + val := m.Value() + schema.Maximum = &val + return nil +} + +func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if !hasNumericType(schema) { + return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type) + } + + if schema.Type == "integer" && !isIntegral(m.Value()) { + return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value()) + } + + val := m.Value() + schema.Minimum = &val + return nil +} + +func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if !hasNumericType(schema) { + return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type) + } + schema.ExclusiveMaximum = bool(m) + return nil +} + +func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if !hasNumericType(schema) { + return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type) + } + + schema.ExclusiveMinimum = bool(m) + return nil +} + +func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if !hasNumericType(schema) { + return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type) + } + + if schema.Type == "integer" && !isIntegral(m.Value()) { + return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value()) + } + + val := m.Value() + schema.MultipleOf = &val + return nil +} + +func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "string" { + return fmt.Errorf("must apply maxlength to a string") + } + val := int64(m) + schema.MaxLength = &val + return nil +} + +func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "string" { + return fmt.Errorf("must apply minlength to a string") + } + val := int64(m) + schema.MinLength = &val + return nil +} + +func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + // Allow string types or IntOrStrings. An IntOrString will still + // apply the pattern validation when a string is detected, the pattern + // will not apply to ints though. + if schema.Type != "string" && !schema.XIntOrString { + return fmt.Errorf("must apply pattern to a `string` or `IntOrString`") + } + schema.Pattern = string(m) + return nil +} + +func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "array" { + return fmt.Errorf("must apply maxitem to an array") + } + val := int64(m) + schema.MaxItems = &val + return nil +} + +func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "array" { + return fmt.Errorf("must apply minitems to an array") + } + val := int64(m) + schema.MinItems = &val + return nil +} + +func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "array" { + return fmt.Errorf("must apply uniqueitems to an array") + } + schema.UniqueItems = bool(m) + return nil +} + +func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "object" { + return fmt.Errorf("must apply minproperties to an object") + } + val := int64(m) + schema.MinProperties = &val + return nil +} + +func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + if schema.Type != "object" { + return fmt.Errorf("must apply maxproperties to an object") + } + val := int64(m) + schema.MaxProperties = &val + return nil +} + +func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + // TODO(directxman12): this is a bit hacky -- we should + // probably support AnyType better + using the schema structure + vals := make([]apiext.JSON, len(m)) + for i, val := range m { + // TODO(directxman12): check actual type with schema type? + // if we're expecting a string, marshal the string properly... + // NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly + valMarshalled, err := json.Marshal(val) + if err != nil { + return err + } + vals[i] = apiext.JSON{Raw: valMarshalled} + } + schema.Enum = vals + return nil +} + +func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.Format = string(m) + return nil +} + +// NB(directxman12): we "typecheck" on target schema properties here, +// which means the "Type" marker *must* be applied first. +// TODO(directxman12): find a less hacky way to do this +// (we could preserve ordering of markers, but that feels bad in its own right). + +func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.Type = string(m) + return nil +} + +func (m Type) ApplyFirst() {} + +func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.Nullable = true + return nil +} + +// Defaults are only valid CRDs created with the v1 API +func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + marshalledDefault, err := json.Marshal(m.Value) + if err != nil { + return err + } + schema.Default = &apiext.JSON{Raw: marshalledDefault} + return nil +} + +func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + marshalledExample, err := json.Marshal(m.Value) + if err != nil { + return err + } + schema.Example = &apiext.JSON{Raw: marshalledExample} + return nil +} + +func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + defTrue := true + schema.XPreserveUnknownFields = &defTrue + return nil +} + +func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.XEmbeddedResource = true + return nil +} + +// NB(JoelSpeed): we use this property in other markers here, +// which means the "XIntOrString" marker *must* be applied first. + +func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.XIntOrString = true + return nil +} + +func (m XIntOrString) ApplyFirst() {} + +func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error { + schema.XValidations = append(schema.XValidations, apiext.ValidationRule{ + Rule: m.Rule, + Message: m.Message, + }) + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/zz_generated.markerhelp.go new file mode 100644 index 0000000000..1884bc35ff --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/markers/zz_generated.markerhelp.go @@ -0,0 +1,525 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package markers + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Default) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "sets the default value for this field. ", + Details: "A default value will be accepted as any value valid for the field. Formatting for common types include: boolean: `true`, string: `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: \"delete\"}`). Defaults should be defined in pruned form, and only best-effort validation will be performed. Full validation of a default requires submission of the containing CRD to an apiserver.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Value": { + Summary: "", + Details: "", + }, + }, + } +} + +func (DeprecatedVersion) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "marks this version as deprecated.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Warning": { + Summary: "message to be shown on the deprecated version", + Details: "", + }, + }, + } +} + +func (Enum) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies that this (scalar) field is restricted to the *exact* values specified here.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Example) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "sets the example value for this field. ", + Details: "An example value will be accepted as any value valid for the field. Formatting for common types include: boolean: `true`, string: `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: \"delete\"}`). Examples should be defined in pruned form, and only best-effort validation will be performed. Full validation of an example requires submission of the containing CRD to an apiserver.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Value": { + Summary: "", + Details: "", + }, + }, + } +} + +func (ExclusiveMaximum) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "indicates that the maximum is \"up to\" but not including that value.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (ExclusiveMinimum) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "indicates that the minimum is \"up to\" but not including that value.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Format) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies additional \"complex\" formatting for this field. ", + Details: "For example, a date-time field would be marked as \"type: string\" and \"format: date-time\".", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (ListMapKey) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD processing", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the keys to map listTypes. ", + Details: "It indicates the index of a map list. They can be repeated if multiple keys must be used. It can only be used when ListType is set to map, and the keys should be scalar types.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (ListType) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD processing", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the type of data-structure that the list represents (map, set, atomic). ", + Details: "Possible data-structure types of a list are: \n - \"map\": it needs to have a key field, which will be used to build an associative list. A typical example is a the pod container list, which is indexed by the container name. \n - \"set\": Fields need to be \"scalar\", and there can be only one occurrence of each. \n - \"atomic\": All the fields in the list are treated as a single value, are typically manipulated together by the same actor.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MapType) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD processing", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the level of atomicity of the map; i.e. whether each item in the map is independent of the others, or all fields are treated as a single unit. ", + Details: "Possible values: \n - \"granular\": items in the map are independent of each other, and can be manipulated by different actors. This is the default behavior. \n - \"atomic\": all fields are treated as one unit. Any changes have to replace the entire map.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MaxItems) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the maximum length for this list.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MaxLength) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the maximum length for this string.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MaxProperties) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "restricts the number of keys in an object", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Maximum) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the maximum numeric value that this field can have.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Metadata) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "configures the additional annotations or labels for this CRD. For example adding annotation \"api-approved.kubernetes.io\" for a CRD with Kubernetes groups, or annotation \"cert-manager.io/inject-ca-from-secret\" for a CRD that needs CA injection.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Annotations": { + Summary: "will be added into the annotations of this CRD.", + Details: "", + }, + "Labels": { + Summary: "will be added into the labels of this CRD.", + Details: "", + }, + }, + } +} + +func (MinItems) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the minimun length for this list.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MinLength) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the minimum length for this string.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MinProperties) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "restricts the number of keys in an object", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Minimum) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the minimum numeric value that this field can have. Negative numbers are supported.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (MultipleOf) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies that this field must have a numeric value that's a multiple of this one.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Nullable) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "marks this field as allowing the \"null\" value. ", + Details: "This is often not necessary, but may be helpful with custom serialization.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Pattern) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies that this string must match the given regular expression.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (PrintColumn) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "adds a column to \"kubectl get\" output for this CRD.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Name": { + Summary: "specifies the name of the column.", + Details: "", + }, + "Type": { + Summary: "indicates the type of the column. ", + Details: "It may be any OpenAPI data type listed at https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.", + }, + "JSONPath": { + Summary: "specifies the jsonpath expression used to extract the value of the column.", + Details: "", + }, + "Description": { + Summary: "specifies the help/description for this column.", + Details: "", + }, + "Format": { + Summary: "specifies the format of the column. ", + Details: "It may be any OpenAPI data format corresponding to the type, listed at https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.", + }, + "Priority": { + Summary: "indicates how important it is that this column be displayed. ", + Details: "Lower priority (*higher* numbered) columns will be hidden if the terminal width is too small.", + }, + }, + } +} + +func (Resource) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "configures naming and scope for a CRD.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Path": { + Summary: "specifies the plural \"resource\" for this CRD. ", + Details: "It generally corresponds to a plural, lower-cased version of the Kind. See https://book.kubebuilder.io/cronjob-tutorial/gvks.html.", + }, + "ShortName": { + Summary: "specifies aliases for this CRD. ", + Details: "Short names are often used when people have work with your resource over and over again. For instance, \"rs\" for \"replicaset\" or \"crd\" for customresourcedefinition.", + }, + "Categories": { + Summary: "specifies which group aliases this resource is part of. ", + Details: "Group aliases are used to work with groups of resources at once. The most common one is \"all\" which covers about a third of the base resources in Kubernetes, and is generally used for \"user-facing\" resources.", + }, + "Singular": { + Summary: "overrides the singular form of your resource. ", + Details: "The singular form is otherwise defaulted off the plural (path).", + }, + "Scope": { + Summary: "overrides the scope of the CRD (Cluster vs Namespaced). ", + Details: "Scope defaults to \"Namespaced\". Cluster-scoped (\"Cluster\") resources don't exist in namespaces.", + }, + }, + } +} + +func (Schemaless) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "marks a field as being a schemaless object. ", + Details: "Schemaless objects are not introspected, so you must provide any type and validation information yourself. One use for this tag is for embedding fields that hold JSONSchema typed objects. Because this field disables all type checking, it is recommended to be used only as a last resort.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (SkipVersion) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "removes the particular version of the CRD from the CRDs spec. ", + Details: "This is useful if you need to skip generating and listing version entries for 'internal' resource versions, which typically exist if using the Kubernetes upstream conversion-gen tool.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (StorageVersion) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "marks this version as the \"storage version\" for the CRD for conversion. ", + Details: "When conversion is enabled for a CRD (i.e. it's not a trivial-versions/single-version CRD), one version is set as the \"storage version\" to be stored in etcd. Attempting to store any other version will result in conversion to the storage version via a conversion webhook.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (StructType) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD processing", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies the level of atomicity of the struct; i.e. whether each field in the struct is independent of the others, or all fields are treated as a single unit. ", + Details: "Possible values: \n - \"granular\": fields in the struct are independent of each other, and can be manipulated by different actors. This is the default behavior. \n - \"atomic\": all fields are treated as one unit. Any changes have to replace the entire struct.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (SubresourceScale) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "enables the \"/scale\" subresource on a CRD.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "SpecPath": { + Summary: "specifies the jsonpath to the replicas field for the scale's spec.", + Details: "", + }, + "StatusPath": { + Summary: "specifies the jsonpath to the replicas field for the scale's status.", + Details: "", + }, + "SelectorPath": { + Summary: "specifies the jsonpath to the pod label selector field for the scale's status. ", + Details: "The selector field must be the *string* form (serialized form) of a selector. Setting a pod label selector is necessary for your type to work with the HorizontalPodAutoscaler.", + }, + }, + } +} + +func (SubresourceStatus) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "enables the \"/status\" subresource on a CRD.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (Type) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "overrides the type for this field (which defaults to the equivalent of the Go type). ", + Details: "This generally must be paired with custom serialization. For example, the metav1.Time field would be marked as \"type: string\" and \"format: date-time\".", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (UniqueItems) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies that all items in this list must be unique.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (UnservedVersion) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD", + DetailedHelp: markers.DetailedHelp{ + Summary: "does not serve this version. ", + Details: "This is useful if you need to drop support for a version in favor of a newer version.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (XEmbeddedResource) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. ", + Details: "An embedded resource is a value that has apiVersion, kind and metadata fields. They are validated implicitly according to the semantics of the currently running apiserver. It is not necessary to add any additional schema for these field, yet it is possible. This can be combined with PreserveUnknownFields.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (XIntOrString) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "IntOrString marks a fields as an IntOrString. ", + Details: "This is required when applying patterns or other validations to an IntOrString field. Knwon information about the type is applied during the collapse phase and as such is not normally available during marker application.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (XPreserveUnknownFields) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD processing", + DetailedHelp: markers.DetailedHelp{ + Summary: "PreserveUnknownFields stops the apiserver from pruning fields which are not specified. ", + Details: "By default the apiserver drops unknown fields from the request payload during the decoding step. This marker stops the API server from doing so. It affects fields recursively, but switches back to normal pruning behaviour if nested properties or additionalProperties are specified in the schema. This can either be true or undefined. False is forbidden. \n NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function identically.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (XValidation) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "CRD validation", + DetailedHelp: markers.DetailedHelp{ + Summary: "marks a field as requiring a value for which a given expression evaluates to true. ", + Details: "This marker may be repeated to specify multiple expressions, all of which must evaluate to true.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Rule": { + Summary: "", + Details: "", + }, + "Message": { + Summary: "", + Details: "", + }, + }, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/parser.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/parser.go new file mode 100644 index 0000000000..4c15de9072 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/parser.go @@ -0,0 +1,243 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + "fmt" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// TypeIdent represents some type in a Package. +type TypeIdent struct { + Package *loader.Package + Name string +} + +func (t TypeIdent) String() string { + return fmt.Sprintf("%q.%s", t.Package.ID, t.Name) +} + +// PackageOverride overrides the loading of some package +// (potentially setting custom schemata, etc). It must +// call AddPackage if it wants to continue with the default +// loading behavior. +type PackageOverride func(p *Parser, pkg *loader.Package) + +// Parser knows how to parse out CRD information and generate +// OpenAPI schemata from some collection of types and markers. +// Most methods on Parser cache their results automatically, +// and thus may be called any number of times. +type Parser struct { + Collector *markers.Collector + + // Types contains the known TypeInfo for this parser. + Types map[TypeIdent]*markers.TypeInfo + // Schemata contains the known OpenAPI JSONSchemata for this parser. + Schemata map[TypeIdent]apiext.JSONSchemaProps + // GroupVersions contains the known group-versions of each package in this parser. + GroupVersions map[*loader.Package]schema.GroupVersion + // CustomResourceDefinitions contains the known CustomResourceDefinitions for types in this parser. + CustomResourceDefinitions map[schema.GroupKind]apiext.CustomResourceDefinition + // FlattenedSchemata contains fully flattened schemata for use in building + // CustomResourceDefinition validation. Each schema has been flattened by the flattener, + // and then embedded fields have been flattened with FlattenEmbedded. + FlattenedSchemata map[TypeIdent]apiext.JSONSchemaProps + + // PackageOverrides indicates that the loading of any package with + // the given path should be handled by the given overrider. + PackageOverrides map[string]PackageOverride + + // checker stores persistent partial type-checking/reference-traversal information. + Checker *loader.TypeChecker + // packages marks packages as loaded, to avoid re-loading them. + packages map[*loader.Package]struct{} + + flattener *Flattener + + // AllowDangerousTypes controls the handling of non-recommended types such as float. If + // false (the default), these types are not supported. + // There is a continuum here: + // 1. Types that are always supported. + // 2. Types that are allowed by default, but not recommended (warning emitted when they are encountered as per PR #443). + // Possibly they are allowed by default for historical reasons and may even be "on their way out" at some point in the future. + // 3. Types that are not allowed by default, not recommended, but there are some legitimate reasons to need them in certain corner cases. + // Possibly these types should also emit a warning as per PR #443 even when they are "switched on" (an integration point between + // this feature and #443 if desired). This is the category that this flag deals with. + // 4. Types that are not allowed and will not be allowed, possibly because it just "doesn't make sense" or possibly + // because the implementation is too difficult/clunky to promote them to category 3. + // TODO: Should we have a more formal mechanism for putting "type patterns" in each of the above categories? + AllowDangerousTypes bool + + // IgnoreUnexportedFields specifies if unexported fields on the struct should be skipped + IgnoreUnexportedFields bool + + // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta should be generated + GenerateEmbeddedObjectMeta bool +} + +func (p *Parser) init() { + if p.packages == nil { + p.packages = make(map[*loader.Package]struct{}) + } + if p.flattener == nil { + p.flattener = &Flattener{ + Parser: p, + } + } + if p.Schemata == nil { + p.Schemata = make(map[TypeIdent]apiext.JSONSchemaProps) + } + if p.Types == nil { + p.Types = make(map[TypeIdent]*markers.TypeInfo) + } + if p.PackageOverrides == nil { + p.PackageOverrides = make(map[string]PackageOverride) + } + if p.GroupVersions == nil { + p.GroupVersions = make(map[*loader.Package]schema.GroupVersion) + } + if p.CustomResourceDefinitions == nil { + p.CustomResourceDefinitions = make(map[schema.GroupKind]apiext.CustomResourceDefinition) + } + if p.FlattenedSchemata == nil { + p.FlattenedSchemata = make(map[TypeIdent]apiext.JSONSchemaProps) + } +} + +// indexTypes loads all types in the package into Types. +func (p *Parser) indexTypes(pkg *loader.Package) { + // autodetect + pkgMarkers, err := markers.PackageMarkers(p.Collector, pkg) + if err != nil { + pkg.AddError(err) + } else { + if skipPkg := pkgMarkers.Get("kubebuilder:skip"); skipPkg != nil { + return + } + if nameVal := pkgMarkers.Get("groupName"); nameVal != nil { + versionVal := pkg.Name // a reasonable guess + if versionMarker := pkgMarkers.Get("versionName"); versionMarker != nil { + versionVal = versionMarker.(string) + } + + p.GroupVersions[pkg] = schema.GroupVersion{ + Version: versionVal, + Group: nameVal.(string), + } + } + } + + if err := markers.EachType(p.Collector, pkg, func(info *markers.TypeInfo) { + ident := TypeIdent{ + Package: pkg, + Name: info.Name, + } + + p.Types[ident] = info + }); err != nil { + pkg.AddError(err) + } +} + +// LookupType fetches type info from Types. +func (p *Parser) LookupType(pkg *loader.Package, name string) *markers.TypeInfo { + return p.Types[TypeIdent{Package: pkg, Name: name}] +} + +// NeedSchemaFor indicates that a schema should be generated for the given type. +func (p *Parser) NeedSchemaFor(typ TypeIdent) { + p.init() + + p.NeedPackage(typ.Package) + if _, knownSchema := p.Schemata[typ]; knownSchema { + return + } + + info, knownInfo := p.Types[typ] + if !knownInfo { + typ.Package.AddError(fmt.Errorf("unknown type %s", typ)) + return + } + + // avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema + p.Schemata[typ] = apiext.JSONSchemaProps{} + + schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes, p.IgnoreUnexportedFields) + ctxForInfo := schemaCtx.ForInfo(info) + + pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package) + if err != nil { + typ.Package.AddError(err) + } + ctxForInfo.PackageMarkers = pkgMarkers + + schema := infoToSchema(ctxForInfo) + + p.Schemata[typ] = *schema +} + +func (p *Parser) NeedFlattenedSchemaFor(typ TypeIdent) { + p.init() + + if _, knownSchema := p.FlattenedSchemata[typ]; knownSchema { + return + } + + p.NeedSchemaFor(typ) + partialFlattened := p.flattener.FlattenType(typ) + fullyFlattened := FlattenEmbedded(partialFlattened, typ.Package) + + p.FlattenedSchemata[typ] = *fullyFlattened +} + +// NeedCRDFor lives off in spec.go + +// AddPackage indicates that types and type-checking information is needed +// for the the given package, *ignoring* overrides. +// Generally, consumers should call NeedPackage, while PackageOverrides should +// call AddPackage to continue with the normal loading procedure. +func (p *Parser) AddPackage(pkg *loader.Package) { + p.init() + if _, checked := p.packages[pkg]; checked { + return + } + p.indexTypes(pkg) + p.Checker.Check(pkg) + p.packages[pkg] = struct{}{} +} + +// NeedPackage indicates that types and type-checking information +// is needed for the given package. +func (p *Parser) NeedPackage(pkg *loader.Package) { + p.init() + if _, checked := p.packages[pkg]; checked { + return + } + // overrides are going to be written without vendor. This is why we index by the actual + // object when we can. + if override, overridden := p.PackageOverrides[loader.NonVendorPath(pkg.PkgPath)]; overridden { + override(p, pkg) + p.packages[pkg] = struct{}{} + return + } + p.AddPackage(pkg) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema.go new file mode 100644 index 0000000000..e76d3ea88c --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema.go @@ -0,0 +1,464 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "go/types" + "strings" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers" + + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// Schema flattening is done in a recursive mapping method. +// Start reading at infoToSchema. + +const ( + // defPrefix is the prefix used to link to definitions in the OpenAPI schema. + defPrefix = "#/definitions/" +) + +// byteType is the types.Type for byte (see the types documention +// for why we need to look this up in the Universe), saved +// for quick comparison. +var byteType = types.Universe.Lookup("byte").Type() + +// SchemaMarker is any marker that needs to modify the schema of the underlying type or field. +type SchemaMarker interface { + // ApplyToSchema is called after the rest of the schema for a given type + // or field is generated, to modify the schema appropriately. + ApplyToSchema(*apiext.JSONSchemaProps) error +} + +// applyFirstMarker is applied before any other markers. It's a bit of a hack. +type applyFirstMarker interface { + ApplyFirst() +} + +// schemaRequester knows how to marker that another schema (e.g. via an external reference) is necessary. +type schemaRequester interface { + NeedSchemaFor(typ TypeIdent) +} + +// schemaContext stores and provides information across a hierarchy of schema generation. +type schemaContext struct { + pkg *loader.Package + info *markers.TypeInfo + + schemaRequester schemaRequester + PackageMarkers markers.MarkerValues + + allowDangerousTypes bool + ignoreUnexportedFields bool +} + +// newSchemaContext constructs a new schemaContext for the given package and schema requester. +// It must have type info added before use via ForInfo. +func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes, ignoreUnexportedFields bool) *schemaContext { + pkg.NeedTypesInfo() + return &schemaContext{ + pkg: pkg, + schemaRequester: req, + allowDangerousTypes: allowDangerousTypes, + ignoreUnexportedFields: ignoreUnexportedFields, + } +} + +// ForInfo produces a new schemaContext with containing the same information +// as this one, except with the given type information. +func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext { + return &schemaContext{ + pkg: c.pkg, + info: info, + schemaRequester: c.schemaRequester, + allowDangerousTypes: c.allowDangerousTypes, + ignoreUnexportedFields: c.ignoreUnexportedFields, + } +} + +// requestSchema asks for the schema for a type in the package with the +// given import path. +func (c *schemaContext) requestSchema(pkgPath, typeName string) { + pkg := c.pkg + if pkgPath != "" { + pkg = c.pkg.Imports()[pkgPath] + } + c.schemaRequester.NeedSchemaFor(TypeIdent{ + Package: pkg, + Name: typeName, + }) +} + +// infoToSchema creates a schema for the type in the given set of type information. +func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps { + // If the obj implements a JSON marshaler and has a marker, use the markers value and do not traverse as + // the marshaler could be doing anything. If there is no marker, fall back to traversing. + if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil && implementsJSONMarshaler(obj.Type()) { + schema := &apiext.JSONSchemaProps{} + applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type) + if schema.Type != "" { + return schema + } + } + return typeToSchema(ctx, ctx.info.RawSpec.Type) +} + +// applyMarkers applies schema markers to the given schema, respecting "apply first" markers. +func applyMarkers(ctx *schemaContext, markerSet markers.MarkerValues, props *apiext.JSONSchemaProps, node ast.Node) { + // apply "apply first" markers first... + for _, markerValues := range markerSet { + for _, markerValue := range markerValues { + if _, isApplyFirst := markerValue.(applyFirstMarker); !isApplyFirst { + continue + } + + schemaMarker, isSchemaMarker := markerValue.(SchemaMarker) + if !isSchemaMarker { + continue + } + + if err := schemaMarker.ApplyToSchema(props); err != nil { + ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node)) + } + } + } + + // ...then the rest of the markers + for _, markerValues := range markerSet { + for _, markerValue := range markerValues { + if _, isApplyFirst := markerValue.(applyFirstMarker); isApplyFirst { + // skip apply-first markers, which were already applied + continue + } + + schemaMarker, isSchemaMarker := markerValue.(SchemaMarker) + if !isSchemaMarker { + continue + } + if err := schemaMarker.ApplyToSchema(props); err != nil { + ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node)) + } + } + } +} + +// typeToSchema creates a schema for the given AST type. +func typeToSchema(ctx *schemaContext, rawType ast.Expr) *apiext.JSONSchemaProps { + var props *apiext.JSONSchemaProps + switch expr := rawType.(type) { + case *ast.Ident: + props = localNamedToSchema(ctx, expr) + case *ast.SelectorExpr: + props = namedToSchema(ctx, expr) + case *ast.ArrayType: + props = arrayToSchema(ctx, expr) + case *ast.MapType: + props = mapToSchema(ctx, expr) + case *ast.StarExpr: + props = typeToSchema(ctx, expr.X) + case *ast.StructType: + props = structToSchema(ctx, expr) + default: + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unsupported AST kind %T", expr), rawType)) + // NB(directxman12): we explicitly don't handle interfaces + return &apiext.JSONSchemaProps{} + } + + props.Description = ctx.info.Doc + + applyMarkers(ctx, ctx.info.Markers, props, rawType) + + return props +} + +// qualifiedName constructs a JSONSchema-safe qualified name for a type +// (`` or `~0`, where `` +// is the package path with `/` replaced by `~1`, according to JSONPointer +// escapes). +func qualifiedName(pkgName, typeName string) string { + if pkgName != "" { + return strings.Replace(pkgName, "/", "~1", -1) + "~0" + typeName + } + return typeName +} + +// TypeRefLink creates a definition link for the given type and package. +func TypeRefLink(pkgName, typeName string) string { + return defPrefix + qualifiedName(pkgName, typeName) +} + +// localNamedToSchema creates a schema (ref) for a *potentially* local type reference +// (could be external from a dot-import). +func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchemaProps { + typeInfo := ctx.pkg.TypesInfo.TypeOf(ident) + if typeInfo == types.Typ[types.Invalid] { + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown type %s", ident.Name), ident)) + return &apiext.JSONSchemaProps{} + } + if basicInfo, isBasic := typeInfo.(*types.Basic); isBasic { + typ, fmt, err := builtinToType(basicInfo, ctx.allowDangerousTypes) + if err != nil { + ctx.pkg.AddError(loader.ErrFromNode(err, ident)) + } + return &apiext.JSONSchemaProps{ + Type: typ, + Format: fmt, + } + } + // NB(directxman12): if there are dot imports, this might be an external reference, + // so use typechecking info to get the actual object + typeNameInfo := typeInfo.(*types.Named).Obj() + pkg := typeNameInfo.Pkg() + pkgPath := loader.NonVendorPath(pkg.Path()) + if pkg == ctx.pkg.Types { + pkgPath = "" + } + ctx.requestSchema(pkgPath, typeNameInfo.Name()) + link := TypeRefLink(pkgPath, typeNameInfo.Name()) + return &apiext.JSONSchemaProps{ + Ref: &link, + } +} + +// namedSchema creates a schema (ref) for an explicitly external type reference. +func namedToSchema(ctx *schemaContext, named *ast.SelectorExpr) *apiext.JSONSchemaProps { + typeInfoRaw := ctx.pkg.TypesInfo.TypeOf(named) + if typeInfoRaw == types.Typ[types.Invalid] { + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown type %v.%s", named.X, named.Sel.Name), named)) + return &apiext.JSONSchemaProps{} + } + typeInfo := typeInfoRaw.(*types.Named) + typeNameInfo := typeInfo.Obj() + nonVendorPath := loader.NonVendorPath(typeNameInfo.Pkg().Path()) + ctx.requestSchema(nonVendorPath, typeNameInfo.Name()) + link := TypeRefLink(nonVendorPath, typeNameInfo.Name()) + return &apiext.JSONSchemaProps{ + Ref: &link, + } + // NB(directxman12): we special-case things like resource.Quantity during the "collapse" phase. +} + +// arrayToSchema creates a schema for the items of the given array, dealing appropriately +// with the special `[]byte` type (according to OpenAPI standards). +func arrayToSchema(ctx *schemaContext, array *ast.ArrayType) *apiext.JSONSchemaProps { + eltType := ctx.pkg.TypesInfo.TypeOf(array.Elt) + if eltType == byteType && array.Len == nil { + // byte slices are represented as base64-encoded strings + // (the format is defined in OpenAPI v3, but not JSON Schema) + return &apiext.JSONSchemaProps{ + Type: "string", + Format: "byte", + } + } + // TODO(directxman12): backwards-compat would require access to markers from base info + items := typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), array.Elt) + + return &apiext.JSONSchemaProps{ + Type: "array", + Items: &apiext.JSONSchemaPropsOrArray{Schema: items}, + } +} + +// mapToSchema creates a schema for items of the given map. Key types must eventually resolve +// to string (other types aren't allowed by JSON, and thus the kubernetes API standards). +func mapToSchema(ctx *schemaContext, mapType *ast.MapType) *apiext.JSONSchemaProps { + keyInfo := ctx.pkg.TypesInfo.TypeOf(mapType.Key) + // check that we've got a type that actually corresponds to a string + for keyInfo != nil { + switch typedKey := keyInfo.(type) { + case *types.Basic: + if typedKey.Info()&types.IsString == 0 { + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map keys must be strings, not %s", keyInfo.String()), mapType.Key)) + return &apiext.JSONSchemaProps{} + } + keyInfo = nil // stop iterating + case *types.Named: + keyInfo = typedKey.Underlying() + default: + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map keys must be strings, not %s", keyInfo.String()), mapType.Key)) + return &apiext.JSONSchemaProps{} + } + } + + // TODO(directxman12): backwards-compat would require access to markers from base info + var valSchema *apiext.JSONSchemaProps + switch val := mapType.Value.(type) { + case *ast.Ident: + valSchema = localNamedToSchema(ctx.ForInfo(&markers.TypeInfo{}), val) + case *ast.SelectorExpr: + valSchema = namedToSchema(ctx.ForInfo(&markers.TypeInfo{}), val) + case *ast.ArrayType: + valSchema = arrayToSchema(ctx.ForInfo(&markers.TypeInfo{}), val) + case *ast.StarExpr: + valSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), val) + case *ast.MapType: + valSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), val) + default: + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("not a supported map value type: %T", mapType.Value), mapType.Value)) + return &apiext.JSONSchemaProps{} + } + + return &apiext.JSONSchemaProps{ + Type: "object", + AdditionalProperties: &apiext.JSONSchemaPropsOrBool{ + Schema: valSchema, + Allows: true, /* set automatically by serialization, but useful for testing */ + }, + } +} + +// structToSchema creates a schema for the given struct. Embedded fields are placed in AllOf, +// and can be flattened later with a Flattener. +func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSONSchemaProps { + props := &apiext.JSONSchemaProps{ + Type: "object", + Properties: make(map[string]apiext.JSONSchemaProps), + } + + if ctx.info.RawSpec.Type != structType { + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("encountered non-top-level struct (possibly embedded), those aren't allowed"), structType)) + return props + } + + for _, field := range ctx.info.Fields { + // Skip if the field is not an inline field, ignoreUnexportedFields is true, and the field is not exported + if field.Name != "" && ctx.ignoreUnexportedFields && !ast.IsExported(field.Name) { + continue + } + + jsonTag, hasTag := field.Tag.Lookup("json") + if !hasTag { + // if the field doesn't have a JSON tag, it doesn't belong in output (and shouldn't exist in a serialized type) + ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("encountered struct field %q without JSON tag in type %q", field.Name, ctx.info.Name), field.RawField)) + continue + } + jsonOpts := strings.Split(jsonTag, ",") + if len(jsonOpts) == 1 && jsonOpts[0] == "-" { + // skipped fields have the tag "-" (note that "-," means the field is named "-") + continue + } + + inline := false + omitEmpty := false + for _, opt := range jsonOpts[1:] { + switch opt { + case "inline": + inline = true + case "omitempty": + omitEmpty = true + } + } + fieldName := jsonOpts[0] + inline = inline || fieldName == "" // anonymous fields are inline fields in YAML/JSON + + // if no default required mode is set, default to required + defaultMode := "required" + if ctx.PackageMarkers.Get("kubebuilder:validation:Optional") != nil { + defaultMode = "optional" + } + + switch defaultMode { + // if this package isn't set to optional default... + case "required": + // ...everything that's not inline, omitempty, or explicitly optional is required + if !inline && !omitEmpty && field.Markers.Get("kubebuilder:validation:Optional") == nil && field.Markers.Get("optional") == nil { + props.Required = append(props.Required, fieldName) + } + + // if this package isn't set to required default... + case "optional": + // ...everything that isn't explicitly required is optional + if field.Markers.Get("kubebuilder:validation:Required") != nil { + props.Required = append(props.Required, fieldName) + } + } + + var propSchema *apiext.JSONSchemaProps + if field.Markers.Get(crdmarkers.SchemalessName) != nil { + propSchema = &apiext.JSONSchemaProps{} + } else { + propSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), field.RawField.Type) + } + propSchema.Description = field.Doc + + applyMarkers(ctx, field.Markers, propSchema, field.RawField) + + if inline { + props.AllOf = append(props.AllOf, *propSchema) + continue + } + + props.Properties[fieldName] = *propSchema + } + + return props +} + +// builtinToType converts builtin basic types to their equivalent JSON schema form. +// It *only* handles types allowed by the kubernetes API standards. Floats are not +// allowed unless allowDangerousTypes is true +func builtinToType(basic *types.Basic, allowDangerousTypes bool) (typ string, format string, err error) { + // NB(directxman12): formats from OpenAPI v3 are slightly different than those defined + // in JSONSchema. This'll use the OpenAPI v3 ones, since they're useful for bounding our + // non-string types. + basicInfo := basic.Info() + switch { + case basicInfo&types.IsBoolean != 0: + typ = "boolean" + case basicInfo&types.IsString != 0: + typ = "string" + case basicInfo&types.IsInteger != 0: + typ = "integer" + case basicInfo&types.IsFloat != 0: + if allowDangerousTypes { + typ = "number" + } else { + return "", "", errors.New("found float, the usage of which is highly discouraged, as support for them varies across languages. Please consider serializing your float as string instead. If you are really sure you want to use them, re-run with crd:allowDangerousTypes=true") + } + default: + return "", "", fmt.Errorf("unsupported type %q", basic.String()) + } + + switch basic.Kind() { + case types.Int32, types.Uint32: + format = "int32" + case types.Int64, types.Uint64: + format = "int64" + } + + return typ, format, nil +} + +// Open coded go/types representation of encoding/json.Marshaller +var jsonMarshaler = types.NewInterfaceType([]*types.Func{ + types.NewFunc(token.NoPos, nil, "MarshalJSON", + types.NewSignature(nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())), + types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)), +}, nil).Complete() + +func implementsJSONMarshaler(typ types.Type) bool { + return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema_visitor.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema_visitor.go new file mode 100644 index 0000000000..2604d739b6 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/schema_visitor.go @@ -0,0 +1,131 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crd + +import ( + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" +) + +// SchemaVisitor walks the nodes of a schema. +type SchemaVisitor interface { + // Visit is called for each schema node. If it returns a visitor, + // the visitor will be called on each direct child node, and then + // this visitor will be called again with `nil` to indicate that + // all children have been visited. If a nil visitor is returned, + // children are not visited. + // + // It is *NOT* safe to save references to the given schema. + // Make deepcopies if you need to keep things around beyond + // the lifetime of the call. + Visit(schema *apiext.JSONSchemaProps) SchemaVisitor +} + +// EditSchema walks the given schema using the given visitor. Actual +// pointers to each schema node are passed to the visitor, so any changes +// made by the visitor will be reflected to the passed-in schema. +func EditSchema(schema *apiext.JSONSchemaProps, visitor SchemaVisitor) { + walker := schemaWalker{visitor: visitor} + walker.walkSchema(schema) +} + +// schemaWalker knows how to walk the schema, saving modifications +// made by the given visitor. +type schemaWalker struct { + visitor SchemaVisitor +} + +// walkSchema walks the given schema, saving modifications made by the visitor +// (this is as simple as passing a pointer in most cases, but special care +// needs to be taken to persist with maps). It also visits referenced +// schemata, dealing with circular references appropriately. The returned +// visitor will be used to visit all "children" of the current schema, followed +// by a nil schema with the returned visitor to mark completion. If a nil visitor +// is returned, traversal will no continue into the children of the current schema. +func (w schemaWalker) walkSchema(schema *apiext.JSONSchemaProps) { + // Walk a potential chain of schema references, keeping track of seen + // references to avoid circular references + subVisitor := w.visitor + seenRefs := map[string]bool{} + if schema.Ref != nil { + seenRefs[*schema.Ref] = true + } + for { + subVisitor = subVisitor.Visit(schema) + if subVisitor == nil { + return + } + // mark completion of the visitor + defer subVisitor.Visit(nil) + + // Break if schema is not a reference or a cycle is detected + if schema.Ref == nil || len(*schema.Ref) == 0 || seenRefs[*schema.Ref] { + break + } + seenRefs[*schema.Ref] = true + } + + // walk sub-schemata + subWalker := schemaWalker{visitor: subVisitor} + if schema.Items != nil { + subWalker.walkPtr(schema.Items.Schema) + subWalker.walkSlice(schema.Items.JSONSchemas) + } + subWalker.walkSlice(schema.AllOf) + subWalker.walkSlice(schema.OneOf) + subWalker.walkSlice(schema.AnyOf) + subWalker.walkPtr(schema.Not) + subWalker.walkMap(schema.Properties) + if schema.AdditionalProperties != nil { + subWalker.walkPtr(schema.AdditionalProperties.Schema) + } + subWalker.walkMap(schema.PatternProperties) + for name, dep := range schema.Dependencies { + subWalker.walkPtr(dep.Schema) + schema.Dependencies[name] = dep + } + if schema.AdditionalItems != nil { + subWalker.walkPtr(schema.AdditionalItems.Schema) + } + subWalker.walkMap(schema.Definitions) +} + +// walkMap walks over values of the given map, saving changes to them. +func (w schemaWalker) walkMap(defs map[string]apiext.JSONSchemaProps) { + for name, def := range defs { + // this is iter var reference is because we immediately preseve it below + //nolint:gosec + w.walkSchema(&def) + // make sure the edits actually go through since we can't + // take a reference to the value in the map + defs[name] = def + } +} + +// walkSlice walks over items of the given slice. +func (w schemaWalker) walkSlice(defs []apiext.JSONSchemaProps) { + for i := range defs { + w.walkSchema(&defs[i]) + } +} + +// walkPtr walks over the contents of the given pointer, if it's not nil. +func (w schemaWalker) walkPtr(def *apiext.JSONSchemaProps) { + if def == nil { + return + } + w.walkSchema(def) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/spec.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/spec.go new file mode 100644 index 0000000000..5fd246c3fc --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/spec.go @@ -0,0 +1,178 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package crd + +import ( + "fmt" + "sort" + "strings" + + "github.com/gobuffalo/flect" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// SpecMarker is a marker that knows how to apply itself to a particular +// version in a CRD Spec. +type SpecMarker interface { + // ApplyToCRD applies this marker to the given CRD, in the given version + // within that CRD. It's called after everything else in the CRD is populated. + ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error +} + +// Marker is a marker that knows how to apply itself to a particular +// version in a CRD. +type Marker interface { + // ApplyToCRD applies this marker to the given CRD, in the given version + // within that CRD. It's called after everything else in the CRD is populated. + ApplyToCRD(crd *apiext.CustomResourceDefinition, version string) error +} + +// NeedCRDFor requests the full CRD for the given group-kind. It requires +// that the packages containing the Go structs for that CRD have already +// been loaded with NeedPackage. +func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) { + p.init() + + if _, exists := p.CustomResourceDefinitions[groupKind]; exists { + return + } + + var packages []*loader.Package + for pkg, gv := range p.GroupVersions { + if gv.Group != groupKind.Group { + continue + } + packages = append(packages, pkg) + } + + defaultPlural := strings.ToLower(flect.Pluralize(groupKind.Kind)) + crd := apiext.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiext.SchemeGroupVersion.String(), + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: defaultPlural + "." + groupKind.Group, + }, + Spec: apiext.CustomResourceDefinitionSpec{ + Group: groupKind.Group, + Names: apiext.CustomResourceDefinitionNames{ + Kind: groupKind.Kind, + ListKind: groupKind.Kind + "List", + Plural: defaultPlural, + Singular: strings.ToLower(groupKind.Kind), + }, + Scope: apiext.NamespaceScoped, + }, + } + + for _, pkg := range packages { + typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind} + typeInfo := p.Types[typeIdent] + if typeInfo == nil { + continue + } + p.NeedFlattenedSchemaFor(typeIdent) + fullSchema := p.FlattenedSchemata[typeIdent] + fullSchema = *fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc) + if maxDescLen != nil { + TruncateDescription(&fullSchema, *maxDescLen) + } + ver := apiext.CustomResourceDefinitionVersion{ + Name: p.GroupVersions[pkg].Version, + Served: true, + Schema: &apiext.CustomResourceValidation{ + OpenAPIV3Schema: &fullSchema, // fine to take a reference since we deepcopy above + }, + } + crd.Spec.Versions = append(crd.Spec.Versions, ver) + } + + // markers are applied *after* initial generation of objects + for _, pkg := range packages { + typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind} + typeInfo := p.Types[typeIdent] + if typeInfo == nil { + continue + } + ver := p.GroupVersions[pkg].Version + + for _, markerVals := range typeInfo.Markers { + for _, val := range markerVals { + if specMarker, isSpecMarker := val.(SpecMarker); isSpecMarker { + if err := specMarker.ApplyToCRD(&crd.Spec, ver); err != nil { + pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + } + } else if crdMarker, isCRDMarker := val.(Marker); isCRDMarker { + if err := crdMarker.ApplyToCRD(&crd, ver); err != nil { + pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec)) + } + } + } + } + } + + // fix the name if the plural was changed (this is the form the name *has* to take, so no harm in changing it). + crd.Name = crd.Spec.Names.Plural + "." + groupKind.Group + + // nothing to actually write + if len(crd.Spec.Versions) == 0 { + return + } + + // it is necessary to make sure the order of CRD versions in crd.Spec.Versions is stable and explicitly set crd.Spec.Version. + // Otherwise, crd.Spec.Version may point to different CRD versions across different runs. + sort.Slice(crd.Spec.Versions, func(i, j int) bool { return crd.Spec.Versions[i].Name < crd.Spec.Versions[j].Name }) + + // make sure we have *a* storage version + // (default it if we only have one, otherwise, bail) + if len(crd.Spec.Versions) == 1 { + crd.Spec.Versions[0].Storage = true + } + + hasStorage := false + for _, ver := range crd.Spec.Versions { + if ver.Storage { + hasStorage = true + break + } + } + if !hasStorage { + // just add the error to the first relevant package for this CRD, + // since there's no specific error location + packages[0].AddError(fmt.Errorf("CRD for %s has no storage version", groupKind)) + } + + served := false + for _, ver := range crd.Spec.Versions { + if ver.Served { + served = true + break + } + } + if !served { + // just add the error to the first relevant package for this CRD, + // since there's no specific error location + packages[0].AddError(fmt.Errorf("CRD for %s with version(s) %v does not serve any version", groupKind, crd.Spec.Versions)) + } + + p.CustomResourceDefinitions[groupKind] = crd +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/crd/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/crd/zz_generated.markerhelp.go new file mode 100644 index 0000000000..15f2a85c37 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/crd/zz_generated.markerhelp.go @@ -0,0 +1,66 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package crd + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "generates CustomResourceDefinition objects.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "IgnoreUnexportedFields": { + Summary: "indicates that we should skip unexported fields. ", + Details: "Left unspecified, the default is false.", + }, + "AllowDangerousTypes": { + Summary: "allows types which are usually omitted from CRD generation because they are not recommended. ", + Details: "Currently the following additional types are allowed when this is true: float32 float64 \n Left unspecified, the default is false", + }, + "MaxDescLen": { + Summary: "specifies the maximum description length for fields in CRD's OpenAPI schema. ", + Details: "0 indicates drop the description for all fields completely. n indicates limit the description to at most n characters and truncate the description to closest sentence boundary if it exceeds n characters.", + }, + "CRDVersions": { + Summary: "specifies the target API versions of the CRD type itself to generate. Defaults to v1. ", + Details: "Currently, the only supported value is v1. \n The first version listed will be assumed to be the \"default\" version and will not get a version suffix in the output filename. \n You'll need to use \"v1\" to get support for features like defaulting, along with an API server that supports it (Kubernetes 1.16+).", + }, + "GenerateEmbeddedObjectMeta": { + Summary: "specifies if any embedded ObjectMeta in the CRD should be generated", + Details: "", + }, + "HeaderFile": { + Summary: "specifies the header text (e.g. license) to prepend to generated files.", + Details: "", + }, + "Year": { + Summary: "specifies the year to substitute for \" YEAR\" in the header file.", + Details: "", + }, + }, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/doc.go new file mode 100644 index 0000000000..f4200f2fc1 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package deepcopy generates DeepCopy, DeepCopyInto, and DeepCopyObject +// implementations for types. +// +// It's ported from k8s.io/code-generator's / k8s.io/gengo's deepcopy-gen, +// but it's scoped specifically to runtime.Object and skips support for +// deepcopying interfaces, which aren't handled in CRDs anyway. +package deepcopy diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/gen.go b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/gen.go new file mode 100644 index 0000000000..7e674a80aa --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/gen.go @@ -0,0 +1,305 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package deepcopy + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "io" + "sort" + "strings" + + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// NB(directxman12): markers.LoadRoots ignores autogenerated code via a build tag +// so any time we check for existing deepcopy functions, we only seen manually written ones. + +const ( + runtimeObjPath = "k8s.io/apimachinery/pkg/runtime.Object" +) + +var ( + enablePkgMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:generate", markers.DescribesPackage, false)) + enableTypeMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:generate", markers.DescribesType, false)) + isObjectMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:root", markers.DescribesType, false)) + + legacyEnablePkgMarker = markers.Must(markers.MakeDefinition("k8s:deepcopy-gen", markers.DescribesPackage, markers.RawArguments(nil))) + legacyEnableTypeMarker = markers.Must(markers.MakeDefinition("k8s:deepcopy-gen", markers.DescribesType, markers.RawArguments(nil))) + legacyIsObjectMarker = markers.Must(markers.MakeDefinition("k8s:deepcopy-gen:interfaces", markers.DescribesType, "")) +) + +// +controllertools:marker:generateHelp + +// Generator generates code containing DeepCopy, DeepCopyInto, and +// DeepCopyObject method implementations. +type Generator struct { + // HeaderFile specifies the header text (e.g. license) to prepend to generated files. + HeaderFile string `marker:",optional"` + // Year specifies the year to substitute for " YEAR" in the header file. + Year string `marker:",optional"` +} + +func (Generator) CheckFilter() loader.NodeFilter { + return func(node ast.Node) bool { + // ignore interfaces + _, isIface := node.(*ast.InterfaceType) + return !isIface + } +} + +func (Generator) RegisterMarkers(into *markers.Registry) error { + if err := markers.RegisterAll(into, + enablePkgMarker, legacyEnablePkgMarker, enableTypeMarker, + legacyEnableTypeMarker, isObjectMarker, legacyIsObjectMarker); err != nil { + return err + } + into.AddHelp(enablePkgMarker, + markers.SimpleHelp("object", "enables or disables object interface & deepcopy implementation generation for this package")) + into.AddHelp( + enableTypeMarker, markers.SimpleHelp("object", "overrides enabling or disabling deepcopy generation for this type")) + into.AddHelp(isObjectMarker, + markers.SimpleHelp("object", "enables object interface implementation generation for this type")) + + into.AddHelp(legacyEnablePkgMarker, + markers.DeprecatedHelp(enablePkgMarker.Name, "object", "enables or disables object interface & deepcopy implementation generation for this package")) + into.AddHelp(legacyEnableTypeMarker, + markers.DeprecatedHelp(enableTypeMarker.Name, "object", "overrides enabling or disabling deepcopy generation for this type")) + into.AddHelp(legacyIsObjectMarker, + markers.DeprecatedHelp(isObjectMarker.Name, "object", "enables object interface implementation generation for this type")) + return nil +} + +func enabledOnPackage(col *markers.Collector, pkg *loader.Package) (bool, error) { + pkgMarkers, err := markers.PackageMarkers(col, pkg) + if err != nil { + return false, err + } + pkgMarker := pkgMarkers.Get(enablePkgMarker.Name) + if pkgMarker != nil { + return pkgMarker.(bool), nil + } + legacyMarker := pkgMarkers.Get(legacyEnablePkgMarker.Name) + if legacyMarker != nil { + legacyMarkerVal := string(legacyMarker.(markers.RawArguments)) + firstArg := strings.Split(legacyMarkerVal, ",")[0] + return firstArg == "package", nil + } + + return false, nil +} + +func enabledOnType(allTypes bool, info *markers.TypeInfo) bool { + if typeMarker := info.Markers.Get(enableTypeMarker.Name); typeMarker != nil { + return typeMarker.(bool) + } + legacyMarker := info.Markers.Get(legacyEnableTypeMarker.Name) + if legacyMarker != nil { + legacyMarkerVal := string(legacyMarker.(markers.RawArguments)) + return legacyMarkerVal == "true" + } + return allTypes || genObjectInterface(info) +} + +func genObjectInterface(info *markers.TypeInfo) bool { + objectEnabled := info.Markers.Get(isObjectMarker.Name) + if objectEnabled != nil { + return objectEnabled.(bool) + } + + for _, legacyEnabled := range info.Markers[legacyIsObjectMarker.Name] { + if legacyEnabled == runtimeObjPath { + return true + } + } + return false +} + +func (d Generator) Generate(ctx *genall.GenerationContext) error { + var headerText string + + if d.HeaderFile != "" { + headerBytes, err := ctx.ReadFile(d.HeaderFile) + if err != nil { + return err + } + headerText = string(headerBytes) + } + headerText = strings.ReplaceAll(headerText, " YEAR", " "+d.Year) + + objGenCtx := ObjectGenCtx{ + Collector: ctx.Collector, + Checker: ctx.Checker, + HeaderText: headerText, + } + + for _, root := range ctx.Roots { + outContents := objGenCtx.generateForPackage(root) + if outContents == nil { + continue + } + + writeOut(ctx, root, outContents) + } + + return nil +} + +// ObjectGenCtx contains the common info for generating deepcopy implementations. +// It mostly exists so that generating for a package can be easily tested without +// requiring a full set of output rules, etc. +type ObjectGenCtx struct { + Collector *markers.Collector + Checker *loader.TypeChecker + HeaderText string +} + +// writeHeader writes out the build tag, package declaration, and imports +func writeHeader(pkg *loader.Package, out io.Writer, packageName string, imports *importsList, headerText string) { + // NB(directxman12): blank line after build tags to distinguish them from comments + _, err := fmt.Fprintf(out, `//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +%[3]s + +// Code generated by controller-gen. DO NOT EDIT. + +package %[1]s + +import ( +%[2]s +) + +`, packageName, strings.Join(imports.ImportSpecs(), "\n"), headerText) + if err != nil { + pkg.AddError(err) + } + +} + +// generateForPackage generates DeepCopy and runtime.Object implementations for +// types in the given package, writing the formatted result to given writer. +// May return nil if source could not be generated. +func (ctx *ObjectGenCtx) generateForPackage(root *loader.Package) []byte { + allTypes, err := enabledOnPackage(ctx.Collector, root) + if err != nil { + root.AddError(err) + return nil + } + + ctx.Checker.Check(root) + + root.NeedTypesInfo() + + byType := make(map[string][]byte) + imports := &importsList{ + byPath: make(map[string]string), + byAlias: make(map[string]string), + pkg: root, + } + // avoid confusing aliases by "reserving" the root package's name as an alias + imports.byAlias[root.Name] = "" + + if err := markers.EachType(ctx.Collector, root, func(info *markers.TypeInfo) { + outContent := new(bytes.Buffer) + + // copy when nabled for all types and not disabled, or enabled + // specifically on this type + if !enabledOnType(allTypes, info) { + return + } + + // avoid copying non-exported types, etc + if !shouldBeCopied(root, info) { + return + } + + copyCtx := ©MethodMaker{ + pkg: root, + importsList: imports, + codeWriter: &codeWriter{out: outContent}, + } + + copyCtx.GenerateMethodsFor(root, info) + + outBytes := outContent.Bytes() + if len(outBytes) > 0 { + byType[info.Name] = outBytes + } + }); err != nil { + root.AddError(err) + return nil + } + + if len(byType) == 0 { + return nil + } + + outContent := new(bytes.Buffer) + writeHeader(root, outContent, root.Name, imports, ctx.HeaderText) + writeMethods(root, outContent, byType) + + outBytes := outContent.Bytes() + formattedBytes, err := format.Source(outBytes) + if err != nil { + root.AddError(err) + // we still write the invalid source to disk to figure out what went wrong + } else { + outBytes = formattedBytes + } + + return outBytes +} + +// writeMethods writes each method to the file, sorted by type name. +func writeMethods(pkg *loader.Package, out io.Writer, byType map[string][]byte) { + sortedNames := make([]string, 0, len(byType)) + for name := range byType { + sortedNames = append(sortedNames, name) + } + sort.Strings(sortedNames) + + for _, name := range sortedNames { + _, err := out.Write(byType[name]) + if err != nil { + pkg.AddError(err) + } + } +} + +// writeFormatted outputs the given code, after gofmt-ing it. If we couldn't gofmt, +// we write the unformatted code for debugging purposes. +func writeOut(ctx *genall.GenerationContext, root *loader.Package, outBytes []byte) { + outputFile, err := ctx.Open(root, "zz_generated.deepcopy.go") + if err != nil { + root.AddError(err) + return + } + defer outputFile.Close() + n, err := outputFile.Write(outBytes) + if err != nil { + root.AddError(err) + return + } + if n < len(outBytes) { + root.AddError(io.ErrShortWrite) + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/traverse.go b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/traverse.go new file mode 100644 index 0000000000..3a751757d5 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/traverse.go @@ -0,0 +1,829 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package deepcopy + +import ( + "fmt" + "go/ast" + "go/types" + "io" + "path" + "strings" + "unicode" + "unicode/utf8" + + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// NB(directxman12): This code is a bit of a byzantine mess. +// I've tried to clean it up a bit from the original in deepcopy-gen, +// but parts remain a bit convoluted. Exercise caution when changing. +// It's perhaps a tad over-commented now, but better safe than sorry. +// It also seriously needs auditing for sanity -- there's parts where we +// copy the original deepcopy-gen's output just to be safe, but some of that +// could be simplified away if we're careful. + +// codeWriter assists in writing out Go code lines and blocks to a writer. +type codeWriter struct { + out io.Writer +} + +// Line writes a single line. +func (c *codeWriter) Line(line string) { + fmt.Fprintln(c.out, line) +} + +// Linef writes a single line with formatting (as per fmt.Sprintf). +func (c *codeWriter) Linef(line string, args ...interface{}) { + fmt.Fprintf(c.out, line+"\n", args...) +} + +// If writes an if statement with the given setup/condition clause, executing +// the given function to write the contents of the block. +func (c *codeWriter) If(setup string, block func()) { + c.Linef("if %s {", setup) + block() + c.Line("}") +} + +// If writes if and else statements with the given setup/condition clause, executing +// the given functions to write the contents of the blocks. +func (c *codeWriter) IfElse(setup string, ifBlock func(), elseBlock func()) { + c.Linef("if %s {", setup) + ifBlock() + c.Line("} else {") + elseBlock() + c.Line("}") +} + +// For writes an for statement with the given setup/condition clause, executing +// the given function to write the contents of the block. +func (c *codeWriter) For(setup string, block func()) { + c.Linef("for %s {", setup) + block() + c.Line("}") +} + +// importsList keeps track of required imports, automatically assigning aliases +// to import statement. +type importsList struct { + byPath map[string]string + byAlias map[string]string + + pkg *loader.Package +} + +// NeedImport marks that the given package is needed in the list of imports, +// returning the ident (import alias) that should be used to reference the package. +func (l *importsList) NeedImport(importPath string) string { + // we get an actual path from Package, which might include venddored + // packages if running on a package in vendor. + if ind := strings.LastIndex(importPath, "/vendor/"); ind != -1 { + importPath = importPath[ind+8: /* len("/vendor/") */] + } + + // check to see if we've already assigned an alias, and just return that. + alias, exists := l.byPath[importPath] + if exists { + return alias + } + + // otherwise, calculate an import alias by joining path parts till we get something unique + restPath, nextWord := path.Split(importPath) + + for otherPath, exists := "", true; exists && otherPath != importPath; otherPath, exists = l.byAlias[alias] { + if restPath == "" { + // do something else to disambiguate if we're run out of parts and + // still have duplicates, somehow + alias += "x" + } + + // can't have a first digit, per Go identifier rules, so just skip them + for firstRune, runeLen := utf8.DecodeRuneInString(nextWord); unicode.IsDigit(firstRune); firstRune, runeLen = utf8.DecodeRuneInString(nextWord) { + nextWord = nextWord[runeLen:] + } + + // make a valid identifier by replacing "bad" characters with underscores + nextWord = strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { + return r + } + return '_' + }, nextWord) + + alias = nextWord + alias + if len(restPath) > 0 { + restPath, nextWord = path.Split(restPath[:len(restPath)-1] /* chop off final slash */) + } + } + + l.byPath[importPath] = alias + l.byAlias[alias] = importPath + return alias +} + +// ImportSpecs returns a string form of each import spec +// (i.e. `alias "path/to/import"). Aliases are only present +// when they don't match the package name. +func (l *importsList) ImportSpecs() []string { + res := make([]string, 0, len(l.byPath)) + for importPath, alias := range l.byPath { + pkg := l.pkg.Imports()[importPath] + if pkg != nil && pkg.Name == alias { + // don't print if alias is the same as package name + // (we've already taken care of duplicates). + res = append(res, fmt.Sprintf("%q", importPath)) + } else { + res = append(res, fmt.Sprintf("%s %q", alias, importPath)) + } + } + return res +} + +// namingInfo holds package and syntax for referencing a field, type, +// etc. It's used to allow lazily marking import usage. +// You should generally retrieve the syntax using Syntax. +type namingInfo struct { + // typeInfo is the type being named. + typeInfo types.Type + nameOverride string +} + +// Syntax calculates the code representation of the given type or name, +// and marks that is used (potentially marking an import as used). +func (n *namingInfo) Syntax(basePkg *loader.Package, imports *importsList) string { + if n.nameOverride != "" { + return n.nameOverride + } + + // NB(directxman12): typeInfo.String gets us most of the way there, + // but fails (for us) on named imports, since it uses the full package path. + switch typeInfo := n.typeInfo.(type) { + case *types.Named: + // register that we need an import for this type, + // so we can get the appropriate alias to use. + typeName := typeInfo.Obj() + otherPkg := typeName.Pkg() + if otherPkg == basePkg.Types { + // local import + return typeName.Name() + } + alias := imports.NeedImport(loader.NonVendorPath(otherPkg.Path())) + return alias + "." + typeName.Name() + case *types.Basic: + return typeInfo.String() + case *types.Pointer: + return "*" + (&namingInfo{typeInfo: typeInfo.Elem()}).Syntax(basePkg, imports) + case *types.Slice: + return "[]" + (&namingInfo{typeInfo: typeInfo.Elem()}).Syntax(basePkg, imports) + case *types.Map: + return fmt.Sprintf( + "map[%s]%s", + (&namingInfo{typeInfo: typeInfo.Key()}).Syntax(basePkg, imports), + (&namingInfo{typeInfo: typeInfo.Elem()}).Syntax(basePkg, imports)) + default: + basePkg.AddError(fmt.Errorf("name requested for invalid type: %s", typeInfo)) + return typeInfo.String() + } +} + +// copyMethodMakers makes DeepCopy (and related) methods for Go types, +// writing them to its codeWriter. +type copyMethodMaker struct { + pkg *loader.Package + *importsList + *codeWriter +} + +// GenerateMethodsFor makes DeepCopy, DeepCopyInto, and DeepCopyObject methods +// for the given type, when appropriate +func (c *copyMethodMaker) GenerateMethodsFor(root *loader.Package, info *markers.TypeInfo) { + typeInfo := root.TypesInfo.TypeOf(info.RawSpec.Name) + if typeInfo == types.Typ[types.Invalid] { + root.AddError(loader.ErrFromNode(fmt.Errorf("unknown type: %s", info.Name), info.RawSpec)) + } + + // figure out if we need to use a pointer receiver -- most types get a pointer receiver, + // except those that are aliases to types that are already pass-by-reference (pointers, + // interfaces. maps, slices). + ptrReceiver := usePtrReceiver(typeInfo) + + hasManualDeepCopyInto := hasDeepCopyIntoMethod(root, typeInfo) + hasManualDeepCopy, deepCopyOnPtr := hasDeepCopyMethod(root, typeInfo) + + // only generate each method if it hasn't been implemented. + if !hasManualDeepCopyInto { + c.Line("// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.") + if ptrReceiver { + c.Linef("func (in *%s) DeepCopyInto(out *%s) {", info.Name, info.Name) + } else { + c.Linef("func (in %s) DeepCopyInto(out *%s) {", info.Name, info.Name) + c.Line("{in := &in") // add an extra block so that we can redefine `in` without type issues + } + + // just wrap the existing deepcopy if present + if hasManualDeepCopy { + if deepCopyOnPtr { + c.Line("clone := in.DeepCopy()") + c.Line("*out = *clone") + } else { + c.Line("*out = in.DeepCopy()") + } + } else { + c.genDeepCopyIntoBlock(&namingInfo{nameOverride: info.Name}, typeInfo) + } + + if !ptrReceiver { + c.Line("}") // close our extra "in redefinition" block + } + c.Line("}") + } + + if !hasManualDeepCopy { + // these are both straightforward, so we just template them out. + if ptrReceiver { + c.Linef(ptrDeepCopy, info.Name) + } else { + c.Linef(bareDeepCopy, info.Name) + } + + // maybe also generate DeepCopyObject, if asked. + if genObjectInterface(info) { + // we always need runtime.Object for DeepCopyObject + runtimeAlias := c.NeedImport("k8s.io/apimachinery/pkg/runtime") + if ptrReceiver { + c.Linef(ptrDeepCopyObj, info.Name, runtimeAlias) + } else { + c.Linef(bareDeepCopyObj, info.Name, runtimeAlias) + } + } + } +} + +// genDeepCopyBody generates a DeepCopyInto block for the given type. The +// block is *not* wrapped in curly braces. +func (c *copyMethodMaker) genDeepCopyIntoBlock(actualName *namingInfo, typeInfo types.Type) { + // we calculate *how* we should copy mostly based on the "eventual" type of + // a given type (i.e. the type that results from following all aliases) + last := eventualUnderlyingType(typeInfo) + + // we might hit a type that has a manual deepcopy method written on non-root types + // (this case is handled for root types in GenerateMethodFor). + // In that case (when we're not dealing with a pointer, since those need special handling + // to match 1-to-1 with k8s deepcopy-gen), just use that. + if _, isPtr := last.(*types.Pointer); !isPtr && hasAnyDeepCopyMethod(c.pkg, typeInfo) { + c.Line("*out = in.DeepCopy()") + return + } + + switch last := last.(type) { + case *types.Basic: + switch last.Kind() { + case types.Invalid, types.UnsafePointer: + c.pkg.AddError(fmt.Errorf("invalid type: %s", last)) + default: + // basic types themselves can be "shallow" copied, so all we need + // to do is check if our *actual* type (not the underlying one) has + // a custom method implemented. + if hasMethod, _ := hasDeepCopyMethod(c.pkg, typeInfo); hasMethod { + c.Line("*out = in.DeepCopy()") + } + c.Line("*out = *in") + } + case *types.Map: + c.genMapDeepCopy(actualName, last) + case *types.Slice: + c.genSliceDeepCopy(actualName, last) + case *types.Struct: + c.genStructDeepCopy(actualName, last) + case *types.Pointer: + c.genPointerDeepCopy(actualName, last) + case *types.Named: + // handled via the above loop, should never happen + c.pkg.AddError(fmt.Errorf("interface type %s encountered directly, invalid condition", last)) + default: + c.pkg.AddError(fmt.Errorf("invalid type: %s", last)) + } +} + +// genMapDeepCopy generates DeepCopy code for the given named type whose eventual +// type is the given map type. +func (c *copyMethodMaker) genMapDeepCopy(actualName *namingInfo, mapType *types.Map) { + // maps *must* have shallow-copiable types, since we just iterate + // through the keys, only trying to deepcopy the values. + if !fineToShallowCopy(mapType.Key()) { + c.pkg.AddError(fmt.Errorf("invalid map key type: %s", mapType.Key())) + return + } + + // make our actual type (not the underlying one)... + c.Linef("*out = make(%[1]s, len(*in))", actualName.Syntax(c.pkg, c.importsList)) + + // ...and copy each element appropriately + c.For("key, val := range *in", func() { + // check if we have manually written methods, + // in which case we'll just try and use those + hasDeepCopy, copyOnPtr := hasDeepCopyMethod(c.pkg, mapType.Elem()) + hasDeepCopyInto := hasDeepCopyIntoMethod(c.pkg, mapType.Elem()) + switch { + case hasDeepCopyInto || hasDeepCopy: + // use the manually-written methods + _, fieldIsPtr := mapType.Elem().(*types.Pointer) // is "out" actually a pointer + inIsPtr := resultWillBePointer(mapType.Elem(), hasDeepCopy, copyOnPtr) // does copying "in" produce a pointer + if hasDeepCopy { + // If we're calling DeepCopy, check if it's receiver needs a pointer + inIsPtr = copyOnPtr + } + if inIsPtr == fieldIsPtr { + c.Line("(*out)[key] = val.DeepCopy()") + } else if fieldIsPtr { + c.Line("{") // use a block because we use `x` as a temporary + c.Line("x := val.DeepCopy()") + c.Line("(*out)[key] = &x") + c.Line("}") + } else { + c.Line("(*out)[key] = *val.DeepCopy()") + } + case fineToShallowCopy(mapType.Elem()): + // just shallow copy types for which it's safe to do so + c.Line("(*out)[key] = val") + default: + // otherwise, we've got some kind-specific actions, + // based on the element's eventual type. + + underlyingElem := eventualUnderlyingType(mapType.Elem()) + + // if it passes by reference, let the main switch handle it + if passesByReference(underlyingElem) { + c.Linef("var outVal %[1]s", (&namingInfo{typeInfo: underlyingElem}).Syntax(c.pkg, c.importsList)) + c.IfElse("val == nil", func() { + c.Line("(*out)[key] = nil") + }, func() { + c.Line("in, out := &val, &outVal") + c.genDeepCopyIntoBlock(&namingInfo{typeInfo: mapType.Elem()}, mapType.Elem()) + }) + c.Line("(*out)[key] = outVal") + + return + } + + // otherwise... + switch underlyingElem := underlyingElem.(type) { + case *types.Struct: + // structs will have deepcopy generated for them, so use that + c.Line("(*out)[key] = *val.DeepCopy()") + default: + c.pkg.AddError(fmt.Errorf("invalid map value type: %s", underlyingElem)) + return + } + } + }) +} + +// genSliceDeepCopy generates DeepCopy code for the given named type whose +// underlying type is the given slice. +func (c *copyMethodMaker) genSliceDeepCopy(actualName *namingInfo, sliceType *types.Slice) { + underlyingElem := eventualUnderlyingType(sliceType.Elem()) + + // make the actual type (not the underlying) + c.Linef("*out = make(%[1]s, len(*in))", actualName.Syntax(c.pkg, c.importsList)) + + // check if we need to do anything special, or just copy each element appropriately + switch { + case hasAnyDeepCopyMethod(c.pkg, sliceType.Elem()): + // just use deepcopy if it's present (deepcopyinto will be filled in by our code) + c.For("i := range *in", func() { + c.Line("(*in)[i].DeepCopyInto(&(*out)[i])") + }) + case fineToShallowCopy(underlyingElem): + // shallow copy if ok + c.Line("copy(*out, *in)") + default: + // copy each element appropriately + c.For("i := range *in", func() { + // fall back to normal code for reference types or those with custom logic + if passesByReference(underlyingElem) || hasAnyDeepCopyMethod(c.pkg, sliceType.Elem()) { + c.If("(*in)[i] != nil", func() { + c.Line("in, out := &(*in)[i], &(*out)[i]") + c.genDeepCopyIntoBlock(&namingInfo{typeInfo: sliceType.Elem()}, sliceType.Elem()) + }) + return + } + + switch underlyingElem.(type) { + case *types.Struct: + // structs will always have deepcopy + c.Linef("(*in)[i].DeepCopyInto(&(*out)[i])") + default: + c.pkg.AddError(fmt.Errorf("invalid slice element type: %s", underlyingElem)) + } + }) + } +} + +// genStructDeepCopy generates DeepCopy code for the given named type whose +// underlying type is the given struct. +func (c *copyMethodMaker) genStructDeepCopy(_ *namingInfo, structType *types.Struct) { + c.Line("*out = *in") + + for i := 0; i < structType.NumFields(); i++ { + field := structType.Field(i) + + // if we have a manual deepcopy, use that + hasDeepCopy, copyOnPtr := hasDeepCopyMethod(c.pkg, field.Type()) + hasDeepCopyInto := hasDeepCopyIntoMethod(c.pkg, field.Type()) + if hasDeepCopyInto || hasDeepCopy { + // NB(directxman12): yes, I know this is kind-of weird that we + // have all this special-casing here, but it's nice for testing + // purposes to be 1-to-1 with deepcopy-gen, which does all sorts of + // stuff like this (I'm pretty sure I found some codepaths that + // never execute there, because they're pretty clearly invalid + // syntax). + + _, fieldIsPtr := field.Type().(*types.Pointer) + inIsPtr := resultWillBePointer(field.Type(), hasDeepCopy, copyOnPtr) + if fieldIsPtr { + // we'll need a if block to check for nilness + // we'll let genDeepCopyIntoBlock handle the details, we just needed the setup + c.If(fmt.Sprintf("in.%s != nil", field.Name()), func() { + c.Linef("in, out := &in.%[1]s, &out.%[1]s", field.Name()) + c.genDeepCopyIntoBlock(&namingInfo{typeInfo: field.Type()}, field.Type()) + }) + } else { + // special-case for compatibility with deepcopy-gen + if inIsPtr == fieldIsPtr { + c.Linef("out.%[1]s = in.%[1]s.DeepCopy()", field.Name()) + } else { + c.Linef("in.%[1]s.DeepCopyInto(&out.%[1]s)", field.Name()) + } + } + continue + } + + // pass-by-reference fields get delegated to the main type + underlyingField := eventualUnderlyingType(field.Type()) + if passesByReference(underlyingField) { + c.If(fmt.Sprintf("in.%s != nil", field.Name()), func() { + c.Linef("in, out := &in.%[1]s, &out.%[1]s", field.Name()) + c.genDeepCopyIntoBlock(&namingInfo{typeInfo: field.Type()}, field.Type()) + }) + continue + } + + // otherwise... + switch underlyingField := underlyingField.(type) { + case *types.Basic: + switch underlyingField.Kind() { + case types.Invalid, types.UnsafePointer: + c.pkg.AddError(loader.ErrFromNode(fmt.Errorf("invalid field type: %s", underlyingField), field)) + return + default: + // nothing to do, initial assignment copied this + } + case *types.Struct: + if fineToShallowCopy(field.Type()) { + c.Linef("out.%[1]s = in.%[1]s", field.Name()) + } else { + c.Linef("in.%[1]s.DeepCopyInto(&out.%[1]s)", field.Name()) + } + default: + c.pkg.AddError(loader.ErrFromNode(fmt.Errorf("invalid field type: %s", underlyingField), field)) + return + } + } +} + +// genPointerDeepCopy generates DeepCopy code for the given named type whose +// underlying type is the given struct. +func (c *copyMethodMaker) genPointerDeepCopy(_ *namingInfo, pointerType *types.Pointer) { + underlyingElem := eventualUnderlyingType(pointerType.Elem()) + + // if we have a manually written deepcopy, just use that + hasDeepCopy, copyOnPtr := hasDeepCopyMethod(c.pkg, pointerType.Elem()) + hasDeepCopyInto := hasDeepCopyIntoMethod(c.pkg, pointerType.Elem()) + if hasDeepCopyInto || hasDeepCopy { + outNeedsPtr := resultWillBePointer(pointerType.Elem(), hasDeepCopy, copyOnPtr) + if hasDeepCopy { + outNeedsPtr = copyOnPtr + } + if outNeedsPtr { + c.Line("*out = (*in).DeepCopy()") + } else { + c.Line("x := (*in).DeepCopy()") + c.Line("*out = &x") + } + return + } + + // shallow-copiable types are pretty easy + if fineToShallowCopy(underlyingElem) { + c.Linef("*out = new(%[1]s)", (&namingInfo{typeInfo: pointerType.Elem()}).Syntax(c.pkg, c.importsList)) + c.Line("**out = **in") + return + } + + // pass-by-reference types get delegated to the main switch + if passesByReference(underlyingElem) { + c.Linef("*out = new(%s)", (&namingInfo{typeInfo: underlyingElem}).Syntax(c.pkg, c.importsList)) + c.If("**in != nil", func() { + c.Line("in, out := *in, *out") + c.genDeepCopyIntoBlock(&namingInfo{typeInfo: underlyingElem}, eventualUnderlyingType(underlyingElem)) + }) + return + } + + // otherwise... + switch underlyingElem := underlyingElem.(type) { + case *types.Struct: + c.Linef("*out = new(%[1]s)", (&namingInfo{typeInfo: pointerType.Elem()}).Syntax(c.pkg, c.importsList)) + c.Line("(*in).DeepCopyInto(*out)") + default: + c.pkg.AddError(fmt.Errorf("invalid pointer element type: %s", underlyingElem)) + return + } +} + +// usePtrReceiver checks if we need a pointer receiver on methods for the given type +// Pass-by-reference types don't get pointer receivers. +func usePtrReceiver(typeInfo types.Type) bool { + switch typeInfo.(type) { + case *types.Pointer: + return false + case *types.Map: + return false + case *types.Slice: + return false + case *types.Named: + return usePtrReceiver(typeInfo.Underlying()) + default: + return true + } +} + +func resultWillBePointer(typeInfo types.Type, hasDeepCopy, deepCopyOnPtr bool) bool { + // if we have a manual deepcopy, we can just check what that returns + if hasDeepCopy { + return deepCopyOnPtr + } + + // otherwise, we'll need to check its type + switch typeInfo := typeInfo.(type) { + case *types.Pointer: + // NB(directxman12): we don't have to worry about the elem having a deepcopy, + // since hasManualDeepCopy would've caught that. + + // we'll be calling on the elem, so check that + return resultWillBePointer(typeInfo.Elem(), false, false) + case *types.Map: + return false + case *types.Slice: + return false + case *types.Named: + return resultWillBePointer(typeInfo.Underlying(), false, false) + default: + return true + } +} + +// shouldBeCopied checks if we're supposed to make deepcopy methods the given type. +// +// This is the case if it's exported *and* either: +// - has a partial manual DeepCopy implementation (in which case we fill in the rest) +// - aliases to a non-basic type eventually +// - is a struct +func shouldBeCopied(pkg *loader.Package, info *markers.TypeInfo) bool { + if !ast.IsExported(info.Name) { + return false + } + + typeInfo := pkg.TypesInfo.TypeOf(info.RawSpec.Name) + if typeInfo == types.Typ[types.Invalid] { + pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown type: %s", info.Name), info.RawSpec)) + return false + } + + // according to gengo, everything named is an alias, except for an alias to a pointer, + // which is just a pointer, afaict. Just roll with it. + if asPtr, isPtr := typeInfo.(*types.Named).Underlying().(*types.Pointer); isPtr { + typeInfo = asPtr + } + + lastType := typeInfo + if _, isNamed := typeInfo.(*types.Named); isNamed { + // if it has a manual deepcopy or deepcopyinto, we're fine + if hasAnyDeepCopyMethod(pkg, typeInfo) { + return true + } + + for underlyingType := typeInfo.Underlying(); underlyingType != lastType; lastType, underlyingType = underlyingType, underlyingType.Underlying() { + // if it has a manual deepcopy or deepcopyinto, we're fine + if hasAnyDeepCopyMethod(pkg, underlyingType) { + return true + } + + // aliases to other things besides basics need copy methods + // (basics can be straight-up shallow-copied) + if _, isBasic := underlyingType.(*types.Basic); !isBasic { + return true + } + } + } + + // structs are the only thing that's not a basic that's copiable by default + _, isStruct := lastType.(*types.Struct) + return isStruct +} + +// hasDeepCopyMethod checks if this type has a manual DeepCopy method and if +// the method has a pointer receiver. +func hasDeepCopyMethod(pkg *loader.Package, typeInfo types.Type) (bool, bool) { + deepCopyMethod, ind, _ := types.LookupFieldOrMethod(typeInfo, true /* check pointers too */, pkg.Types, "DeepCopy") + if len(ind) != 1 { + // ignore embedded methods + return false, false + } + if deepCopyMethod == nil { + return false, false + } + + methodSig := deepCopyMethod.Type().(*types.Signature) + if methodSig.Params() != nil && methodSig.Params().Len() != 0 { + return false, false + } + if methodSig.Results() == nil || methodSig.Results().Len() != 1 { + return false, false + } + + recvAsPtr, recvIsPtr := methodSig.Recv().Type().(*types.Pointer) + if recvIsPtr { + // NB(directxman12): the pointer type returned here isn't comparable even though they + // have the same underlying type, for some reason (probably that + // LookupFieldOrMethod calls types.NewPointer for us), so check the + // underlying values. + + resultPtr, resultIsPtr := methodSig.Results().At(0).Type().(*types.Pointer) + if !resultIsPtr { + // pointer vs non-pointer are different types + return false, false + } + + if recvAsPtr.Elem() != resultPtr.Elem() { + return false, false + } + } else if methodSig.Results().At(0).Type() != methodSig.Recv().Type() { + return false, false + } + + return true, recvIsPtr +} + +// hasDeepCopyIntoMethod checks if this type has a manual DeepCopyInto method. +func hasDeepCopyIntoMethod(pkg *loader.Package, typeInfo types.Type) bool { + deepCopyMethod, ind, _ := types.LookupFieldOrMethod(typeInfo, true /* check pointers too */, pkg.Types, "DeepCopyInto") + if len(ind) != 1 { + // ignore embedded methods + return false + } + if deepCopyMethod == nil { + return false + } + + methodSig := deepCopyMethod.Type().(*types.Signature) + if methodSig.Params() == nil || methodSig.Params().Len() != 1 { + return false + } + paramPtr, isPtr := methodSig.Params().At(0).Type().(*types.Pointer) + if !isPtr { + return false + } + if methodSig.Results() != nil && methodSig.Results().Len() != 0 { + return false + } + + if recvPtr, recvIsPtr := methodSig.Recv().Type().(*types.Pointer); recvIsPtr { + // NB(directxman12): the pointer type returned here isn't comparable even though they + // have the same underlying type, for some reason (probably that + // LookupFieldOrMethod calls types.NewPointer for us), so check the + // underlying values. + return paramPtr.Elem() == recvPtr.Elem() + } + return methodSig.Recv().Type() == paramPtr.Elem() +} + +// hasAnyDeepCopyMethod checks if the given method has DeepCopy or DeepCopyInto +// (either of which implies the other will exist eventually). +func hasAnyDeepCopyMethod(pkg *loader.Package, typeInfo types.Type) bool { + hasDeepCopy, _ := hasDeepCopyMethod(pkg, typeInfo) + return hasDeepCopy || hasDeepCopyIntoMethod(pkg, typeInfo) +} + +// eventualUnderlyingType gets the "final" type in a sequence of named aliases. +// It's effectively a shortcut for calling Underlying in a loop. +func eventualUnderlyingType(typeInfo types.Type) types.Type { + last := typeInfo + for underlying := typeInfo.Underlying(); underlying != last; last, underlying = underlying, underlying.Underlying() { + // get the actual underlying type + } + return last +} + +// fineToShallowCopy checks if a shallow-copying a type is equivalent to deepcopy-ing it. +func fineToShallowCopy(typeInfo types.Type) bool { + switch typeInfo := typeInfo.(type) { + case *types.Basic: + // basic types (int, string, etc) are always fine to shallow-copy, + // except for Invalid and UnsafePointer, which can't be copied at all. + switch typeInfo.Kind() { + case types.Invalid, types.UnsafePointer: + return false + default: + return true + } + case *types.Named: + // aliases are fine to shallow-copy as long as they resolve to a shallow-copyable type + return fineToShallowCopy(typeInfo.Underlying()) + case *types.Struct: + // structs are fine to shallow-copy if they have all shallow-copyable fields + for i := 0; i < typeInfo.NumFields(); i++ { + field := typeInfo.Field(i) + if !fineToShallowCopy(field.Type()) { + return false + } + } + return true + default: + return false + } +} + +// passesByReference checks if the given type passesByReference +// (except for interfaces, which are handled separately). +func passesByReference(typeInfo types.Type) bool { + switch typeInfo.(type) { + case *types.Slice: + return true + case *types.Map: + return true + case *types.Pointer: + return true + default: + return false + } +} + +var ( + // ptrDeepCopy is a DeepCopy for a type with an existing DeepCopyInto and a pointer receiver. + ptrDeepCopy = ` +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new %[1]s. +func (in *%[1]s) DeepCopy() *%[1]s { + if in == nil { return nil } + out := new(%[1]s) + in.DeepCopyInto(out) + return out +} +` + + // ptrDeepCopy is a DeepCopy for a type with an existing DeepCopyInto and a non-pointer receiver. + bareDeepCopy = ` +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new %[1]s. +func (in %[1]s) DeepCopy() %[1]s { + if in == nil { return nil } + out := new(%[1]s) + in.DeepCopyInto(out) + return *out +} +` + + // ptrDeepCopy is a DeepCopyObject for a type with an existing DeepCopyInto and a pointer receiver. + ptrDeepCopyObj = ` +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *%[1]s) DeepCopyObject() %[2]s.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} +` + // ptrDeepCopy is a DeepCopyObject for a type with an existing DeepCopyInto and a non-pointer receiver. + bareDeepCopyObj = ` +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in %[1]s) DeepCopyObject() %[2]s.Object { + return in.DeepCopy() +} +` +) diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/zz_generated.markerhelp.go new file mode 100644 index 0000000000..913bc1fb7f --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/deepcopy/zz_generated.markerhelp.go @@ -0,0 +1,46 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package deepcopy + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "generates code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "HeaderFile": { + Summary: "specifies the header text (e.g. license) to prepend to generated files.", + Details: "", + }, + "Year": { + Summary: "specifies the year to substitute for \" YEAR\" in the header file.", + Details: "", + }, + }, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/doc.go new file mode 100644 index 0000000000..af67402e68 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/doc.go @@ -0,0 +1,58 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package genall defines entrypoints for generation tools to hook into and +// share the same set of parsing, typechecking, and marker information. +// +// # Generators +// +// Each Generator knows how to register its markers into a central Registry, +// and then how to generate output using a Collector and some root packages. +// Each generator can be considered to be the output type of a marker, for easy +// command line parsing. +// +// # Output and Input +// +// Generators output artifacts via an OutputRule. OutputRules know how to +// write output for different package-associated (code) files, as well as +// config files. Each OutputRule should also be considered to be the output +// type as a marker, for easy command-line parsing. +// +// OutputRules groups together an OutputRule per generator, plus a default +// output rule for any not explicitly specified. +// +// OutputRules are defined for stdout, file writing, and sending to /dev/null +// (useful for doing "type-checking" without actually saving the results). +// +// InputRule defines custom input loading, but its shared across all +// Generators. There's currently only a filesystem implementation. +// +// # Runtime and Context +// +// Runtime maps together Generators, and constructs "contexts" which provide +// the common collector and roots, plus the output rule for that generator, and +// a handle for reading files (like boilerplate headers). +// +// It will run all associated generators, printing errors and automatically +// skipping type-checking errors (since those are commonly caused by the +// partial type-checking of loader.TypeChecker). +// +// # Options +// +// The FromOptions (and associated helpers) function makes it easy to use generators +// and output rules as markers that can be parsed from the command line, producing +// a registry from command line args. +package genall diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/genall.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/genall.go new file mode 100644 index 0000000000..63afbac07c --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/genall.go @@ -0,0 +1,269 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package genall + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + + "golang.org/x/tools/go/packages" + rawyaml "gopkg.in/yaml.v2" + + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// Generators are a list of Generators. +// NB(directxman12): this is a pointer so that we can uniquely identify each +// instance of a generator, even if it's not hashable. Different *instances* +// of a generator are treated differently. +type Generators []*Generator + +// RegisterMarkers registers all markers defined by each of the Generators in +// this list into the given registry. +func (g Generators) RegisterMarkers(reg *markers.Registry) error { + for _, gen := range g { + if err := (*gen).RegisterMarkers(reg); err != nil { + return err + } + } + return nil +} + +// CheckFilters returns the set of NodeFilters for all Generators that +// implement NeedsTypeChecking. +func (g Generators) CheckFilters() []loader.NodeFilter { + var filters []loader.NodeFilter + for _, gen := range g { + withFilter, needsChecking := (*gen).(NeedsTypeChecking) + if !needsChecking { + continue + } + filters = append(filters, withFilter.CheckFilter()) + } + return filters +} + +// NeedsTypeChecking indicates that a particular generator needs & has opinions +// on typechecking. If this is not implemented, a generator will be given a +// context with a nil typechecker. +type NeedsTypeChecking interface { + // CheckFilter indicates the loader.NodeFilter (if any) that should be used + // to prune out unused types/packages when type-checking (nodes for which + // the filter returns true are considered "interesting"). This filter acts + // as a baseline -- all types the pass through this filter will be checked, + // but more than that may also be checked due to other generators' filters. + CheckFilter() loader.NodeFilter +} + +// Generator knows how to register some set of markers, and then produce +// output artifacts based on loaded code containing those markers, +// sharing common loaded data. +type Generator interface { + // RegisterMarkers registers all markers needed by this Generator + // into the given registry. + RegisterMarkers(into *markers.Registry) error + // Generate generates artifacts produced by this marker. + // It's called *after* RegisterMarkers has been called. + Generate(*GenerationContext) error +} + +// HasHelp is some Generator, OutputRule, etc with a help method. +type HasHelp interface { + // Help returns help for this generator. + Help() *markers.DefinitionHelp +} + +// Runtime collects generators, loaded program data (Collector, root Packages), +// and I/O rules, running them together. +type Runtime struct { + // Generators are the Generators to be run by this Runtime. + Generators Generators + // GenerationContext is the base generation context that's copied + // to produce the context for each Generator. + GenerationContext + // OutputRules defines how to output artifacts for each Generator. + OutputRules OutputRules + // ErrorWriter defines where to write error messages. + ErrorWriter io.Writer +} + +// GenerationContext defines the common information needed for each Generator +// to run. +type GenerationContext struct { + // Collector is the shared marker collector. + Collector *markers.Collector + // Roots are the base packages to be processed. + Roots []*loader.Package + // Checker is the shared partial type-checker. + Checker *loader.TypeChecker + // OutputRule describes how to output artifacts. + OutputRule + // InputRule describes how to load associated boilerplate artifacts. + // It should *not* be used to load source files. + InputRule +} + +// WriteYAMLOptions implements the Options Pattern for WriteYAML. +type WriteYAMLOptions struct { + transform func(obj map[string]interface{}) error +} + +// WithTransform applies a transformation to objects just before writing them. +func WithTransform(transform func(obj map[string]interface{}) error) *WriteYAMLOptions { + return &WriteYAMLOptions{ + transform: transform, + } +} + +// WriteYAML writes the given objects out, serialized as YAML, using the +// context's OutputRule. Objects are written as separate documents, separated +// from each other by `---` (as per the YAML spec). +func (g GenerationContext) WriteYAML(itemPath string, objs []interface{}, options ...*WriteYAMLOptions) error { + out, err := g.Open(nil, itemPath) + if err != nil { + return err + } + defer out.Close() + + for _, obj := range objs { + yamlContent, err := yamlMarshal(obj, options...) + if err != nil { + return err + } + n, err := out.Write(append([]byte("---\n"), yamlContent...)) + if err != nil { + return err + } + if n < len(yamlContent) { + return io.ErrShortWrite + } + } + + return nil +} + +// yamlMarshal is based on sigs.k8s.io/yaml.Marshal, but allows for transforming the final data before writing. +func yamlMarshal(o interface{}, options ...*WriteYAMLOptions) ([]byte, error) { + j, err := json.Marshal(o) + if err != nil { + return nil, fmt.Errorf("error marshaling into JSON: %v", err) + } + + return yamlJSONToYAMLWithFilter(j, options...) +} + +// yamlJSONToYAMLWithFilter is based on sigs.k8s.io/yaml.JSONToYAML, but allows for transforming the final data before writing. +func yamlJSONToYAMLWithFilter(j []byte, options ...*WriteYAMLOptions) ([]byte, error) { + // Convert the JSON to an object. + var jsonObj map[string]interface{} + // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the + // Go JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshalling to interface{}, it just picks float64 + // universally. go-yaml does go through the effort of picking the right + // number type, so we can preserve number type throughout this process. + if err := rawyaml.Unmarshal(j, &jsonObj); err != nil { + return nil, err + } + + for _, option := range options { + if option.transform != nil { + if err := option.transform(jsonObj); err != nil { + return nil, err + } + } + } + + // Marshal this object into YAML. + return rawyaml.Marshal(jsonObj) +} + +// ReadFile reads the given boilerplate artifact using the context's InputRule. +func (g GenerationContext) ReadFile(path string) ([]byte, error) { + file, err := g.OpenForRead(path) + if err != nil { + return nil, err + } + defer file.Close() + return ioutil.ReadAll(file) +} + +// ForRoots produces a Runtime to run the given generators against the +// given packages. It outputs to /dev/null by default. +func (g Generators) ForRoots(rootPaths ...string) (*Runtime, error) { + roots, err := loader.LoadRoots(rootPaths...) + if err != nil { + return nil, err + } + rt := &Runtime{ + Generators: g, + GenerationContext: GenerationContext{ + Collector: &markers.Collector{ + Registry: &markers.Registry{}, + }, + Roots: roots, + InputRule: InputFromFileSystem, + Checker: &loader.TypeChecker{ + NodeFilters: g.CheckFilters(), + }, + }, + OutputRules: OutputRules{Default: OutputToNothing}, + } + if err := rt.Generators.RegisterMarkers(rt.Collector.Registry); err != nil { + return nil, err + } + return rt, nil +} + +// Run runs the Generators in this Runtime against its packages, printing +// errors (except type errors, which common result from using TypeChecker with +// filters), returning true if errors were found. +func (r *Runtime) Run() bool { + // TODO(directxman12): we could make this parallel, + // but we'd need to ensure all underlying machinery is threadsafe + + if r.ErrorWriter == nil { + r.ErrorWriter = os.Stderr + } + if len(r.Generators) == 0 { + fmt.Fprintln(r.ErrorWriter, "no generators to run") + return true + } + + hadErrs := false + for _, gen := range r.Generators { + ctx := r.GenerationContext // make a shallow copy + ctx.OutputRule = r.OutputRules.ForGenerator(gen) + + // don't pass a typechecker to generators that don't provide a filter + // to avoid accidents + if _, needsChecking := (*gen).(NeedsTypeChecking); !needsChecking { + ctx.Checker = nil + } + + if err := (*gen).Generate(&ctx); err != nil { + fmt.Fprintln(r.ErrorWriter, err) + hadErrs = true + } + } + + // skip TypeErrors -- they're probably just from partial typechecking in crd-gen + return loader.PrintErrors(r.Roots, packages.TypeError) || hadErrs +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/doc.go new file mode 100644 index 0000000000..d84d1798b4 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package help contains utilities for actually writing out marker help. +// +// Namely, it contains a series of structs (and helpers for producing them) +// that represent a merged view of marker definition and help that can be used +// for consumption by the pretty subpackage (for terminal help) or serialized +// as JSON (e.g. for generating HTML help). +package help diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/doc.go new file mode 100644 index 0000000000..bc99e9ba99 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/doc.go @@ -0,0 +1,30 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package pretty contains utilities for formatting terminal help output, +// and a use of those to display marker help. +// +// # Terminal Output +// +// The Span interface and Table struct allow you to construct tables with +// colored formatting, without causing ANSI formatting characters to mess up width +// calculations. +// +// # Marker Help +// +// The MarkersSummary prints a summary table for marker help, while the MarkersDetails +// prints out more detailed information, with explainations of the individual marker fields. +package pretty diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/help.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/help.go new file mode 100644 index 0000000000..3e34cc0d6c --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/help.go @@ -0,0 +1,171 @@ +package pretty + +import ( + "fmt" + "io" + + "sigs.k8s.io/controller-tools/pkg/genall/help" + + "github.com/fatih/color" +) + +var ( + headingStyle = Decoration(*color.New(color.Bold, color.Underline)) + markerNameStyle = Decoration(*color.New(color.Bold)) + fieldSummaryStyle = Decoration(*color.New(color.FgGreen, color.Italic)) + markerTargetStyle = Decoration(*color.New(color.Faint)) + fieldDetailStyle = Decoration(*color.New(color.Italic, color.FgGreen)) + deprecatedStyle = Decoration(*color.New(color.CrossedOut)) +) + +// MarkersSummary returns a condensed summary of help for the given markers. +func MarkersSummary(groupName string, markers []help.MarkerDoc) Span { + out := new(SpanWriter) + + out.Print(Text("\n")) + out.Print(headingStyle.Containing(Text(groupName))) + out.Print(Text("\n\n")) + + table := &Table{Sizing: &TableCalculator{Padding: 2}} + for _, marker := range markers { + table.StartRow() + table.Column(MarkerSyntaxHelp(marker)) + table.Column(markerTargetStyle.Containing(Text(marker.Target))) + + summary := new(SpanWriter) + if marker.DeprecatedInFavorOf != nil && len(*marker.DeprecatedInFavorOf) > 0 { + summary.Print(markerNameStyle.Containing(Text("(use "))) + summary.Print(markerNameStyle.Containing(Text(*marker.DeprecatedInFavorOf))) + summary.Print(markerNameStyle.Containing(Text(") "))) + } + summary.Print(Text(marker.Summary)) + table.Column(summary) + + table.EndRow() + } + out.Print(table) + + out.Print(Text("\n")) + + return out +} + +// MarkersDetails returns detailed help for the given markers, including detailed field help. +func MarkersDetails(fullDetail bool, groupName string, markers []help.MarkerDoc) Span { + out := new(SpanWriter) + + out.Print(Line(headingStyle.Containing(Text(groupName)))) + out.Print(Newlines(2)) + + for _, marker := range markers { + out.Print(Line(markerName(marker))) + out.Print(Text(" ")) + out.Print(markerTargetStyle.Containing(Text(marker.Target))) + + summary := new(SpanWriter) + if marker.DeprecatedInFavorOf != nil && len(*marker.DeprecatedInFavorOf) > 0 { + summary.Print(markerNameStyle.Containing(Text("(use "))) + summary.Print(markerNameStyle.Containing(Text(*marker.DeprecatedInFavorOf))) + summary.Print(markerNameStyle.Containing(Text(") "))) + } + summary.Print(Text(marker.Summary)) + + if !marker.AnonymousField() { + out.Print(Indented(1, Line(summary))) + if len(marker.Details) > 0 && fullDetail { + out.Print(Indented(1, Line(Text(marker.Details)))) + } + } + + if marker.AnonymousField() { + out.Print(Indented(1, Line(fieldDetailStyle.Containing(FieldSyntaxHelp(marker.Fields[0]))))) + out.Print(Text(" ")) + out.Print(summary) + if len(marker.Details) > 0 && fullDetail { + out.Print(Indented(2, Line(Text(marker.Details)))) + } + out.Print(Newlines(1)) + } else if !marker.Empty() { + out.Print(Newlines(1)) + if fullDetail { + for _, arg := range marker.Fields { + out.Print(Indented(1, Line(fieldDetailStyle.Containing(FieldSyntaxHelp(arg))))) + out.Print(Indented(2, Line(Text(arg.Summary)))) + if len(arg.Details) > 0 && fullDetail { + out.Print(Indented(2, Line(Text(arg.Details)))) + out.Print(Newlines(1)) + } + } + out.Print(Newlines(1)) + } else { + table := &Table{Sizing: &TableCalculator{Padding: 2}} + for _, arg := range marker.Fields { + table.StartRow() + table.Column(fieldDetailStyle.Containing(FieldSyntaxHelp(arg))) + table.Column(Text(arg.Summary)) + table.EndRow() + } + + out.Print(Indented(1, table)) + } + } else { + out.Print(Newlines(1)) + } + } + + return out +} + +func FieldSyntaxHelp(arg help.FieldHelp) Span { + return fieldSyntaxHelp(arg, "") +} + +// fieldSyntaxHelp prints the syntax help for a particular marker argument. +func fieldSyntaxHelp(arg help.FieldHelp, sep string) Span { + if arg.Optional { + return FromWriter(func(out io.Writer) error { + _, err := fmt.Fprintf(out, "[%s%s=<%s>]", sep, arg.Name, arg.TypeString()) + return err + }) + } + return FromWriter(func(out io.Writer) error { + _, err := fmt.Fprintf(out, "%s%s=<%s>", sep, arg.Name, arg.TypeString()) + return err + }) +} + +// markerName returns a span containing just the appropriately-formatted marker name. +func markerName(def help.MarkerDoc) Span { + if def.DeprecatedInFavorOf != nil { + return deprecatedStyle.Containing(Text("+" + def.Name)) + } + return markerNameStyle.Containing(Text("+" + def.Name)) +} + +// MarkerSyntaxHelp assembles syntax help for a given marker. +func MarkerSyntaxHelp(def help.MarkerDoc) Span { + out := new(SpanWriter) + + out.Print(markerName(def)) + + if def.Empty() { + return out + } + + sep := ":" + if def.AnonymousField() { + sep = "" + } + + fieldStyle := fieldSummaryStyle + if def.DeprecatedInFavorOf != nil { + fieldStyle = deprecatedStyle + } + + for _, arg := range def.Fields { + out.Print(fieldStyle.Containing(fieldSyntaxHelp(arg, sep))) + sep = "," + } + + return out +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/print.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/print.go new file mode 100644 index 0000000000..8d7452a0b2 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/print.go @@ -0,0 +1,304 @@ +package pretty + +import ( + "bytes" + "fmt" + "io" + + "github.com/fatih/color" +) + +// NB(directxman12): this isn't particularly elegant, but it's also +// sufficiently simple as to be maintained here. Man (roff) would've +// probably worked, but it's not necessarily on Windows by default. + +// Span is a chunk of content that is writable to an output, but knows how to +// calculate its apparent visual "width" on the terminal (not to be confused +// with the raw length, which may include zero-width coloring sequences). +type Span interface { + // VisualLength reports the "width" as perceived by the user on the terminal + // (i.e. widest line, ignoring ANSI escape characters). + VisualLength() int + // WriteTo writes the full span contents to the given writer. + WriteTo(io.Writer) error +} + +// Table is a Span that writes its data in table form, with sizing controlled +// by the given table calculator. Rows are started with StartRow, followed by +// some calls to Column, followed by a call to EndRow. Once all rows are +// added, the table can be used as a Span. +type Table struct { + Sizing *TableCalculator + + cellsByRow [][]Span + colSizes []int +} + +// StartRow starts a new row. +// It must eventually be followed by EndRow. +func (t *Table) StartRow() { + t.cellsByRow = append(t.cellsByRow, []Span(nil)) +} + +// EndRow ends the currently started row. +func (t *Table) EndRow() { + lastRow := t.cellsByRow[len(t.cellsByRow)-1] + sizes := make([]int, len(lastRow)) + for i, cell := range lastRow { + sizes[i] = cell.VisualLength() + } + t.Sizing.AddRowSizes(sizes...) +} + +// Column adds the given span as a new column to the current row. +func (t *Table) Column(contents Span) { + currentRowInd := len(t.cellsByRow) - 1 + t.cellsByRow[currentRowInd] = append(t.cellsByRow[currentRowInd], contents) +} + +// SkipRow prints a span without having it contribute to the table calculation. +func (t *Table) SkipRow(contents Span) { + t.cellsByRow = append(t.cellsByRow, []Span{contents}) +} + +func (t *Table) WriteTo(out io.Writer) error { + if t.colSizes == nil { + t.colSizes = t.Sizing.ColumnWidths() + } + + for _, cells := range t.cellsByRow { + currentPosition := 0 + for colInd, cell := range cells { + colSize := t.colSizes[colInd] + diff := colSize - cell.VisualLength() + + if err := cell.WriteTo(out); err != nil { + return err + } + + if diff > 0 { + if err := writePadding(out, columnPadding, diff); err != nil { + return err + } + } + currentPosition += colSize + } + + if _, err := fmt.Fprint(out, "\n"); err != nil { + return err + } + } + + return nil +} + +func (t *Table) VisualLength() int { + if t.colSizes == nil { + t.colSizes = t.Sizing.ColumnWidths() + } + + res := 0 + for _, colSize := range t.colSizes { + res += colSize + } + return res +} + +// Text is a span that simply contains raw text. It's a good starting point. +type Text string + +func (t Text) VisualLength() int { return len(t) } +func (t Text) WriteTo(w io.Writer) error { + _, err := w.Write([]byte(t)) + return err +} + +// indented is a span that indents all lines by the given number of tabs. +type indented struct { + Amount int + Content Span +} + +func (i *indented) VisualLength() int { return i.Content.VisualLength() } +func (i *indented) WriteTo(w io.Writer) error { + var out bytes.Buffer + if err := i.Content.WriteTo(&out); err != nil { + return err + } + + lines := bytes.Split(out.Bytes(), []byte("\n")) + for lineInd, line := range lines { + if lineInd != 0 { + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + } + if len(line) == 0 { + continue + } + + if err := writePadding(w, indentPadding, i.Amount); err != nil { + return err + } + if _, err := w.Write(line); err != nil { + return err + } + } + return nil +} + +// Indented returns a span that indents all lines by the given number of tabs. +func Indented(amt int, content Span) Span { + return &indented{Amount: amt, Content: content} +} + +// fromWriter is a span that takes content from a function expecting a Writer. +type fromWriter struct { + cache []byte + cacheError error + run func(io.Writer) error +} + +func (f *fromWriter) VisualLength() int { + if f.cache == nil { + var buf bytes.Buffer + if err := f.run(&buf); err != nil { + f.cacheError = err + } + f.cache = buf.Bytes() + } + return len(f.cache) +} +func (f *fromWriter) WriteTo(w io.Writer) error { + if f.cache != nil { + if f.cacheError != nil { + return f.cacheError + } + _, err := w.Write(f.cache) + return err + } + return f.run(w) +} + +// FromWriter returns a span that takes content from a function expecting a Writer. +func FromWriter(run func(io.Writer) error) Span { + return &fromWriter{run: run} +} + +// Decoration represents a terminal decoration. +type Decoration color.Color + +// Containing returns a Span that has the given decoration applied. +func (d Decoration) Containing(contents Span) Span { + return &decorated{ + Contents: contents, + Attributes: color.Color(d), + } +} + +// decorated is a span that has some terminal decoration applied. +type decorated struct { + Contents Span + Attributes color.Color +} + +func (d *decorated) VisualLength() int { return d.Contents.VisualLength() } +func (d *decorated) WriteTo(w io.Writer) error { + oldOut := color.Output + color.Output = w + defer func() { color.Output = oldOut }() + + d.Attributes.Set() + defer color.Unset() + + return d.Contents.WriteTo(w) +} + +// SpanWriter is a span that contains multiple sub-spans. +type SpanWriter struct { + contents []Span +} + +func (m *SpanWriter) VisualLength() int { + res := 0 + for _, span := range m.contents { + res += span.VisualLength() + } + return res +} +func (m *SpanWriter) WriteTo(w io.Writer) error { + for _, span := range m.contents { + if err := span.WriteTo(w); err != nil { + return err + } + } + return nil +} + +// Print adds a new span to this SpanWriter. +func (m *SpanWriter) Print(s Span) { + m.contents = append(m.contents, s) +} + +// lines is a span that adds some newlines, optionally followed by some content. +type lines struct { + content Span + amountBefore int +} + +func (l *lines) VisualLength() int { + if l.content == nil { + return 0 + } + return l.content.VisualLength() +} +func (l *lines) WriteTo(w io.Writer) error { + if err := writePadding(w, linesPadding, l.amountBefore); err != nil { + return err + } + if l.content != nil { + if err := l.content.WriteTo(w); err != nil { + return err + } + } + return nil +} + +// Newlines returns a span just containing some newlines. +func Newlines(amt int) Span { + return &lines{amountBefore: amt} +} + +// Line returns a span that emits a newline, followed by the given content. +func Line(content Span) Span { + return &lines{amountBefore: 1, content: content} +} + +var ( + columnPadding = []byte(" ") + indentPadding = []byte("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t") + linesPadding = []byte("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") +) + +// writePadding writes out padding of the given type in the given amount to the writer. +// Each byte in the padding buffer contributes 1 to the amount -- the padding being +// a buffer is just for efficiency. +func writePadding(out io.Writer, typ []byte, amt int) error { + if amt <= len(typ) { + _, err := out.Write(typ[:amt]) + return err + } + + num := amt / len(typ) + rem := amt % len(typ) + for i := 0; i < num; i++ { + if _, err := out.Write(typ); err != nil { + return err + } + } + + if _, err := out.Write(typ[:rem]); err != nil { + return err + } + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/table.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/table.go new file mode 100644 index 0000000000..5a0b4752af --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/pretty/table.go @@ -0,0 +1,64 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pretty + +// TableCalculator calculates column widths (with optional padding) +// for a table based on the maximum required column width. +type TableCalculator struct { + cellSizesByCol [][]int + + Padding int + MaxWidth int +} + +// AddRowSizes registers a new row with cells of the given sizes. +func (c *TableCalculator) AddRowSizes(cellSizes ...int) { + if len(cellSizes) > len(c.cellSizesByCol) { + for range cellSizes[len(c.cellSizesByCol):] { + c.cellSizesByCol = append(c.cellSizesByCol, []int(nil)) + } + } + for i, size := range cellSizes { + c.cellSizesByCol[i] = append(c.cellSizesByCol[i], size) + } +} + +// ColumnWidths calculates the appropriate column sizes given the +// previously registered rows. +func (c *TableCalculator) ColumnWidths() []int { + maxColWidths := make([]int, len(c.cellSizesByCol)) + + for colInd, cellSizes := range c.cellSizesByCol { + max := 0 + for _, cellSize := range cellSizes { + if max < cellSize { + max = cellSize + } + } + maxColWidths[colInd] = max + } + + actualMaxWidth := c.MaxWidth - c.Padding + for i, width := range maxColWidths { + if actualMaxWidth > 0 && width > actualMaxWidth { + maxColWidths[i] = actualMaxWidth + } + maxColWidths[i] += c.Padding + } + + return maxColWidths +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/sort.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/sort.go new file mode 100644 index 0000000000..53c923e34b --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/sort.go @@ -0,0 +1,106 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package help + +import ( + "strings" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// SortGroup knows how to sort and group marker definitions. +type SortGroup interface { + // Less is equivalent to the Less function from sort, and is used to sort the markers. + Less(*markers.Definition, *markers.Definition) bool + // Group returns the "group" that a given marker belongs to. + Group(*markers.Definition, *markers.DefinitionHelp) string +} + +var ( + // SortByCategory sorts the markers by name and groups them by their help category. + SortByCategory = sortByCategory{} + + // SortByOption sorts by the generator that the option belongs to. + SortByOption = optionsSort{} +) + +type sortByCategory struct{} + +func (sortByCategory) Group(_ *markers.Definition, help *markers.DefinitionHelp) string { + if help == nil { + return "" + } + return help.Category +} +func (sortByCategory) Less(i, j *markers.Definition) bool { + return i.Name < j.Name +} + +type optionsSort struct{} + +func (optionsSort) Less(i, j *markers.Definition) bool { + iParts := strings.Split(i.Name, ":") + jParts := strings.Split(j.Name, ":") + + iGen := "" + iRule := "" + jGen := "" + jRule := "" + + switch len(iParts) { + case 1: + iGen = iParts[0] + // two means a default output rule, so ignore + case 2: + iRule = iParts[1] + case 3: + iGen = iParts[1] + iRule = iParts[2] + } + switch len(jParts) { + case 1: + jGen = jParts[0] + // two means a default output rule, so ignore + case 2: + jRule = jParts[1] + case 3: + jGen = jParts[1] + jRule = jParts[2] + } + + if iGen != jGen { + return iGen > jGen + } + + return iRule < jRule +} +func (optionsSort) Group(def *markers.Definition, _ *markers.DefinitionHelp) string { + parts := strings.Split(def.Name, ":") + + switch len(parts) { + case 1: + if parts[0] == "paths" { + return "generic" + } + return "generators" + case 2: + return "output rules (optionally as output::...)" + default: + return "" + // three means a marker-specific output rule, ignore + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/types.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/types.go new file mode 100644 index 0000000000..be11104324 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/help/types.go @@ -0,0 +1,215 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package help + +import ( + "sort" + "strings" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// DetailedHelp contains both a summary and further details. +type DetailedHelp struct { + // Summary contains a one-line description. + Summary string `json:"summary"` + // Details contains further information. + Details string `json:"details,omitempty"` +} + +// Argument is the type data for a marker argument. +type Argument struct { + // Type is the data type of the argument (string, bool, int, slice, any, raw, invalid) + Type string `json:"type"` + // Optional marks this argument as optional. + Optional bool `json:"optional"` + // ItemType contains the type of the slice item, if this is a slice + ItemType *Argument `json:"itemType,omitempty"` +} + +func (a Argument) typeString(out *strings.Builder) { + if a.Type == "slice" { + out.WriteString("[]") + a.ItemType.typeString(out) + return + } + + out.WriteString(a.Type) +} + +// TypeString returns a string roughly equivalent +// (but not identical) to the underlying Go type that +// this argument would parse to. It's mainly useful +// for user-friendly formatting of this argument (e.g. +// help strings). +func (a Argument) TypeString() string { + out := &strings.Builder{} + a.typeString(out) + return out.String() +} + +// FieldHelp contains information required to print documentation for a marker field. +type FieldHelp struct { + // Name is the field name. + Name string `json:"name"` + // Argument is the type of the field. + Argument `json:",inline"` + + // DetailedHelp contains the textual help for the field. + DetailedHelp `json:",inline"` +} + +// MarkerDoc contains information required to print documentation for a marker. +type MarkerDoc struct { + // definition + + // Name is the name of the marker. + Name string `json:"name"` + // Target is the target (field, package, type) of the marker. + Target string `json:"target"` + + // help + + // DetailedHelp is the textual help for the marker. + DetailedHelp `json:",inline"` + // Category is the general "category" that this marker belongs to. + Category string `json:"category"` + // DeprecatedInFavorOf marks that this marker shouldn't be used when + // non-nil. If also non-empty, another marker should be used instead. + DeprecatedInFavorOf *string `json:"deprecatedInFavorOf,omitempty"` + // Fields is the type and help data for each field of this marker. + Fields []FieldHelp `json:"fields,omitempty"` +} + +// Empty checks if this marker has any arguments, returning true if not. +func (m MarkerDoc) Empty() bool { + return len(m.Fields) == 0 +} + +// AnonymousField chekcs if this is an single-valued marker +// (as opposed to having named fields). +func (m MarkerDoc) AnonymousField() bool { + return len(m.Fields) == 1 && m.Fields[0].Name == "" +} + +// ForArgument returns the equivalent documentation for a marker argument. +func ForArgument(argRaw markers.Argument) Argument { + res := Argument{ + Optional: argRaw.Optional, + } + + if argRaw.ItemType != nil { + itemType := ForArgument(*argRaw.ItemType) + res.ItemType = &itemType + } + + switch argRaw.Type { + case markers.IntType: + res.Type = "int" + case markers.StringType: + res.Type = "string" + case markers.BoolType: + res.Type = "bool" + case markers.AnyType: + res.Type = "any" + case markers.SliceType: + res.Type = "slice" + case markers.RawType: + res.Type = "raw" + case markers.InvalidType: + res.Type = "invalid" + } + + return res +} + +// ForDefinition returns the equivalent marker documentation for a given marker definition and spearate help. +func ForDefinition(defn *markers.Definition, maybeHelp *markers.DefinitionHelp) MarkerDoc { + var help markers.DefinitionHelp + if maybeHelp != nil { + help = *maybeHelp + } + + res := MarkerDoc{ + Name: defn.Name, + Category: help.Category, + DeprecatedInFavorOf: help.DeprecatedInFavorOf, + Target: defn.Target.String(), + DetailedHelp: DetailedHelp{Summary: help.Summary, Details: help.Details}, + } + + helpByField := help.FieldsHelp(defn) + + // TODO(directxman12): deterministic ordering + for fieldName, fieldHelpRaw := range helpByField { + fieldInfo := defn.Fields[fieldName] + fieldHelp := FieldHelp{ + Name: fieldName, + DetailedHelp: DetailedHelp{Summary: fieldHelpRaw.Summary, Details: fieldHelpRaw.Details}, + Argument: ForArgument(fieldInfo), + } + + res.Fields = append(res.Fields, fieldHelp) + } + + sort.Slice(res.Fields, func(i, j int) bool { return res.Fields[i].Name < res.Fields[j].Name }) + + return res +} + +// CategoryDoc contains help information for all markers in a Category. +type CategoryDoc struct { + Category string `json:"category"` + Markers []MarkerDoc `json:"markers"` +} + +// ByCategory returns the marker help for markers in the given +// registry, grouped and sorted according to the given method. +func ByCategory(reg *markers.Registry, sorter SortGroup) []CategoryDoc { + groupedMarkers := make(map[string][]*markers.Definition) + + for _, marker := range reg.AllDefinitions() { + group := sorter.Group(marker, reg.HelpFor(marker)) + groupedMarkers[group] = append(groupedMarkers[group], marker) + } + allGroups := make([]string, 0, len(groupedMarkers)) + for groupName := range groupedMarkers { + allGroups = append(allGroups, groupName) + } + + sort.Strings(allGroups) + + res := make([]CategoryDoc, len(allGroups)) + for i, groupName := range allGroups { + markers := groupedMarkers[groupName] + sort.Slice(markers, func(i, j int) bool { + return sorter.Less(markers[i], markers[j]) + }) + + markerDocs := make([]MarkerDoc, len(markers)) + for i, marker := range markers { + markerDocs[i] = ForDefinition(marker, reg.HelpFor(marker)) + } + + res[i] = CategoryDoc{ + Category: groupName, + Markers: markerDocs, + } + } + + return res +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/input.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/input.go new file mode 100644 index 0000000000..46e191c0c2 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/input.go @@ -0,0 +1,37 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package genall + +import ( + "io" + "os" +) + +// InputRule describes how to load non-code boilerplate artifacts. +// It's not used for loading code. +type InputRule interface { + // OpenForRead opens the given non-code artifact for reading. + OpenForRead(path string) (io.ReadCloser, error) +} +type inputFromFileSystem struct{} + +func (inputFromFileSystem) OpenForRead(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +// InputFromFileSystem reads from the filesystem as normal. +var InputFromFileSystem = inputFromFileSystem{} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/options.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/options.go new file mode 100644 index 0000000000..836b1617e6 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/options.go @@ -0,0 +1,194 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package genall + +import ( + "fmt" + "strings" + + "sigs.k8s.io/controller-tools/pkg/markers" +) + +var ( + InputPathsMarker = markers.Must(markers.MakeDefinition("paths", markers.DescribesPackage, InputPaths(nil))) +) + +// +controllertools:marker:generateHelp:category="" + +// InputPaths represents paths and go-style path patterns to use as package roots. +// +// Multiple paths can be specified using "{path1, path2, path3}". +type InputPaths []string + +// RegisterOptionsMarkers registers "mandatory" options markers for FromOptions into the given registry. +// At this point, that's just InputPaths. +func RegisterOptionsMarkers(into *markers.Registry) error { + if err := into.Register(InputPathsMarker); err != nil { + return err + } + // NB(directxman12): we make this optional so we don't have a bootstrap problem with helpgen + if helpGiver, hasHelp := ((interface{})(InputPaths(nil))).(HasHelp); hasHelp { + into.AddHelp(InputPathsMarker, helpGiver.Help()) + } + return nil +} + +// RegistryFromOptions produces just the marker registry that would be used by FromOptions, without +// attempting to produce a full Runtime. This can be useful if you want to display help without +// trying to load roots. +func RegistryFromOptions(optionsRegistry *markers.Registry, options []string) (*markers.Registry, error) { + protoRt, err := protoFromOptions(optionsRegistry, options) + if err != nil { + return nil, err + } + reg := &markers.Registry{} + if err := protoRt.Generators.RegisterMarkers(reg); err != nil { + return nil, err + } + return reg, nil +} + +// FromOptions parses the options from markers stored in the given registry out into a runtime. +// The markers in the registry must be either +// +// a) Generators +// b) OutputRules +// c) InputPaths +// +// The paths specified in InputPaths are loaded as package roots, and the combined with +// the generators and the specified output rules to produce a runtime that can be run or +// further modified. Not default generators are used if none are specified -- you can check +// the output and rerun for that. +func FromOptions(optionsRegistry *markers.Registry, options []string) (*Runtime, error) { + + protoRt, err := protoFromOptions(optionsRegistry, options) + if err != nil { + return nil, err + } + + // make the runtime + genRuntime, err := protoRt.Generators.ForRoots(protoRt.Paths...) + if err != nil { + return nil, err + } + + // attempt to figure out what the user wants without a lot of verbose specificity: + // if the user specifies a default rule, assume that they probably want to fall back + // to that. Otherwise, assume that they just wanted to customize one option from the + // set, and leave the rest in the standard configuration. + if protoRt.OutputRules.Default != nil { + genRuntime.OutputRules = protoRt.OutputRules + return genRuntime, nil + } + + outRules := DirectoryPerGenerator("config", protoRt.GeneratorsByName) + for gen, rule := range protoRt.OutputRules.ByGenerator { + outRules.ByGenerator[gen] = rule + } + + genRuntime.OutputRules = outRules + return genRuntime, nil +} + +// protoFromOptions returns a proto-Runtime from the given options registry and +// options set. This can then be used to construct an actual Runtime. See the +// FromOptions function for more details about how the options work. +func protoFromOptions(optionsRegistry *markers.Registry, options []string) (protoRuntime, error) { + var gens Generators + rules := OutputRules{ + ByGenerator: make(map[*Generator]OutputRule), + } + var paths []string + + // collect the generators first, so that we can key the output on the actual + // generator, which matters if there's settings in the gen object and it's not a pointer. + outputByGen := make(map[string]OutputRule) + gensByName := make(map[string]*Generator) + + for _, rawOpt := range options { + if rawOpt[0] != '+' { + rawOpt = "+" + rawOpt // add a `+` to make it acceptable for usage with the registry + } + defn := optionsRegistry.Lookup(rawOpt, markers.DescribesPackage) + if defn == nil { + return protoRuntime{}, fmt.Errorf("unknown option %q", rawOpt[1:]) + } + + val, err := defn.Parse(rawOpt) + if err != nil { + return protoRuntime{}, fmt.Errorf("unable to parse option %q: %w", rawOpt[1:], err) + } + + switch val := val.(type) { + case Generator: + gens = append(gens, &val) + gensByName[defn.Name] = &val + case OutputRule: + _, genName := splitOutputRuleOption(defn.Name) + if genName == "" { + // it's a default rule + rules.Default = val + continue + } + + outputByGen[genName] = val + continue + case InputPaths: + paths = append(paths, val...) + default: + return protoRuntime{}, fmt.Errorf("unknown option marker %q", defn.Name) + } + } + + // actually associate the rules now that we know the generators + for genName, outputRule := range outputByGen { + gen, knownGen := gensByName[genName] + if !knownGen { + return protoRuntime{}, fmt.Errorf("non-invoked generator %q", genName) + } + + rules.ByGenerator[gen] = outputRule + } + + return protoRuntime{ + Paths: paths, + Generators: Generators(gens), + OutputRules: rules, + GeneratorsByName: gensByName, + }, nil +} + +// protoRuntime represents the raw pieces needed to compose a runtime, as +// parsed from some options. +type protoRuntime struct { + Paths []string + Generators Generators + OutputRules OutputRules + GeneratorsByName map[string]*Generator +} + +// splitOutputRuleOption splits a marker name of "output:rule:gen" or "output:rule" +// into its compontent rule and generator name. +func splitOutputRuleOption(name string) (ruleName string, genName string) { + parts := strings.SplitN(name, ":", 3) + if len(parts) == 3 { + // output:: + return parts[2], parts[1] + } + // output: + return parts[1], "" +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/output.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/output.go new file mode 100644 index 0000000000..5dc3fe19cd --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/output.go @@ -0,0 +1,160 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package genall + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// nopCloser is a WriteCloser whose Close +// is a no-op. +type nopCloser struct { + io.Writer +} + +func (n nopCloser) Close() error { + return nil +} + +// DirectoryPerGenerator produces output rules mapping output to a different subdirectory +// of the given base directory for each generator (with each subdirectory specified as +// the key in the input map). +func DirectoryPerGenerator(base string, generators map[string]*Generator) OutputRules { + rules := OutputRules{ + Default: OutputArtifacts{Config: OutputToDirectory(base)}, + ByGenerator: make(map[*Generator]OutputRule, len(generators)), + } + + for name, gen := range generators { + rules.ByGenerator[gen] = OutputArtifacts{ + Config: OutputToDirectory(filepath.Join(base, name)), + } + } + + return rules +} + +// OutputRules defines how to output artificats on a per-generator basis. +type OutputRules struct { + // Default is the output rule used when no specific per-generator overrides match. + Default OutputRule + // ByGenerator contains specific per-generator overrides. + // NB(directxman12): this is a pointer to avoid issues if a given Generator becomes unhashable + // (interface values compare by "dereferencing" their internal pointer first, whereas pointers + // compare by the actual pointer itself). + ByGenerator map[*Generator]OutputRule +} + +// ForGenerator returns the output rule that should be used +// by the given Generator. +func (o OutputRules) ForGenerator(gen *Generator) OutputRule { + if forGen, specific := o.ByGenerator[gen]; specific { + return forGen + } + return o.Default +} + +// OutputRule defines how to output artifacts from a generator. +type OutputRule interface { + // Open opens the given artifact path for writing. If a package is passed, + // the artifact is considered to be used as part of the package (e.g. + // generated code), while a nil package indicates that the artifact is + // config (or something else not involved in Go compilation). + Open(pkg *loader.Package, path string) (io.WriteCloser, error) +} + +// OutputToNothing skips outputting anything. +var OutputToNothing = outputToNothing{} + +// +controllertools:marker:generateHelp:category="" + +// outputToNothing skips outputting anything. +type outputToNothing struct{} + +func (o outputToNothing) Open(_ *loader.Package, _ string) (io.WriteCloser, error) { + return nopCloser{ioutil.Discard}, nil +} + +// +controllertools:marker:generateHelp:category="" + +// OutputToDirectory outputs each artifact to the given directory, regardless +// of if it's package-associated or not. +type OutputToDirectory string + +func (o OutputToDirectory) Open(_ *loader.Package, itemPath string) (io.WriteCloser, error) { + // ensure the directory exists + if err := os.MkdirAll(filepath.Dir(filepath.Join(string(o), itemPath)), os.ModePerm); err != nil { + return nil, err + } + path := filepath.Join(string(o), itemPath) + return os.Create(path) +} + +// OutputToStdout outputs everything to standard-out, with no separation. +// +// Generally useful for single-artifact outputs. +var OutputToStdout = outputToStdout{} + +// +controllertools:marker:generateHelp:category="" + +// outputToStdout outputs everything to standard-out, with no separation. +// +// Generally useful for single-artifact outputs. +type outputToStdout struct{} + +func (o outputToStdout) Open(_ *loader.Package, itemPath string) (io.WriteCloser, error) { + return nopCloser{os.Stdout}, nil +} + +// +controllertools:marker:generateHelp:category="" + +// OutputArtifacts outputs artifacts to different locations, depending on +// whether they're package-associated or not. +// +// Non-package associated artifacts +// are output to the Config directory, while package-associated ones are output +// to their package's source files' directory, unless an alternate path is +// specified in Code. +type OutputArtifacts struct { + // Config points to the directory to which to write configuration. + Config OutputToDirectory + // Code overrides the directory in which to write new code (defaults to where the existing code lives). + Code OutputToDirectory `marker:",optional"` +} + +func (o OutputArtifacts) Open(pkg *loader.Package, itemPath string) (io.WriteCloser, error) { + if pkg == nil { + return o.Config.Open(pkg, itemPath) + } + + if o.Code != "" { + return o.Code.Open(pkg, itemPath) + } + + if len(pkg.CompiledGoFiles) == 0 { + return nil, fmt.Errorf("cannot output to a package with no path on disk") + } + outDir := filepath.Dir(pkg.CompiledGoFiles[0]) + outPath := filepath.Join(outDir, itemPath) + return os.Create(outPath) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/genall/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/genall/zz_generated.markerhelp.go new file mode 100644 index 0000000000..6428f0ce28 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/genall/zz_generated.markerhelp.go @@ -0,0 +1,90 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package genall + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (InputPaths) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "represents paths and go-style path patterns to use as package roots. ", + Details: "Multiple paths can be specified using \"{path1, path2, path3}\".", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (OutputArtifacts) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "outputs artifacts to different locations, depending on whether they're package-associated or not. ", + Details: "Non-package associated artifacts are output to the Config directory, while package-associated ones are output to their package's source files' directory, unless an alternate path is specified in Code.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Config": { + Summary: "points to the directory to which to write configuration.", + Details: "", + }, + "Code": { + Summary: "overrides the directory in which to write new code (defaults to where the existing code lives).", + Details: "", + }, + }, + } +} + +func (OutputToDirectory) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "outputs each artifact to the given directory, regardless of if it's package-associated or not.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (outputToNothing) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "skips outputting anything.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + +func (outputToStdout) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "outputs everything to standard-out, with no separation. ", + Details: "Generally useful for single-artifact outputs.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/doc.go new file mode 100644 index 0000000000..6853c062f4 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/doc.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package loader defines helpers for loading packages from sources. It wraps +// go/packages, allow incremental loading of source code and manual control +// over which packages get type-checked. This allows for faster loading in +// cases where you don't actually care about certain imports. +// +// Because it uses go/packages, it's modules-aware, and works in both modules- +// and non-modules environments. +// +// # Loading +// +// The main entrypoint for loading is LoadRoots, which traverse the package +// graph starting at the given patterns (file, package, path, or ...-wildcard, +// as one might pass to go list). Packages beyond the roots can be accessed +// via the Imports() method. Packages are initially loaded with export data +// paths, filenames, and imports. +// +// Packages are suitable for comparison, as each unique package only ever has +// one *Package object returned. +// +// # Syntax and TypeChecking +// +// ASTs and type-checking information can be loaded with NeedSyntax and +// NeedTypesInfo, respectively. Both are idempotent -- repeated calls will +// simply re-use the cached contents. Note that NeedTypesInfo will *only* type +// check the current package -- if you want to type-check imports as well, +// you'll need to type-check them first. +// +// # Reference Pruning and Recursive Checking +// +// In order to type-check using only the packages you care about, you can use a +// TypeChecker. TypeChecker will visit each top-level type declaration, +// collect (optionally filtered) references, and type-check references +// packages. +// +// # Errors +// +// Errors can be added to each package. Use ErrFromNode to create an error +// from an AST node. Errors can then be printed (complete with file and +// position information) using PrintErrors, optionally filtered by error type. +// It's generally a good idea to filter out TypeErrors when doing incomplete +// type-checking with TypeChecker. You can use MaybeErrList to return multiple +// errors if you need to return an error instead of adding it to a package. +// AddError will later unroll it into individual errors. +package loader diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/errors.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/errors.go new file mode 100644 index 0000000000..5a675798e2 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/errors.go @@ -0,0 +1,67 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "fmt" + "go/token" +) + +// PositionedError represents some error with an associated position. +// The position is tied to some external token.FileSet. +type PositionedError struct { + Pos token.Pos + error +} + +// Node is the intersection of go/ast.Node and go/types.Var. +type Node interface { + Pos() token.Pos // position of first character belonging to the node +} + +// ErrFromNode returns the given error, with additional information +// attaching it to the given AST node. It will automatically map +// over error lists. +func ErrFromNode(err error, node Node) error { + if asList, isList := err.(ErrList); isList { + resList := make(ErrList, len(asList)) + for i, baseErr := range asList { + resList[i] = ErrFromNode(baseErr, node) + } + return resList + } + return PositionedError{ + Pos: node.Pos(), + error: err, + } +} + +// MaybeErrList constructs an ErrList if the given list of +// errors has any errors, otherwise returning nil. +func MaybeErrList(errs []error) error { + if len(errs) == 0 { + return nil + } + return ErrList(errs) +} + +// ErrList is a list of errors aggregated together into a single error. +type ErrList []error + +func (l ErrList) Error() string { + return fmt.Sprintf("%v", []error(l)) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/loader.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/loader.go new file mode 100644 index 0000000000..2efa94c7d9 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/loader.go @@ -0,0 +1,629 @@ +/* +Copyright 2019-2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "fmt" + "go/ast" + "go/parser" + "go/scanner" + "go/token" + "go/types" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "sync" + + "golang.org/x/tools/go/packages" + "k8s.io/apimachinery/pkg/util/sets" +) + +// Much of this is strongly inspired by the contents of go/packages, +// except that it allows for lazy loading of syntax and type-checking +// information to speed up cases where full traversal isn't needed. + +// PrintErrors print errors associated with all packages +// in the given package graph, starting at the given root +// packages and traversing through all imports. It will skip +// any errors of the kinds specified in filterKinds. It will +// return true if any errors were printed. +func PrintErrors(pkgs []*Package, filterKinds ...packages.ErrorKind) bool { + pkgsRaw := make([]*packages.Package, len(pkgs)) + for i, pkg := range pkgs { + pkgsRaw[i] = pkg.Package + } + toSkip := make(map[packages.ErrorKind]struct{}) + for _, errKind := range filterKinds { + toSkip[errKind] = struct{}{} + } + hadErrors := false + packages.Visit(pkgsRaw, nil, func(pkgRaw *packages.Package) { + for _, err := range pkgRaw.Errors { + if _, skip := toSkip[err.Kind]; skip { + continue + } + hadErrors = true + fmt.Fprintln(os.Stderr, err) + } + }) + return hadErrors +} + +// Package is a single, unique Go package that can be +// lazily parsed and type-checked. Packages should not +// be constructed directly -- instead, use LoadRoots. +// For a given call to LoadRoots, only a single instance +// of each package exists, and thus they may be used as keys +// and for comparison. +type Package struct { + *packages.Package + + imports map[string]*Package + + loader *loader + sync.Mutex +} + +// Imports returns the imports for the given package, indexed by +// package path (*not* name in any particular file). +func (p *Package) Imports() map[string]*Package { + if p.imports == nil { + p.imports = p.loader.packagesFor(p.Package.Imports) + } + + return p.imports +} + +// NeedTypesInfo indicates that type-checking information is needed for this package. +// Actual type-checking information can be accessed via the Types and TypesInfo fields. +func (p *Package) NeedTypesInfo() { + if p.TypesInfo != nil { + return + } + p.NeedSyntax() + p.loader.typeCheck(p) +} + +// NeedSyntax indicates that a parsed AST is needed for this package. +// Actual ASTs can be accessed via the Syntax field. +func (p *Package) NeedSyntax() { + if p.Syntax != nil { + return + } + out := make([]*ast.File, len(p.CompiledGoFiles)) + var wg sync.WaitGroup + wg.Add(len(p.CompiledGoFiles)) + for i, filename := range p.CompiledGoFiles { + go func(i int, filename string) { + defer wg.Done() + src, err := ioutil.ReadFile(filename) + if err != nil { + p.AddError(err) + return + } + out[i], err = p.loader.parseFile(filename, src) + if err != nil { + p.AddError(err) + return + } + }(i, filename) + } + wg.Wait() + for _, file := range out { + if file == nil { + return + } + } + p.Syntax = out +} + +// AddError adds an error to the errors associated with the given package. +func (p *Package) AddError(err error) { + switch typedErr := err.(type) { + case *os.PathError: + // file-reading errors + p.Errors = append(p.Errors, packages.Error{ + Pos: typedErr.Path + ":1", + Msg: typedErr.Err.Error(), + Kind: packages.ParseError, + }) + case scanner.ErrorList: + // parsing/scanning errors + for _, subErr := range typedErr { + p.Errors = append(p.Errors, packages.Error{ + Pos: subErr.Pos.String(), + Msg: subErr.Msg, + Kind: packages.ParseError, + }) + } + case types.Error: + // type-checking errors + p.Errors = append(p.Errors, packages.Error{ + Pos: typedErr.Fset.Position(typedErr.Pos).String(), + Msg: typedErr.Msg, + Kind: packages.TypeError, + }) + case ErrList: + for _, subErr := range typedErr { + p.AddError(subErr) + } + case PositionedError: + p.Errors = append(p.Errors, packages.Error{ + Pos: p.loader.cfg.Fset.Position(typedErr.Pos).String(), + Msg: typedErr.Error(), + Kind: packages.UnknownError, + }) + default: + // should only happen for external errors, like ref checking + p.Errors = append(p.Errors, packages.Error{ + Pos: p.ID + ":-", + Msg: err.Error(), + Kind: packages.UnknownError, + }) + } +} + +// loader loads packages and their imports. Loaded packages will have +// type size, imports, and exports file information populated. Additional +// information, like ASTs and type-checking information, can be accessed +// via methods on individual packages. +type loader struct { + // Roots are the loaded "root" packages in the package graph loaded via + // LoadRoots. + Roots []*Package + + // cfg contains the package loading config (initialized on demand) + cfg *packages.Config + // packages contains the cache of Packages indexed by the underlying + // package.Package, so that we don't ever produce two Packages with + // the same underlying packages.Package. + packages map[*packages.Package]*Package + packagesMu sync.Mutex +} + +// packageFor returns a wrapped Package for the given packages.Package, +// ensuring that there's a one-to-one mapping between the two. +// It's *not* threadsafe -- use packagesFor for that. +func (l *loader) packageFor(pkgRaw *packages.Package) *Package { + if l.packages[pkgRaw] == nil { + l.packages[pkgRaw] = &Package{ + Package: pkgRaw, + loader: l, + } + } + return l.packages[pkgRaw] +} + +// packagesFor returns a map of Package objects for each packages.Package in the input +// map, ensuring that there's a one-to-one mapping between package.Package and Package +// (as per packageFor). +func (l *loader) packagesFor(pkgsRaw map[string]*packages.Package) map[string]*Package { + l.packagesMu.Lock() + defer l.packagesMu.Unlock() + + out := make(map[string]*Package, len(pkgsRaw)) + for name, rawPkg := range pkgsRaw { + out[name] = l.packageFor(rawPkg) + } + return out +} + +// typeCheck type-checks the given package. +func (l *loader) typeCheck(pkg *Package) { + // don't conflict with typeCheckFromExportData + + pkg.TypesInfo = &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Scopes: make(map[ast.Node]*types.Scope), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + } + + pkg.Fset = l.cfg.Fset + pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name) + + importer := importerFunc(func(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + // The imports map is keyed by import path. + importedPkg := pkg.Imports()[path] + if importedPkg == nil { + return nil, fmt.Errorf("package %q possibly creates an import loop", path) + } + + // it's possible to have a call to check in parallel to a call to this + // if one package in the package graph gets its dependency filtered out, + // but another doesn't (so one wants a "placeholder" package here, and another + // wants the full check). + // + // Thus, we need to lock here (at least for the time being) to avoid + // races between the above write to `pkg.Types` and this checking of + // importedPkg.Types. + importedPkg.Lock() + defer importedPkg.Unlock() + + if importedPkg.Types != nil && importedPkg.Types.Complete() { + return importedPkg.Types, nil + } + + // if we haven't already loaded typecheck data, we don't care about this package's types + return types.NewPackage(importedPkg.PkgPath, importedPkg.Name), nil + }) + + var errs []error + + // type-check + checkConfig := &types.Config{ + Importer: importer, + + IgnoreFuncBodies: true, // we only need decl-level info + + Error: func(err error) { + errs = append(errs, err) + }, + + Sizes: pkg.TypesSizes, + } + if err := types.NewChecker(checkConfig, l.cfg.Fset, pkg.Types, pkg.TypesInfo).Files(pkg.Syntax); err != nil { + errs = append(errs, err) + } + + // make sure that if a given sub-import is ill-typed, we mark this package as ill-typed as well. + illTyped := len(errs) > 0 + if !illTyped { + for _, importedPkg := range pkg.Imports() { + if importedPkg.IllTyped { + illTyped = true + break + } + } + } + pkg.IllTyped = illTyped + + // publish errors to the package error list. + for _, err := range errs { + pkg.AddError(err) + } +} + +// parseFile parses the given file, including comments. +func (l *loader) parseFile(filename string, src []byte) (*ast.File, error) { + // skip function bodies + file, err := parser.ParseFile(l.cfg.Fset, filename, src, parser.AllErrors|parser.ParseComments) + if err != nil { + return nil, err + } + + return file, nil +} + +// LoadRoots loads the given "root" packages by path, transitively loading +// and all imports as well. +// +// Loaded packages will have type size, imports, and exports file information +// populated. Additional information, like ASTs and type-checking information, +// can be accessed via methods on individual packages. +func LoadRoots(roots ...string) ([]*Package, error) { + return LoadRootsWithConfig(&packages.Config{}, roots...) +} + +// LoadRootsWithConfig functions like LoadRoots, except that it allows passing +// a custom loading config. The config will be modified to suit the needs of +// the loader. +// +// This is generally only useful for use in testing when you need to modify +// loading settings to load from a fake location. +// +// This function will traverse Go module boundaries for roots that are file- +// system paths and end with "...". Please note this feature currently only +// supports roots that are filesystem paths. For more information, please +// refer to the high-level outline of this function's logic: +// +// 1. If no roots are provided then load the working directory and return +// early. +// +// 2. Otherwise sort the provided roots into two, distinct buckets: +// +// a. package/module names +// b. filesystem paths +// +// A filesystem path is distinguished from a Go package/module name by +// the same rules as followed by the "go" command. At a high level, a +// root is a filesystem path IFF it meets ANY of the following criteria: +// +// * is absolute +// * begins with . +// * begins with .. +// +// For more information please refer to the output of the command +// "go help packages". +// +// 3. Load the package/module roots as a single call to packages.Load. If +// there are no filesystem path roots then return early. +// +// 4. For filesystem path roots ending with "...", check to see if its +// descendants include any nested, Go modules. If so, add the directory +// that contains the nested Go module to the filesystem path roots. +// +// 5. Load the filesystem path roots and return the load packages for the +// package/module roots AND the filesystem path roots. +func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, error) { + l := &loader{ + cfg: cfg, + packages: make(map[*packages.Package]*Package), + } + l.cfg.Mode |= packages.LoadImports | packages.NeedTypesSizes + if l.cfg.Fset == nil { + l.cfg.Fset = token.NewFileSet() + } + // put our build flags first so that callers can override them + l.cfg.BuildFlags = append([]string{"-tags", "ignore_autogenerated"}, l.cfg.BuildFlags...) + + // Visit the import graphs of the loaded, root packages. If an imported + // package refers to another loaded, root package, then replace the + // instance of the imported package with a reference to the loaded, root + // package. This is required to make kubebuilder markers work correctly + // when multiple root paths are loaded and types from one path reference + // types from another root path. + defer func() { + for i := range l.Roots { + visitImports(l.Roots, l.Roots[i], nil) + } + }() + + // uniquePkgIDs is used to keep track of the discovered packages to be nice + // and try and prevent packages from showing up twice when nested module + // support is enabled. there is not harm that comes from this per se, but + // it makes testing easier when a known number of modules can be asserted + uniquePkgIDs := sets.String{} + + // loadPackages returns the Go packages for the provided roots + // + // if validatePkgFn is nil, a package will be returned in the slice, + // otherwise the package is only returned if the result of + // validatePkgFn(pkg.ID) is truthy + loadPackages := func(roots ...string) ([]*Package, error) { + rawPkgs, err := packages.Load(l.cfg, roots...) + if err != nil { + loadRoot := l.cfg.Dir + if l.cfg.Dir == "" { + loadRoot, _ = os.Getwd() + } + return nil, fmt.Errorf("load packages in root %q: %w", loadRoot, err) + } + var pkgs []*Package + for _, rp := range rawPkgs { + p := l.packageFor(rp) + if !uniquePkgIDs.Has(p.ID) { + pkgs = append(pkgs, p) + uniquePkgIDs.Insert(p.ID) + } + } + return pkgs, nil + } + + // if no roots were provided then load the current package and return early + if len(roots) == 0 { + pkgs, err := loadPackages() + if err != nil { + return nil, err + } + l.Roots = append(l.Roots, pkgs...) + return l.Roots, nil + } + + // pkgRoots is a slice of roots that are package/modules and fspRoots + // is a slice of roots that are local filesystem paths. + // + // please refer to this function's godoc comments for more information on + // how these two types of roots are distinguished from one another + var ( + pkgRoots []string + fspRoots []string + fspRootRx = regexp.MustCompile(`^\.{1,2}`) + ) + for _, r := range roots { + if filepath.IsAbs(r) || fspRootRx.MatchString(r) { + fspRoots = append(fspRoots, r) + } else { + pkgRoots = append(pkgRoots, r) + } + } + + // handle the package roots by sending them into the packages.Load function + // all at once. this is more efficient, but cannot be used for the file- + // system path roots due to them needing a custom, calculated value for the + // cfg.Dir field + if len(pkgRoots) > 0 { + pkgs, err := loadPackages(pkgRoots...) + if err != nil { + return nil, err + } + l.Roots = append(l.Roots, pkgs...) + } + + // if there are no filesystem path roots then go ahead and return early + if len(fspRoots) == 0 { + return l.Roots, nil + } + + // + // at this point we are handling filesystem path roots + // + + // ensure the cfg.Dir field is reset to its original value upon + // returning from this function. it should honestly be fine if it is + // not given most callers will not send in the cfg parameter directly, + // as it's largely for testing, but still, let's be good stewards. + defer func(d string) { + cfg.Dir = d + }(cfg.Dir) + + // store the value of cfg.Dir so we can use it later if it is non-empty. + // we need to store it now as the value of cfg.Dir will be updated by + // a loop below + cfgDir := cfg.Dir + + // addNestedGoModulesToRoots is given to filepath.WalkDir and adds the + // directory part of p to the list of filesystem path roots IFF p is the + // path to a file named "go.mod" + addNestedGoModulesToRoots := func( + p string, + d os.DirEntry, + e error) error { + + if e != nil { + return e + } + if !d.IsDir() && filepath.Base(p) == "go.mod" { + fspRoots = append(fspRoots, filepath.Join(filepath.Dir(p), "...")) + } + return nil + } + + // in the first pass over the filesystem path roots we: + // + // 1. make the root into an absolute path + // + // 2. check to see if a root uses the nested path syntax, ex. ... + // + // 3. if so, walk the root's descendants, searching for any nested Go + // modules + // + // 4. if found then the directory containing the Go module is added to + // the list of the filesystem path roots + for i := range fspRoots { + r := fspRoots[i] + + // clean up the root + r = filepath.Clean(r) + + // get the absolute path of the root + if !filepath.IsAbs(r) { + + // if the initial value of cfg.Dir was non-empty then use it when + // building the absolute path to this root. otherwise use the + // filepath.Abs function to get the absolute path of the root based + // on the working directory + if cfgDir != "" { + r = filepath.Join(cfgDir, r) + } else { + ar, err := filepath.Abs(r) + if err != nil { + return nil, err + } + r = ar + } + } + + // update the root to be an absolute path + fspRoots[i] = r + + b, d := filepath.Base(r), filepath.Dir(r) + + // if the base element is "..." then it means nested traversal is + // activated. this can be passed directly to the loader. however, if + // specified we also want to traverse the path manually to determine if + // there are any nested Go modules we want to add to the list of file- + // system path roots to process + if b == "..." { + if err := filepath.WalkDir( + d, + addNestedGoModulesToRoots); err != nil { + + return nil, err + } + } + } + + // in the second pass over the filesystem path roots we: + // + // 1. determine the directory from which to execute the loader + // + // 2. update the loader config's Dir property to be the directory from + // step one + // + // 3. determine whether the root passed to the loader should be "./." + // or "./..." + // + // 4. execute the loader with the value from step three + for _, r := range fspRoots { + b, d := filepath.Base(r), filepath.Dir(r) + + // we want the base part of the path to be either "..." or ".", except + // Go's filepath utilities clean paths during manipulation, removing the + // ".". thus, if not "...", let's update the path components so that: + // + // d = r + // b = "." + if b != "..." { + d = r + b = "." + } + + // update the loader configuration's Dir field to the directory part of + // the root + l.cfg.Dir = d + + // update the root to be "./..." or "./." + // (with OS-specific filepath separator). please note filepath.Join + // would clean up the trailing "." character that we want preserved, + // hence the more manual path concatenation logic + r = fmt.Sprintf(".%s%s", string(filepath.Separator), b) + + // load the packages from the roots + pkgs, err := loadPackages(r) + if err != nil { + return nil, err + } + l.Roots = append(l.Roots, pkgs...) + } + + return l.Roots, nil +} + +// visitImports walks a dependency graph, replacing imported package +// references with those from the rootPkgs list. This ensures the +// kubebuilder marker generation is handled correctly. For more info, +// please see issue 680. +func visitImports(rootPkgs []*Package, pkg *Package, seen sets.String) { + if seen == nil { + seen = sets.String{} + } + for importedPkgID, importedPkg := range pkg.Imports() { + for i := range rootPkgs { + if importedPkgID == rootPkgs[i].ID { + pkg.imports[importedPkgID] = rootPkgs[i] + } + } + if !seen.Has(importedPkgID) { + seen.Insert(importedPkgID) + visitImports(rootPkgs, importedPkg, seen) + } + } +} + +// importFunc is an implementation of the single-method +// types.Importer interface based on a function value. +type importerFunc func(path string) (*types.Package, error) + +func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/paths.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/paths.go new file mode 100644 index 0000000000..3b783e1685 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/paths.go @@ -0,0 +1,32 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "strings" +) + +// NonVendorPath returns a package path that does not include anything before the +// last vendor directory. This is useful for when using vendor directories, +// and using go/types.Package.Path(), which returns the full path including vendor. +// +// If you're using this, make sure you really need it -- it's better to index by +// the actual Package object when you can. +func NonVendorPath(rawPath string) string { + parts := strings.Split(rawPath, "/vendor/") + return parts[len(parts)-1] +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/refs.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/refs.go new file mode 100644 index 0000000000..37c3295f14 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/refs.go @@ -0,0 +1,273 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "fmt" + + "go/ast" + "strconv" + "sync" +) + +// NB(directxman12): most of this is done by the typechecker, +// but it's a bit slow/heavyweight for what we want -- we want +// to resolve external imports *only* if we actually need them. + +// Basically, what we do is: +// 1. Map imports to names +// 2. Find all explicit external references (`name.type`) +// 3. Find all referenced packages by merging explicit references and dot imports +// 4. Only type-check those packages +// 5. Ignore type-checking errors from the missing packages, because we won't ever +// touch unloaded types (they're probably used in ignored fields/types, variables, or functions) +// (done using PrintErrors with an ignore argument from the caller). +// 6. Notice any actual type-checking errors via invalid types + +// importsMap saves import aliases, mapping them to underlying packages. +type importsMap struct { + // dotImports maps package IDs to packages for any packages that have/ been imported as `.` + dotImports map[string]*Package + // byName maps package aliases or names to the underlying package. + byName map[string]*Package +} + +// mapImports maps imports from the names they use in the given file to the underlying package, +// using a map of package import paths to packages (generally from Package.Imports()). +func mapImports(file *ast.File, importedPkgs map[string]*Package) (*importsMap, error) { + m := &importsMap{ + dotImports: make(map[string]*Package), + byName: make(map[string]*Package), + } + for _, importSpec := range file.Imports { + path, err := strconv.Unquote(importSpec.Path.Value) + if err != nil { + return nil, ErrFromNode(err, importSpec.Path) + } + importedPkg := importedPkgs[path] + if importedPkg == nil { + return nil, ErrFromNode(fmt.Errorf("no such package located"), importSpec.Path) + } + if importSpec.Name == nil { + m.byName[importedPkg.Name] = importedPkg + continue + } + if importSpec.Name.Name == "." { + m.dotImports[importedPkg.ID] = importedPkg + continue + } + m.byName[importSpec.Name.Name] = importedPkg + } + + return m, nil +} + +// referenceSet finds references to external packages' types in the given file, +// without otherwise calling into the type-checker. When checking structs, +// it only checks fields with JSON tags. +type referenceSet struct { + file *ast.File + imports *importsMap + pkg *Package + + externalRefs map[*Package]struct{} +} + +func (r *referenceSet) init() { + if r.externalRefs == nil { + r.externalRefs = make(map[*Package]struct{}) + } +} + +// NodeFilter filters nodes, accepting them for reference collection +// when true is returned and rejecting them when false is returned. +type NodeFilter func(ast.Node) bool + +// collectReferences saves all references to external types in the given info. +func (r *referenceSet) collectReferences(rawType ast.Expr, filterNode NodeFilter) { + r.init() + col := &referenceCollector{ + refs: r, + filterNode: filterNode, + } + ast.Walk(col, rawType) +} + +// external saves an external reference to the given named package. +func (r *referenceSet) external(pkgName string) { + pkg := r.imports.byName[pkgName] + if pkg == nil { + r.pkg.AddError(fmt.Errorf("use of unimported package %q", pkgName)) + return + } + r.externalRefs[pkg] = struct{}{} +} + +// referenceCollector visits nodes in an AST, adding external references to a +// referenceSet. +type referenceCollector struct { + refs *referenceSet + filterNode NodeFilter +} + +func (c *referenceCollector) Visit(node ast.Node) ast.Visitor { + if !c.filterNode(node) { + return nil + } + switch typedNode := node.(type) { + case *ast.Ident: + // local reference or dot-import, ignore + return nil + case *ast.SelectorExpr: + switch x := typedNode.X.(type) { + case *ast.Ident: + pkgName := x.Name + c.refs.external(pkgName) + return nil + default: + return c + } + default: + return c + } +} + +// allReferencedPackages finds all directly referenced packages in the given package. +func allReferencedPackages(pkg *Package, filterNodes NodeFilter) []*Package { + pkg.NeedSyntax() + refsByFile := make(map[*ast.File]*referenceSet) + for _, file := range pkg.Syntax { + imports, err := mapImports(file, pkg.Imports()) + if err != nil { + pkg.AddError(err) + return nil + } + refs := &referenceSet{ + file: file, + imports: imports, + pkg: pkg, + } + refsByFile[file] = refs + } + + EachType(pkg, func(file *ast.File, decl *ast.GenDecl, spec *ast.TypeSpec) { + refs := refsByFile[file] + refs.collectReferences(spec.Type, filterNodes) + }) + + allPackages := make(map[*Package]struct{}) + for _, refs := range refsByFile { + for _, pkg := range refs.imports.dotImports { + allPackages[pkg] = struct{}{} + } + for ref := range refs.externalRefs { + allPackages[ref] = struct{}{} + } + } + + res := make([]*Package, 0, len(allPackages)) + for pkg := range allPackages { + res = append(res, pkg) + } + return res +} + +// TypeChecker performs type-checking on a limitted subset of packages by +// checking each package's types' externally-referenced types, and only +// type-checking those packages. +type TypeChecker struct { + // NodeFilters are used to filter the set of references that are followed + // when typechecking. If any of the filters returns true for a given node, + // its package will be added to the set of packages to check. + // + // If no filters are specified, all references are followed (this may be slow). + // + // Modifying this after the first call to check may yield strange/invalid + // results. + NodeFilters []NodeFilter + + checkedPackages map[*Package]struct{} + sync.Mutex +} + +// Check type-checks the given package and all packages referenced by types +// that pass through (have true returned by) any of the NodeFilters. +func (c *TypeChecker) Check(root *Package) { + c.init() + + // use a sub-checker with the appropriate settings + (&TypeChecker{ + NodeFilters: c.NodeFilters, + checkedPackages: c.checkedPackages, + }).check(root) +} + +func (c *TypeChecker) isNodeInteresting(node ast.Node) bool { + // no filters --> everything is important + if len(c.NodeFilters) == 0 { + return true + } + + // otherwise, passing through any one filter means this node is important + for _, filter := range c.NodeFilters { + if filter(node) { + return true + } + } + return false +} + +func (c *TypeChecker) init() { + if c.checkedPackages == nil { + c.checkedPackages = make(map[*Package]struct{}) + } +} + +// check recursively type-checks the given package, only loading packages that +// are actually referenced by our types (it's the actual implementation of Check, +// without initialization). +func (c *TypeChecker) check(root *Package) { + root.Lock() + defer root.Unlock() + + c.Lock() + _, ok := c.checkedPackages[root] + c.Unlock() + if ok { + return + } + + refedPackages := allReferencedPackages(root, c.isNodeInteresting) + + // first, resolve imports for all leaf packages... + var wg sync.WaitGroup + for _, pkg := range refedPackages { + wg.Add(1) + go func(pkg *Package) { + defer wg.Done() + c.check(pkg) + }(pkg) + } + wg.Wait() + + // ...then, we can safely type-check ourself + root.NeedTypesInfo() + + c.Lock() + defer c.Unlock() + c.checkedPackages[root] = struct{}{} +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/loader/visit.go b/vendor/sigs.k8s.io/controller-tools/pkg/loader/visit.go new file mode 100644 index 0000000000..b5646fde1d --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/loader/visit.go @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "go/ast" + "reflect" + "strconv" +) + +// TypeCallback is a callback called for each raw AST (gendecl, typespec) combo. +type TypeCallback func(file *ast.File, decl *ast.GenDecl, spec *ast.TypeSpec) + +// EachType calls the given callback for each (gendecl, typespec) combo in the +// given package. Generally, using markers.EachType is better when working +// with marker data, and has a more convinient representation. +func EachType(pkg *Package, cb TypeCallback) { + visitor := &typeVisitor{ + callback: cb, + } + pkg.NeedSyntax() + for _, file := range pkg.Syntax { + visitor.file = file + ast.Walk(visitor, file) + } +} + +// typeVisitor visits all TypeSpecs, calling the given callback for each. +type typeVisitor struct { + callback TypeCallback + decl *ast.GenDecl + file *ast.File +} + +// Visit visits all TypeSpecs. +func (v *typeVisitor) Visit(node ast.Node) ast.Visitor { + if node == nil { + v.decl = nil + return v + } + + switch typedNode := node.(type) { + case *ast.File: + v.file = typedNode + return v + case *ast.GenDecl: + v.decl = typedNode + return v + case *ast.TypeSpec: + v.callback(v.file, v.decl, typedNode) + return nil // don't recurse + default: + return nil + } +} + +// ParseAstTag parses the given raw tag literal into a reflect.StructTag. +func ParseAstTag(tag *ast.BasicLit) reflect.StructTag { + if tag == nil { + return reflect.StructTag("") + } + tagStr, err := strconv.Unquote(tag.Value) + if err != nil { + return reflect.StructTag("") + } + return reflect.StructTag(tagStr) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/collect.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/collect.go new file mode 100644 index 0000000000..b2f8219914 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/collect.go @@ -0,0 +1,422 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "go/ast" + "go/token" + "strings" + "sync" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// Collector collects and parses marker comments defined in the registry +// from package source code. If no registry is provided, an empty one will +// be initialized on the first call to MarkersInPackage. +type Collector struct { + *Registry + + byPackage map[string]map[ast.Node]MarkerValues + mu sync.Mutex +} + +// MarkerValues are all the values for some set of markers. +type MarkerValues map[string][]interface{} + +// Get fetches the first value that for the given marker, returning +// nil if no values are available. +func (v MarkerValues) Get(name string) interface{} { + vals := v[name] + if len(vals) == 0 { + return nil + } + return vals[0] +} + +func (c *Collector) init() { + if c.Registry == nil { + c.Registry = &Registry{} + } + if c.byPackage == nil { + c.byPackage = make(map[string]map[ast.Node]MarkerValues) + } +} + +// MarkersInPackage computes the marker values by node for the given package. Results +// are cached by package ID, so this is safe to call repeatedly from different functions. +// Each file in the package is treated as a distinct node. +// +// We consider a marker to be associated with a given AST node if either of the following are true: +// +// - it's in the Godoc for that AST node +// +// - it's in the closest non-godoc comment group above that node, +// *and* that node is a type or field node, *and* [it's either +// registered as type-level *or* it's not registered as being +// package-level] +// +// - it's not in the Godoc of a node, doesn't meet the above criteria, and +// isn't in a struct definition (in which case it's package-level) +func (c *Collector) MarkersInPackage(pkg *loader.Package) (map[ast.Node]MarkerValues, error) { + c.mu.Lock() + c.init() + if markers, exist := c.byPackage[pkg.ID]; exist { + c.mu.Unlock() + return markers, nil + } + // unlock early, it's ok if we do a bit extra work rather than locking while we're working + c.mu.Unlock() + + pkg.NeedSyntax() + nodeMarkersRaw := c.associatePkgMarkers(pkg) + markers, err := c.parseMarkersInPackage(nodeMarkersRaw) + if err != nil { + return nil, err + } + + c.mu.Lock() + defer c.mu.Unlock() + c.byPackage[pkg.ID] = markers + + return markers, nil +} + +// parseMarkersInPackage parses the given raw marker comments into output values using the registry. +func (c *Collector) parseMarkersInPackage(nodeMarkersRaw map[ast.Node][]markerComment) (map[ast.Node]MarkerValues, error) { + var errors []error + nodeMarkerValues := make(map[ast.Node]MarkerValues) + for node, markersRaw := range nodeMarkersRaw { + var target TargetType + switch node.(type) { + case *ast.File: + target = DescribesPackage + case *ast.Field: + target = DescribesField + default: + target = DescribesType + } + markerVals := make(map[string][]interface{}) + for _, markerRaw := range markersRaw { + markerText := markerRaw.Text() + def := c.Registry.Lookup(markerText, target) + if def == nil { + continue + } + val, err := def.Parse(markerText) + if err != nil { + errors = append(errors, loader.ErrFromNode(err, markerRaw)) + continue + } + markerVals[def.Name] = append(markerVals[def.Name], val) + } + nodeMarkerValues[node] = markerVals + } + + return nodeMarkerValues, loader.MaybeErrList(errors) +} + +// associatePkgMarkers associates markers with AST nodes in the given package. +func (c *Collector) associatePkgMarkers(pkg *loader.Package) map[ast.Node][]markerComment { + nodeMarkers := make(map[ast.Node][]markerComment) + for _, file := range pkg.Syntax { + fileNodeMarkers := c.associateFileMarkers(file) + for node, markers := range fileNodeMarkers { + nodeMarkers[node] = append(nodeMarkers[node], markers...) + } + } + + return nodeMarkers +} + +// associateFileMarkers associates markers with AST nodes in the given file. +func (c *Collector) associateFileMarkers(file *ast.File) map[ast.Node][]markerComment { + // grab all the raw marker comments by node + visitor := markerSubVisitor{ + collectPackageLevel: true, + markerVisitor: &markerVisitor{ + nodeMarkers: make(map[ast.Node][]markerComment), + allComments: file.Comments, + }, + } + ast.Walk(visitor, file) + + // grab the last package-level comments at the end of the file (if any) + lastFileMarkers := visitor.markersBetween(false, visitor.commentInd, len(visitor.allComments)) + visitor.pkgMarkers = append(visitor.pkgMarkers, lastFileMarkers...) + + // figure out if any type-level markers are actually package-level markers + for node, markers := range visitor.nodeMarkers { + _, isType := node.(*ast.TypeSpec) + if !isType { + continue + } + endOfMarkers := 0 + for _, marker := range markers { + if marker.fromGodoc { + // markers from godoc are never package level + markers[endOfMarkers] = marker + endOfMarkers++ + continue + } + markerText := marker.Text() + typeDef := c.Registry.Lookup(markerText, DescribesType) + if typeDef != nil { + // prefer assuming type-level markers + markers[endOfMarkers] = marker + endOfMarkers++ + continue + } + def := c.Registry.Lookup(markerText, DescribesPackage) + if def == nil { + // assume type-level unless proven otherwise + markers[endOfMarkers] = marker + endOfMarkers++ + continue + } + // it's package-level, since a package-level definition exists + visitor.pkgMarkers = append(visitor.pkgMarkers, marker) + } + visitor.nodeMarkers[node] = markers[:endOfMarkers] // re-set after trimming the package markers + } + visitor.nodeMarkers[file] = visitor.pkgMarkers + + return visitor.nodeMarkers +} + +// markerComment is an AST comment that contains a marker. +// It may or may not be from a Godoc comment, which affects +// marker re-associated (from type-level to package-level) +type markerComment struct { + *ast.Comment + fromGodoc bool +} + +// Text returns the text of the marker, stripped of the comment +// marker and leading spaces, as should be passed to Registry.Lookup +// and Registry.Parse. +func (c markerComment) Text() string { + return strings.TrimSpace(c.Comment.Text[2:]) +} + +// markerVisistor visits AST nodes, recording markers associated with each node. +type markerVisitor struct { + allComments []*ast.CommentGroup + commentInd int + + declComments []markerComment + lastLineCommentGroup *ast.CommentGroup + + pkgMarkers []markerComment + nodeMarkers map[ast.Node][]markerComment +} + +// isMarkerComment checks that the given comment is a single-line (`//`) +// comment and it's first non-space content is `+`. +func isMarkerComment(comment string) bool { + if comment[0:2] != "//" { + return false + } + stripped := strings.TrimSpace(comment[2:]) + if len(stripped) < 1 || stripped[0] != '+' { + return false + } + return true +} + +// markersBetween grabs the markers between the given indicies in the list of all comments. +func (v *markerVisitor) markersBetween(fromGodoc bool, start, end int) []markerComment { + if start < 0 || end < 0 { + return nil + } + var res []markerComment + for i := start; i < end; i++ { + commentGroup := v.allComments[i] + for _, comment := range commentGroup.List { + if !isMarkerComment(comment.Text) { + continue + } + res = append(res, markerComment{Comment: comment, fromGodoc: fromGodoc}) + } + } + return res +} + +type markerSubVisitor struct { + *markerVisitor + node ast.Node + collectPackageLevel bool +} + +// Visit collects markers for each node in the AST, optionally +// collecting unassociated markers as package-level. +func (v markerSubVisitor) Visit(node ast.Node) ast.Visitor { + if node == nil { + // end of the node, so we might need to advance comments beyond the end + // of the block if we don't want to collect package-level markers in + // this block. + + if !v.collectPackageLevel { + if v.commentInd < len(v.allComments) { + lastCommentInd := v.commentInd + nextGroup := v.allComments[lastCommentInd] + for nextGroup.Pos() < v.node.End() { + lastCommentInd++ + if lastCommentInd >= len(v.allComments) { + // after the increment so our decrement below still makes sense + break + } + nextGroup = v.allComments[lastCommentInd] + } + v.commentInd = lastCommentInd + } + } + + return nil + } + + // skip comments on the same line as the previous node + // making sure to double-check for the case where we've gone past the end of the comments + // but still have to finish up typespec-gendecl association (see below). + if v.lastLineCommentGroup != nil && v.commentInd < len(v.allComments) && v.lastLineCommentGroup.Pos() == v.allComments[v.commentInd].Pos() { + v.commentInd++ + } + + // stop visiting if there are no more comments in the file + // NB(directxman12): we can't just stop immediately, because we + // still need to check if there are typespecs associated with gendecls. + var markerCommentBlock []markerComment + var docCommentBlock []markerComment + lastCommentInd := v.commentInd + if v.commentInd < len(v.allComments) { + // figure out the first comment after the node in question... + nextGroup := v.allComments[lastCommentInd] + for nextGroup.Pos() < node.Pos() { + lastCommentInd++ + if lastCommentInd >= len(v.allComments) { + // after the increment so our decrement below still makes sense + break + } + nextGroup = v.allComments[lastCommentInd] + } + lastCommentInd-- // ...then decrement to get the last comment before the node in question + + // figure out the godoc comment so we can deal with it separately + var docGroup *ast.CommentGroup + docGroup, v.lastLineCommentGroup = associatedCommentsFor(node) + + // find the last comment group that's not godoc + markerCommentInd := lastCommentInd + if docGroup != nil && v.allComments[markerCommentInd].Pos() == docGroup.Pos() { + markerCommentInd-- + } + + // check if we have freestanding package markers, + // and find the markers in our "closest non-godoc" comment block, + // plus our godoc comment block + if markerCommentInd >= v.commentInd { + if v.collectPackageLevel { + // assume anything between the comment ind and the marker ind (not including it) + // are package-level + v.pkgMarkers = append(v.pkgMarkers, v.markersBetween(false, v.commentInd, markerCommentInd)...) + } + markerCommentBlock = v.markersBetween(false, markerCommentInd, markerCommentInd+1) + docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1) + } else { + docCommentBlock = v.markersBetween(true, markerCommentInd+1, lastCommentInd+1) + } + } + + resVisitor := markerSubVisitor{ + collectPackageLevel: false, // don't collect package level by default + markerVisitor: v.markerVisitor, + node: node, + } + + // associate those markers with a node + switch typedNode := node.(type) { + case *ast.GenDecl: + // save the comments associated with the gen-decl if it's a single-line type decl + if typedNode.Lparen != token.NoPos || typedNode.Tok != token.TYPE { + // not a single-line type spec, treat them as free comments + v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...) + break + } + // save these, we'll need them when we encounter the actual type spec + v.declComments = append(v.declComments, markerCommentBlock...) + v.declComments = append(v.declComments, docCommentBlock...) + case *ast.TypeSpec: + // add in comments attributed to the gen-decl, if any, + // as well as comments associated with the actual type + v.nodeMarkers[node] = append(v.nodeMarkers[node], v.declComments...) + v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...) + v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...) + + v.declComments = nil + v.collectPackageLevel = false // don't collect package-level inside type structs + case *ast.Field: + v.nodeMarkers[node] = append(v.nodeMarkers[node], markerCommentBlock...) + v.nodeMarkers[node] = append(v.nodeMarkers[node], docCommentBlock...) + case *ast.File: + v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...) + v.pkgMarkers = append(v.pkgMarkers, docCommentBlock...) + + // collect markers in root file scope + resVisitor.collectPackageLevel = true + default: + // assume markers before anything else are package-level markers, + // *but* don't include any markers in godoc + if v.collectPackageLevel { + v.pkgMarkers = append(v.pkgMarkers, markerCommentBlock...) + } + } + + // increment the comment ind so that we start at the right place for the next node + v.commentInd = lastCommentInd + 1 + + return resVisitor + +} + +// associatedCommentsFor returns the doc comment group (if relevant and present) and end-of-line comment +// (again if relevant and present) for the given AST node. +func associatedCommentsFor(node ast.Node) (docGroup *ast.CommentGroup, lastLineCommentGroup *ast.CommentGroup) { + switch typedNode := node.(type) { + case *ast.Field: + docGroup = typedNode.Doc + lastLineCommentGroup = typedNode.Comment + case *ast.File: + docGroup = typedNode.Doc + case *ast.FuncDecl: + docGroup = typedNode.Doc + case *ast.GenDecl: + docGroup = typedNode.Doc + case *ast.ImportSpec: + docGroup = typedNode.Doc + lastLineCommentGroup = typedNode.Comment + case *ast.TypeSpec: + docGroup = typedNode.Doc + lastLineCommentGroup = typedNode.Comment + case *ast.ValueSpec: + docGroup = typedNode.Doc + lastLineCommentGroup = typedNode.Comment + default: + lastLineCommentGroup = nil + } + + return docGroup, lastLineCommentGroup +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/doc.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/doc.go new file mode 100644 index 0000000000..ab6947ccaf --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/doc.go @@ -0,0 +1,113 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package markers contains utilities for defining and parsing "marker +// comments", also occasionally called tag comments (we use the term marker to +// avoid confusing with struct tags). Parsed result (output) values take the +// form of Go values, much like the "encoding/json" package. +// +// # Definitions and Parsing +// +// Markers are defined as structured Definitions which can be used to +// consistently parse marker comments. A Definition contains an concrete +// output type for the marker, which can be a simple type (like string), a +// struct, or a wrapper type (useful for defining additional methods on marker +// types). +// +// Markers take the general form +// +// +path:to:marker=val +// +// +path:to:marker:arg1=val,arg2=val2 +// +// +path:to:marker +// +// Arguments may be ints, bools, strings, and slices. Ints and bool take their +// standard form from Go. Strings may take any of their standard forms, or any +// sequence of unquoted characters up until a `,` or `;` is encountered. Lists +// take either of the following forms: +// +// val;val;val +// +// {val, val, val} +// +// Note that the first form will not properly parse nested slices, but is +// generally convenient and is the form used in many existing markers. +// +// Each of those argument types maps to the corresponding go type. Pointers +// mark optional fields (a struct tag, below, may also be used). The empty +// interface will match any type. +// +// Struct fields may optionally be annotated with the `marker` struct tag. The +// first argument is a name override. If it's left blank (or the tag isn't +// present), the camelCase version of the name will be used. The only +// additional argument defined is `optional`, which marks a field as optional +// without using a pointer. +// +// All parsed values are unmarshalled into the output type. If any +// non-optional fields aren't mentioned, an error will be raised unless +// `Strict` is set to false. +// +// # Registries and Lookup +// +// Definitions can be added to registries to facilitate lookups. Each +// definition is marked as either describing a type, struct field, or package +// (unassociated). The same marker name may be registered multiple times, as +// long as each describes a different construct (type, field, or package). +// Definitions can then be looked up by passing unparsed markers. +// +// # Collection and Extraction +// +// Markers can be collected from a loader.Package using a Collector. The +// Collector will read from a given Registry, collecting comments that look +// like markers and parsing them if they match some definition on the registry. +// +// Markers are considered associated with a particular field or type if they +// exist in the Godoc, or the closest non-godoc comment. Any other markers not +// inside a some other block (e.g. a struct definition, interface definition, +// etc) are considered package level. Markers in a "closest non-Go comment +// block" may also be considered package level if registered as such and no +// identical type-level definition exists. +// +// Like loader.Package, Collector's methods are idempotent and will not +// reperform work. +// +// # Traversal +// +// EachType function iterates over each type in a Package, providing +// conveniently structured type and field information with marker values +// associated. +// +// PackageMarkers can be used to fetch just package-level markers. +// +// # Help +// +// Help can be defined for each marker using the DefinitionHelp struct. It's +// mostly intended to be generated off of godocs using cmd/helpgen, which takes +// the first line as summary (removing the type/field name), and considers the +// rest as details. It looks for the +// +// +controllertools:generateHelp[:category=] +// +// marker to start generation. +// +// If you can't use godoc-based generation for whatever reasons (e.g. +// primitive-typed markers), you can use the SimpleHelp and DeprecatedHelp +// helper functions to generate help structs. +// +// Help is then registered into a registry as associated with the actual +// definition, and can then be later retrieved from the registry. +package markers diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/help.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/help.go new file mode 100644 index 0000000000..26ca059bc8 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/help.go @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +// You *probably* don't want to write these structs by hand +// -- use cmd/helpgen if you can write Godoc, and {Simple,Deprecated}Help +// otherwise. + +// DetailedHelp contains brief help, as well as more details. +// For the "full" help, join the two together. +type DetailedHelp struct { + Summary string + Details string +} + +// DefinitionHelp contains overall help for a marker Definition, +// as well as per-field help. +type DefinitionHelp struct { + // DetailedHelp contains the overall help for the marker. + DetailedHelp + // Category describes what kind of marker this is. + Category string + // DeprecatedInFavorOf marks the marker as deprecated. + // If non-nil & empty, it's assumed to just mean deprecated permanently. + // If non-empty, it's assumed to be a marker name. + DeprecatedInFavorOf *string + + // NB(directxman12): we make FieldHelp be in terms of the Go struct field + // names so that we don't have to know the conversion or processing rules + // for struct fields at compile-time for help generation. + + // FieldHelp defines the per-field help for this marker, *in terms of the + // go struct field names. Use the FieldsHelp method to map this to + // marker argument names. + FieldHelp map[string]DetailedHelp +} + +// FieldsHelp maps per-field help to the actual marker argument names from the +// given definition. +func (d *DefinitionHelp) FieldsHelp(def *Definition) map[string]DetailedHelp { + fieldsHelp := make(map[string]DetailedHelp, len(def.FieldNames)) + for fieldName, argName := range def.FieldNames { + fieldsHelp[fieldName] = d.FieldHelp[argName] + } + return fieldsHelp +} + +// SimpleHelp returns help that just has marker-level summary information +// (e.g. for use with empty or primitive-typed markers, where Godoc-based +// generation isn't possible). +func SimpleHelp(category, summary string) *DefinitionHelp { + return &DefinitionHelp{ + Category: category, + DetailedHelp: DetailedHelp{Summary: summary}, + } +} + +// DeprecatedHelp returns simple help (a la SimpleHelp), except marked as +// deprecated in favor of the given marker (or an empty string for just +// deprecated). +func DeprecatedHelp(inFavorOf, category, summary string) *DefinitionHelp { + return &DefinitionHelp{ + Category: category, + DetailedHelp: DetailedHelp{Summary: summary}, + DeprecatedInFavorOf: &inFavorOf, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/parse.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/parse.go new file mode 100644 index 0000000000..3e1d75a833 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/parse.go @@ -0,0 +1,963 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + sc "text/scanner" + "unicode" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// expect checks that the next token of the scanner is the given token, adding an error +// to the scanner if not. It returns whether the token was as expected. +func expect(scanner *sc.Scanner, expected rune, errDesc string) bool { + tok := scanner.Scan() + if tok != expected { + scanner.Error(scanner, fmt.Sprintf("expected %s, got %q", errDesc, scanner.TokenText())) + return false + } + return true +} + +// peekNoSpace is equivalent to scanner.Peek, except that it will consume intervening whitespace. +func peekNoSpace(scanner *sc.Scanner) rune { + hint := scanner.Peek() + for ; hint <= rune(' ') && ((1<") + case IntType: + out.WriteString("int") + case NumberType: + out.WriteString("float64") + case StringType: + out.WriteString("string") + case BoolType: + out.WriteString("bool") + case AnyType: + out.WriteString("") + case SliceType: + out.WriteString("[]") + // arguments can't be non-pointer optional, so just call into typeString again. + a.ItemType.typeString(out) + case MapType: + out.WriteString("map[string]") + a.ItemType.typeString(out) + case RawType: + out.WriteString("") + } +} + +// TypeString returns a string roughly equivalent +// (but not identical) to the underlying Go type that +// this argument would parse to. It's mainly useful +// for user-friendly formatting of this argument (e.g. +// help strings). +func (a Argument) TypeString() string { + out := &strings.Builder{} + a.typeString(out) + return out.String() +} + +func (a Argument) String() string { + if a.Optional { + return fmt.Sprintf("", a.TypeString()) + } + return fmt.Sprintf("", a.TypeString()) +} + +// castAndSet casts val to out's type if needed, +// then sets out to val. +func castAndSet(out, val reflect.Value) { + outType := out.Type() + if outType != val.Type() { + val = val.Convert(outType) + } + out.Set(val) +} + +// makeSliceType makes a reflect.Type for a slice of the given type. +// Useful for constructing the out value for when AnyType's guess returns a slice. +func makeSliceType(itemType Argument) (reflect.Type, error) { + var itemReflectedType reflect.Type + switch itemType.Type { + case IntType: + itemReflectedType = reflect.TypeOf(int(0)) + case NumberType: + itemReflectedType = reflect.TypeOf(float64(0)) + case StringType: + itemReflectedType = reflect.TypeOf("") + case BoolType: + itemReflectedType = reflect.TypeOf(false) + case SliceType: + subItemType, err := makeSliceType(*itemType.ItemType) + if err != nil { + return nil, err + } + itemReflectedType = subItemType + case MapType: + subItemType, err := makeMapType(*itemType.ItemType) + if err != nil { + return nil, err + } + itemReflectedType = subItemType + // TODO(directxman12): support non-uniform slices? (probably not) + default: + return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type) + } + + if itemType.Pointer { + itemReflectedType = reflect.PtrTo(itemReflectedType) + } + + return reflect.SliceOf(itemReflectedType), nil +} + +// makeMapType makes a reflect.Type for a map of the given item type. +// Useful for constructing the out value for when AnyType's guess returns a map. +func makeMapType(itemType Argument) (reflect.Type, error) { + var itemReflectedType reflect.Type + switch itemType.Type { + case IntType: + itemReflectedType = reflect.TypeOf(int(0)) + case NumberType: + itemReflectedType = reflect.TypeOf(float64(0)) + case StringType: + itemReflectedType = reflect.TypeOf("") + case BoolType: + itemReflectedType = reflect.TypeOf(false) + case SliceType: + subItemType, err := makeSliceType(*itemType.ItemType) + if err != nil { + return nil, err + } + itemReflectedType = subItemType + // TODO(directxman12): support non-uniform slices? (probably not) + case MapType: + subItemType, err := makeMapType(*itemType.ItemType) + if err != nil { + return nil, err + } + itemReflectedType = subItemType + case AnyType: + // NB(directxman12): maps explicitly allow non-uniform item types, unlike slices at the moment + itemReflectedType = interfaceType + default: + return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type) + } + + if itemType.Pointer { + itemReflectedType = reflect.PtrTo(itemReflectedType) + } + + return reflect.MapOf(reflect.TypeOf(""), itemReflectedType), nil +} + +// guessType takes an educated guess about the type of the next field. If allowSlice +// is false, it will not guess slices. It's less efficient than parsing with actual +// type information, since we need to allocate to peek ahead full tokens, and the scanner +// only allows peeking ahead one character. +// Maps are *always* non-uniform (i.e. type the AnyType item type), since they're frequently +// used to represent things like defaults for an object in JSON. +func guessType(scanner *sc.Scanner, raw string, allowSlice bool) *Argument { + if allowSlice { + maybeItem := guessType(scanner, raw, false) + + subRaw := raw[scanner.Pos().Offset:] + subScanner := parserScanner(subRaw, scanner.Error) + + var tok rune + for tok = subScanner.Scan(); tok != ',' && tok != sc.EOF && tok != ';'; tok = subScanner.Scan() { + // wait till we get something interesting + } + + // semicolon means it's a legacy slice + if tok == ';' { + return &Argument{ + Type: SliceType, + ItemType: maybeItem, + } + } + + return maybeItem + } + + // everything else needs a duplicate scanner to scan properly + // (so we don't consume our scanner tokens until we actually + // go to use this -- Go doesn't like scanners that can be rewound). + subRaw := raw[scanner.Pos().Offset:] + subScanner := parserScanner(subRaw, scanner.Error) + + // skip whitespace + hint := peekNoSpace(subScanner) + + // first, try the easy case -- quoted strings strings + switch hint { + case '"', '\'', '`': + return &Argument{Type: StringType} + } + + // next, check for slices or maps + if hint == '{' { + subScanner.Scan() + + // TODO(directxman12): this can't guess at empty objects, but that's generally ok. + // We'll cross that bridge when we get there. + + // look ahead till we can figure out if this is a map or a slice + firstElemType := guessType(subScanner, subRaw, false) + if firstElemType.Type == StringType { + // might be a map or slice, parse the string and check for colon + // (blech, basically arbitrary look-ahead due to raw strings). + var keyVal string // just ignore this + (&Argument{Type: StringType}).parseString(subScanner, raw, reflect.Indirect(reflect.ValueOf(&keyVal))) + + if subScanner.Scan() == ':' { + // it's got a string followed by a colon -- it's a map + return &Argument{ + Type: MapType, + ItemType: &Argument{Type: AnyType}, + } + } + } + + // definitely a slice -- maps have to have string keys and have a value followed by a colon + return &Argument{ + Type: SliceType, + ItemType: firstElemType, + } + } + + // then, bools... + probablyString := false + if hint == 't' || hint == 'f' { + // maybe a bool + if nextTok := subScanner.Scan(); nextTok == sc.Ident { + switch subScanner.TokenText() { + case "true", "false": + // definitely a bool + return &Argument{Type: BoolType} + } + // probably a string + probablyString = true + } else { + // we shouldn't ever get here + scanner.Error(scanner, fmt.Sprintf("got a token (%q) that looked like an ident, but was not", scanner.TokenText())) + return &Argument{Type: InvalidType} + } + } + + // then, integers... + if !probablyString { + nextTok := subScanner.Scan() + if nextTok == '-' { + nextTok = subScanner.Scan() + } + + if nextTok == sc.Int { + return &Argument{Type: IntType} + } + if nextTok == sc.Float { + return &Argument{Type: NumberType} + } + } + + // otherwise assume bare strings + return &Argument{Type: StringType} +} + +// parseString parses either of the two accepted string forms (quoted, or bare tokens). +func (a *Argument) parseString(scanner *sc.Scanner, raw string, out reflect.Value) { + // strings are a bit weird -- the "easy" case is quoted strings (tokenized as strings), + // the "hard" case (present for backwards compat) is a bare sequence of tokens that aren't + // a comma. + tok := scanner.Scan() + if tok == sc.String || tok == sc.RawString { + // the easy case + val, err := strconv.Unquote(scanner.TokenText()) + if err != nil { + scanner.Error(scanner, fmt.Sprintf("unable to parse string: %v", err)) + return + } + castAndSet(out, reflect.ValueOf(val)) + return + } + + // the "hard" case -- bare tokens not including ',' (the argument + // separator), ';' (the slice separator), ':' (the map separator), or '}' + // (delimitted slice ender) + startPos := scanner.Position.Offset + for hint := peekNoSpace(scanner); hint != ',' && hint != ';' && hint != ':' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) { + // skip this token + scanner.Scan() + } + endPos := scanner.Position.Offset + len(scanner.TokenText()) + castAndSet(out, reflect.ValueOf(raw[startPos:endPos])) +} + +// parseSlice parses either of the two slice forms (curly-brace-delimitted and semicolon-separated). +func (a *Argument) parseSlice(scanner *sc.Scanner, raw string, out reflect.Value) { + // slices have two supported formats, like string: + // - `{val, val, val}` (preferred) + // - `val;val;val` (legacy) + resSlice := reflect.Zero(out.Type()) + elem := reflect.Indirect(reflect.New(out.Type().Elem())) + + // preferred case + if peekNoSpace(scanner) == '{' { + // NB(directxman12): supporting delimitted slices in bare slices + // would require an extra look-ahead here :-/ + + scanner.Scan() // skip '{' + for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) { + a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */) + resSlice = reflect.Append(resSlice, elem) + tok := peekNoSpace(scanner) + if tok == '}' { + break + } + if !expect(scanner, ',', "comma") { + return + } + } + if !expect(scanner, '}', "close curly brace") { + return + } + castAndSet(out, resSlice) + return + } + + // legacy case + for hint := peekNoSpace(scanner); hint != ',' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) { + a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */) + resSlice = reflect.Append(resSlice, elem) + tok := peekNoSpace(scanner) + if tok == ',' || tok == '}' || tok == sc.EOF { + break + } + scanner.Scan() + if tok != ';' { + scanner.Error(scanner, fmt.Sprintf("expected comma, got %q", scanner.TokenText())) + return + } + } + castAndSet(out, resSlice) +} + +// parseMap parses a map of the form {string: val, string: val, string: val} +func (a *Argument) parseMap(scanner *sc.Scanner, raw string, out reflect.Value) { + resMap := reflect.MakeMap(out.Type()) + elem := reflect.Indirect(reflect.New(out.Type().Elem())) + key := reflect.Indirect(reflect.New(out.Type().Key())) + + if !expect(scanner, '{', "open curly brace") { + return + } + + for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) { + a.parseString(scanner, raw, key) + if !expect(scanner, ':', "colon") { + return + } + a.ItemType.parse(scanner, raw, elem, false /* not in a slice */) + resMap.SetMapIndex(key, elem) + + if peekNoSpace(scanner) == '}' { + break + } + if !expect(scanner, ',', "comma") { + return + } + } + + if !expect(scanner, '}', "close curly brace") { + return + } + + castAndSet(out, resMap) +} + +// parse functions like Parse, except that it allows passing down whether or not we're +// already in a slice, to avoid duplicate legacy slice detection for AnyType +func (a *Argument) parse(scanner *sc.Scanner, raw string, out reflect.Value, inSlice bool) { + // nolint:gocyclo + if a.Type == InvalidType { + scanner.Error(scanner, "cannot parse invalid type") + return + } + if a.Pointer { + out.Set(reflect.New(out.Type().Elem())) + out = reflect.Indirect(out) + } + switch a.Type { + case RawType: + // raw consumes everything else + castAndSet(out, reflect.ValueOf(raw[scanner.Pos().Offset:])) + // consume everything else + for tok := scanner.Scan(); tok != sc.EOF; tok = scanner.Scan() { + } + case NumberType: + nextChar := scanner.Peek() + isNegative := false + if nextChar == '-' { + isNegative = true + scanner.Scan() // eat the '-' + } + + tok := scanner.Scan() + if tok != sc.Float && tok != sc.Int { + scanner.Error(scanner, fmt.Sprintf("expected integer or float, got %q", scanner.TokenText())) + return + } + + text := scanner.TokenText() + if isNegative { + text = "-" + text + } + + val, err := strconv.ParseFloat(text, 64) + if err != nil { + scanner.Error(scanner, fmt.Sprintf("unable to parse number: %v", err)) + return + } + + castAndSet(out, reflect.ValueOf(val)) + case IntType: + nextChar := scanner.Peek() + isNegative := false + if nextChar == '-' { + isNegative = true + scanner.Scan() // eat the '-' + } + if !expect(scanner, sc.Int, "integer") { + return + } + // TODO(directxman12): respect the size when parsing + text := scanner.TokenText() + if isNegative { + text = "-" + text + } + val, err := strconv.Atoi(text) + if err != nil { + scanner.Error(scanner, fmt.Sprintf("unable to parse integer: %v", err)) + return + } + castAndSet(out, reflect.ValueOf(val)) + case StringType: + // strings are a bit weird -- the "easy" case is quoted strings (tokenized as strings), + // the "hard" case (present for backwards compat) is a bare sequence of tokens that aren't + // a comma. + a.parseString(scanner, raw, out) + case BoolType: + if !expect(scanner, sc.Ident, "true or false") { + return + } + switch scanner.TokenText() { + case "true": + castAndSet(out, reflect.ValueOf(true)) + case "false": + castAndSet(out, reflect.ValueOf(false)) + default: + scanner.Error(scanner, fmt.Sprintf("expected true or false, got %q", scanner.TokenText())) + return + } + case AnyType: + guessedType := guessType(scanner, raw, !inSlice) + newOut := out + + // we need to be able to construct the right element types, below + // in parse, so construct a concretely-typed value to use as "out" + switch guessedType.Type { + case SliceType: + newType, err := makeSliceType(*guessedType.ItemType) + if err != nil { + scanner.Error(scanner, err.Error()) + return + } + newOut = reflect.Indirect(reflect.New(newType)) + case MapType: + newType, err := makeMapType(*guessedType.ItemType) + if err != nil { + scanner.Error(scanner, err.Error()) + return + } + newOut = reflect.Indirect(reflect.New(newType)) + } + if !newOut.CanSet() { + panic("at the disco") // TODO(directxman12): this is left over from debugging -- it might need to be an error + } + guessedType.Parse(scanner, raw, newOut) + castAndSet(out, newOut) + case SliceType: + // slices have two supported formats, like string: + // - `{val, val, val}` (preferred) + // - `val;val;val` (legacy) + a.parseSlice(scanner, raw, out) + case MapType: + // maps are {string: val, string: val, string: val} + a.parseMap(scanner, raw, out) + } +} + +// Parse attempts to consume the argument from the given scanner (based on the given +// raw input as well for collecting ranges of content), and places the output value +// in the given reflect.Value. Errors are reported via the given scanner. +func (a *Argument) Parse(scanner *sc.Scanner, raw string, out reflect.Value) { + a.parse(scanner, raw, out, false) +} + +// ArgumentFromType constructs an Argument by examining the given +// raw reflect.Type. It can construct arguments from the Go types +// corresponding to any of the types listed in ArgumentType. +func ArgumentFromType(rawType reflect.Type) (Argument, error) { + if rawType == rawArgsType { + return Argument{ + Type: RawType, + }, nil + } + + if rawType == interfaceType { + return Argument{ + Type: AnyType, + }, nil + } + + arg := Argument{} + if rawType.Kind() == reflect.Ptr { + rawType = rawType.Elem() + arg.Pointer = true + arg.Optional = true + } + + switch rawType.Kind() { + case reflect.String: + arg.Type = StringType + case reflect.Int, reflect.Int32: // NB(directxman12): all ints in kubernetes are int32, so explicitly support that + arg.Type = IntType + case reflect.Float64: + arg.Type = NumberType + case reflect.Bool: + arg.Type = BoolType + case reflect.Slice: + arg.Type = SliceType + itemType, err := ArgumentFromType(rawType.Elem()) + if err != nil { + return Argument{}, fmt.Errorf("bad slice item type: %w", err) + } + arg.ItemType = &itemType + case reflect.Map: + arg.Type = MapType + if rawType.Key().Kind() != reflect.String { + return Argument{}, fmt.Errorf("bad map key type: map keys must be strings") + } + itemType, err := ArgumentFromType(rawType.Elem()) + if err != nil { + return Argument{}, fmt.Errorf("bad slice item type: %w", err) + } + arg.ItemType = &itemType + default: + return Argument{}, fmt.Errorf("type has unsupported kind %s", rawType.Kind()) + } + + return arg, nil +} + +// TargetType describes which kind of node a given marker is associated with. +type TargetType int + +const ( + // DescribesPackage indicates that a marker is associated with a package. + DescribesPackage TargetType = iota + // DescribesType indicates that a marker is associated with a type declaration. + DescribesType + // DescribesField indicates that a marker is associated with a struct field. + DescribesField +) + +func (t TargetType) String() string { + switch t { + case DescribesPackage: + return "package" + case DescribesType: + return "type" + case DescribesField: + return "field" + default: + return "(unknown)" + } +} + +// Definition is a parsed definition of a marker. +type Definition struct { + // Output is the deserialized Go type of the marker. + Output reflect.Type + // Name is the marker's name. + Name string + // Target indicates which kind of node this marker can be associated with. + Target TargetType + // Fields lists out the types of each field that this marker has, by + // argument name as used in the marker (if the output type isn't a struct, + // it'll have a single, blank field name). This only lists exported fields, + // (as per reflection rules). + Fields map[string]Argument + // FieldNames maps argument names (as used in the marker) to struct field name + // in the output type. + FieldNames map[string]string + // Strict indicates that this definition should error out when parsing if + // not all non-optional fields were seen. + Strict bool +} + +// AnonymousField indicates that the definition has one field, +// (actually the original object), and thus the field +// doesn't get named as part of the name. +func (d *Definition) AnonymousField() bool { + if len(d.Fields) != 1 { + return false + } + _, hasAnonField := d.Fields[""] + return hasAnonField +} + +// Empty indicates that this definition has no fields. +func (d *Definition) Empty() bool { + return len(d.Fields) == 0 +} + +// argumentInfo returns information about an argument field as the marker parser's field loader +// would see it. This can be useful if you have to interact with marker definition structs +// externally (e.g. at compile time). +func argumentInfo(fieldName string, tag reflect.StructTag) (argName string, optionalOpt bool) { + argName = lowerCamelCase(fieldName) + markerTag, tagSpecified := tag.Lookup("marker") + markerTagParts := strings.Split(markerTag, ",") + if tagSpecified && markerTagParts[0] != "" { + // allow overriding to support legacy cases where we don't follow camelCase conventions + argName = markerTagParts[0] + } + optionalOpt = false + for _, tagOption := range markerTagParts[1:] { + switch tagOption { + case "optional": + optionalOpt = true + } + } + + return argName, optionalOpt +} + +// loadFields uses reflection to populate argument information from the Output type. +func (d *Definition) loadFields() error { + if d.Fields == nil { + d.Fields = make(map[string]Argument) + d.FieldNames = make(map[string]string) + } + if d.Output.Kind() != reflect.Struct { + // anonymous field type + argType, err := ArgumentFromType(d.Output) + if err != nil { + return err + } + d.Fields[""] = argType + d.FieldNames[""] = "" + return nil + } + + for i := 0; i < d.Output.NumField(); i++ { + field := d.Output.Field(i) + if field.PkgPath != "" { + // as per the reflect package docs, pkgpath is empty for exported fields, + // so non-empty package path means a private field, which we should skip + continue + } + argName, optionalOpt := argumentInfo(field.Name, field.Tag) + + argType, err := ArgumentFromType(field.Type) + if err != nil { + return fmt.Errorf("unable to extract type information for field %q: %w", field.Name, err) + } + + if argType.Type == RawType { + return fmt.Errorf("RawArguments must be the direct type of a marker, and not a field") + } + + argType.Optional = optionalOpt || argType.Optional + + d.Fields[argName] = argType + d.FieldNames[argName] = field.Name + } + + return nil +} + +// parserScanner makes a new scanner appropriate for use in parsing definitions and arguments. +func parserScanner(raw string, err func(*sc.Scanner, string)) *sc.Scanner { + scanner := &sc.Scanner{} + scanner.Init(bytes.NewBufferString(raw)) + scanner.Mode = sc.ScanIdents | sc.ScanInts | sc.ScanFloats | sc.ScanStrings | sc.ScanRawStrings | sc.SkipComments + scanner.Error = err + + return scanner +} + +// Parse uses the type information in this Definition to parse the given +// raw marker in the form `+a:b:c=arg,d=arg` into an output object of the +// type specified in the definition. +func (d *Definition) Parse(rawMarker string) (interface{}, error) { + name, anonName, fields := splitMarker(rawMarker) + + out := reflect.Indirect(reflect.New(d.Output)) + + // if we're a not a struct or have no arguments, treat the full `a:b:c` as the name, + // otherwise, treat `c` as a field name, and `a:b` as the marker name. + if !d.AnonymousField() && !d.Empty() && len(anonName) >= len(name)+1 { + fields = anonName[len(name)+1:] + "=" + fields + } + + var errs []error + scanner := parserScanner(fields, func(scanner *sc.Scanner, msg string) { + errs = append(errs, &ScannerError{Msg: msg, Pos: scanner.Position}) + }) + + // TODO(directxman12): strict parsing where we error out if certain fields aren't optional + seen := make(map[string]struct{}, len(d.Fields)) + if d.AnonymousField() && scanner.Peek() != sc.EOF { + // might still be a struct that something fiddled with, so double check + structFieldName := d.FieldNames[""] + outTarget := out + if structFieldName != "" { + // it's a struct field mapped to an anonymous marker + outTarget = out.FieldByName(structFieldName) + if !outTarget.CanSet() { + scanner.Error(scanner, fmt.Sprintf("cannot set field %q (might not exist)", structFieldName)) + return out.Interface(), loader.MaybeErrList(errs) + } + } + + // no need for trying to parse field names if we're not a struct + field := d.Fields[""] + field.Parse(scanner, fields, outTarget) + seen[""] = struct{}{} // mark as seen for strict definitions + } else if !d.Empty() && scanner.Peek() != sc.EOF { + // if we expect *and* actually have arguments passed + for { + // parse the argument name + if !expect(scanner, sc.Ident, "argument name") { + break + } + argName := scanner.TokenText() + if !expect(scanner, '=', "equals") { + break + } + + // make sure we know the field + fieldName, known := d.FieldNames[argName] + if !known { + scanner.Error(scanner, fmt.Sprintf("unknown argument %q", argName)) + break + } + fieldType, known := d.Fields[argName] + if !known { + scanner.Error(scanner, fmt.Sprintf("unknown argument %q", argName)) + break + } + seen[argName] = struct{}{} // mark as seen for strict definitions + + // parse the field value + fieldVal := out.FieldByName(fieldName) + if !fieldVal.CanSet() { + scanner.Error(scanner, fmt.Sprintf("cannot set field %q (might not exist)", fieldName)) + break + } + fieldType.Parse(scanner, fields, fieldVal) + + if len(errs) > 0 { + break + } + + if scanner.Peek() == sc.EOF { + break + } + if !expect(scanner, ',', "comma") { + break + } + } + } + + if tok := scanner.Scan(); tok != sc.EOF { + scanner.Error(scanner, fmt.Sprintf("extra arguments provided: %q", fields[scanner.Position.Offset:])) + } + + if d.Strict { + for argName, arg := range d.Fields { + if _, wasSeen := seen[argName]; !wasSeen && !arg.Optional { + scanner.Error(scanner, fmt.Sprintf("missing argument %q", argName)) + } + } + } + + return out.Interface(), loader.MaybeErrList(errs) +} + +// MakeDefinition constructs a definition from a name, type, and the output type. +// All such definitions are strict by default. If a struct is passed as the output +// type, its public fields will automatically be populated into Fields (and similar +// fields in Definition). Other values will have a single, empty-string-named Fields +// entry. +func MakeDefinition(name string, target TargetType, output interface{}) (*Definition, error) { + def := &Definition{ + Name: name, + Target: target, + Output: reflect.TypeOf(output), + Strict: true, + } + + if err := def.loadFields(); err != nil { + return nil, err + } + + return def, nil +} + +// MakeAnyTypeDefinition constructs a definition for an output struct with a +// field named `Value` of type `interface{}`. The argument to the marker will +// be parsed as AnyType and assigned to the field named `Value`. +func MakeAnyTypeDefinition(name string, target TargetType, output interface{}) (*Definition, error) { + defn, err := MakeDefinition(name, target, output) + if err != nil { + return nil, err + } + defn.FieldNames = map[string]string{"": "Value"} + defn.Fields = map[string]Argument{"": defn.Fields["value"]} + return defn, nil +} + +// splitMarker takes a marker in the form of `+a:b:c=arg,d=arg` and splits it +// into the name (`a:b`), the name if it's not a struct (`a:b:c`), and the parts +// that are definitely fields (`arg,d=arg`). +func splitMarker(raw string) (name string, anonymousName string, restFields string) { + raw = raw[1:] // get rid of the leading '+' + nameFieldParts := strings.SplitN(raw, "=", 2) + if len(nameFieldParts) == 1 { + return nameFieldParts[0], nameFieldParts[0], "" + } + anonymousName = nameFieldParts[0] + name = anonymousName + restFields = nameFieldParts[1] + + nameParts := strings.Split(name, ":") + if len(nameParts) > 1 { + name = strings.Join(nameParts[:len(nameParts)-1], ":") + } + return name, anonymousName, restFields +} + +type ScannerError struct { + Msg string + Pos sc.Position +} + +func (e *ScannerError) Error() string { + return fmt.Sprintf("%s (at %s)", e.Msg, e.Pos) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/reg.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/reg.go new file mode 100644 index 0000000000..7dcd458994 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/reg.go @@ -0,0 +1,154 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "fmt" + "sync" +) + +// Registry keeps track of registered definitions, and allows for easy lookup. +// It's thread-safe, and the zero-value can be safely used. +type Registry struct { + forPkg map[string]*Definition + forType map[string]*Definition + forField map[string]*Definition + helpFor map[*Definition]*DefinitionHelp + + mu sync.RWMutex + initOnce sync.Once +} + +func (r *Registry) init() { + r.initOnce.Do(func() { + if r.forPkg == nil { + r.forPkg = make(map[string]*Definition) + } + if r.forType == nil { + r.forType = make(map[string]*Definition) + } + if r.forField == nil { + r.forField = make(map[string]*Definition) + } + if r.helpFor == nil { + r.helpFor = make(map[*Definition]*DefinitionHelp) + } + }) +} + +// Define defines a new marker with the given name, target, and output type. +// It's a shortcut around +// +// r.Register(MakeDefinition(name, target, obj)) +func (r *Registry) Define(name string, target TargetType, obj interface{}) error { + def, err := MakeDefinition(name, target, obj) + if err != nil { + return err + } + return r.Register(def) +} + +// Register registers the given marker definition with this registry for later lookup. +func (r *Registry) Register(def *Definition) error { + r.init() + + r.mu.Lock() + defer r.mu.Unlock() + + switch def.Target { + case DescribesPackage: + r.forPkg[def.Name] = def + case DescribesType: + r.forType[def.Name] = def + case DescribesField: + r.forField[def.Name] = def + default: + return fmt.Errorf("unknown target type %v", def.Target) + } + return nil +} + +// AddHelp stores the given help in the registry, marking it as associated with +// the given definition. +func (r *Registry) AddHelp(def *Definition, help *DefinitionHelp) { + r.init() + + r.mu.Lock() + defer r.mu.Unlock() + + r.helpFor[def] = help +} + +// Lookup fetches the definition corresponding to the given name and target type. +func (r *Registry) Lookup(name string, target TargetType) *Definition { + r.init() + + r.mu.RLock() + defer r.mu.RUnlock() + + switch target { + case DescribesPackage: + return tryAnonLookup(name, r.forPkg) + case DescribesType: + return tryAnonLookup(name, r.forType) + case DescribesField: + return tryAnonLookup(name, r.forField) + default: + return nil + } +} + +// HelpFor fetches the help for a given definition, if present. +func (r *Registry) HelpFor(def *Definition) *DefinitionHelp { + r.init() + + r.mu.RLock() + defer r.mu.RUnlock() + + return r.helpFor[def] +} + +// AllDefinitions returns all marker definitions known to this registry. +func (r *Registry) AllDefinitions() []*Definition { + res := make([]*Definition, 0, len(r.forPkg)+len(r.forType)+len(r.forField)) + for _, def := range r.forPkg { + res = append(res, def) + } + for _, def := range r.forType { + res = append(res, def) + } + for _, def := range r.forField { + res = append(res, def) + } + return res +} + +// tryAnonLookup tries looking up the given marker as both an struct-based +// marker and an anonymous marker, returning whichever format matches first, +// preferring the longer (anonymous) name in case of conflicts. +func tryAnonLookup(name string, defs map[string]*Definition) *Definition { + // NB(directxman12): we look up anonymous names first to work with + // legacy style marker definitions that have a namespaced approach + // (e.g. deepcopy-gen, which uses `+k8s:deepcopy-gen=foo,bar` *and* + // `+k8s.io:deepcopy-gen:interfaces=foo`). + name, anonName, _ := splitMarker(name) + if def, exists := defs[anonName]; exists { + return def + } + + return defs[name] +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/regutil.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/regutil.go new file mode 100644 index 0000000000..a9160c3c2f --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/regutil.go @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +// Must panics on errors creating definitions. +func Must(def *Definition, err error) *Definition { + if err != nil { + panic(err) + } + return def +} + +// RegisterAll attempts to register all definitions against the given registry, +// stopping and returning if an error occurs. +func RegisterAll(reg *Registry, defs ...*Definition) error { + for _, def := range defs { + if err := reg.Register(def); err != nil { + return err + } + } + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/markers/zip.go b/vendor/sigs.k8s.io/controller-tools/pkg/markers/zip.go new file mode 100644 index 0000000000..b352ededc7 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/markers/zip.go @@ -0,0 +1,201 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package markers + +import ( + "go/ast" + "go/token" + "reflect" + "strings" + + "sigs.k8s.io/controller-tools/pkg/loader" +) + +// extractDoc extracts documentation from the given node, skipping markers +// in the godoc and falling back to the decl if necessary (for single-line decls). +func extractDoc(node ast.Node, decl *ast.GenDecl) string { + var docs *ast.CommentGroup + switch docced := node.(type) { + case *ast.Field: + docs = docced.Doc + case *ast.File: + docs = docced.Doc + case *ast.GenDecl: + docs = docced.Doc + case *ast.TypeSpec: + docs = docced.Doc + // type Ident expr expressions get docs attached to the decl, + // so check for that case (missing Lparen == single line type decl) + if docs == nil && decl.Lparen == token.NoPos { + docs = decl.Doc + } + } + + if docs == nil { + return "" + } + + // filter out markers + var outGroup ast.CommentGroup + outGroup.List = make([]*ast.Comment, 0, len(docs.List)) + for _, comment := range docs.List { + if isMarkerComment(comment.Text) { + continue + } + outGroup.List = append(outGroup.List, comment) + } + + // split lines, and re-join together as a single + // paragraph, respecting double-newlines as + // paragraph markers. + outLines := strings.Split(outGroup.Text(), "\n") + if outLines[len(outLines)-1] == "" { + // chop off the extraneous last part + outLines = outLines[:len(outLines)-1] + } + + for i, line := range outLines { + // Trim any extranous whitespace, + // for handling /*…*/-style comments, + // which have whitespace preserved in go/ast: + line = strings.TrimSpace(line) + + // Respect that double-newline means + // actual newline: + if line == "" { + outLines[i] = "\n" + } else { + outLines[i] = line + } + } + + return strings.Join(outLines, " ") +} + +// PackageMarkers collects all the package-level marker values for the given package. +func PackageMarkers(col *Collector, pkg *loader.Package) (MarkerValues, error) { + markers, err := col.MarkersInPackage(pkg) + if err != nil { + return nil, err + } + res := make(MarkerValues) + for _, file := range pkg.Syntax { + fileMarkers := markers[file] + for name, vals := range fileMarkers { + res[name] = append(res[name], vals...) + } + } + + return res, nil +} + +// FieldInfo contains marker values and commonly used information for a struct field. +type FieldInfo struct { + // Name is the name of the field (or "" for embedded fields) + Name string + // Doc is the Godoc of the field, pre-processed to remove markers and joine + // single newlines together. + Doc string + // Tag struct tag associated with this field (or "" if non existed). + Tag reflect.StructTag + + // Markers are all registered markers associated with this field. + Markers MarkerValues + + // RawField is the raw, underlying field AST object that this field represents. + RawField *ast.Field +} + +// TypeInfo contains marker values and commonly used information for a type declaration. +type TypeInfo struct { + // Name is the name of the type. + Name string + // Doc is the Godoc of the type, pre-processed to remove markers and joine + // single newlines together. + Doc string + + // Markers are all registered markers associated with the type. + Markers MarkerValues + + // Fields are all the fields associated with the type, if it's a struct. + // (if not, Fields will be nil). + Fields []FieldInfo + + // RawDecl contains the raw GenDecl that the type was declared as part of. + RawDecl *ast.GenDecl + // RawSpec contains the raw Spec that declared this type. + RawSpec *ast.TypeSpec + // RawFile contains the file in which this type was declared. + RawFile *ast.File +} + +// TypeCallback is a callback called for each type declaration in a package. +type TypeCallback func(info *TypeInfo) + +// EachType collects all markers, then calls the given callback for each type declaration in a package. +// Each individual spec is considered separate, so +// +// type ( +// Foo string +// Bar int +// Baz struct{} +// ) +// +// yields three calls to the callback. +func EachType(col *Collector, pkg *loader.Package, cb TypeCallback) error { + markers, err := col.MarkersInPackage(pkg) + if err != nil { + return err + } + + loader.EachType(pkg, func(file *ast.File, decl *ast.GenDecl, spec *ast.TypeSpec) { + var fields []FieldInfo + if structSpec, isStruct := spec.Type.(*ast.StructType); isStruct { + for _, field := range structSpec.Fields.List { + for _, name := range field.Names { + fields = append(fields, FieldInfo{ + Name: name.Name, + Doc: extractDoc(field, nil), + Tag: loader.ParseAstTag(field.Tag), + Markers: markers[field], + RawField: field, + }) + } + if field.Names == nil { + fields = append(fields, FieldInfo{ + Doc: extractDoc(field, nil), + Tag: loader.ParseAstTag(field.Tag), + Markers: markers[field], + RawField: field, + }) + } + } + } + + cb(&TypeInfo{ + Name: spec.Name.Name, + Markers: markers[spec], + Doc: extractDoc(spec, decl), + Fields: fields, + RawDecl: decl, + RawSpec: spec, + RawFile: file, + }) + }) + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/rbac/parser.go b/vendor/sigs.k8s.io/controller-tools/pkg/rbac/parser.go new file mode 100644 index 0000000000..c2a24f4710 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/rbac/parser.go @@ -0,0 +1,267 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rbac contain libraries for generating RBAC manifests from RBAC +// markers in Go source files. +// +// The markers take the form: +// +// +kubebuilder:rbac:groups=,resources=,resourceNames=,verbs=,urls= +package rbac + +import ( + "fmt" + "sort" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +var ( + // RuleDefinition is a marker for defining RBAC rules. + // Call ToRule on the value to get a Kubernetes RBAC policy rule. + RuleDefinition = markers.Must(markers.MakeDefinition("kubebuilder:rbac", markers.DescribesPackage, Rule{})) +) + +// +controllertools:marker:generateHelp:category=RBAC + +// Rule specifies an RBAC rule to all access to some resources or non-resource URLs. +type Rule struct { + // Groups specifies the API groups that this rule encompasses. + Groups []string `marker:",optional"` + // Resources specifies the API resources that this rule encompasses. + Resources []string `marker:",optional"` + // ResourceNames specifies the names of the API resources that this rule encompasses. + // + // Create requests cannot be restricted by resourcename, as the object's name + // is not known at authorization time. + ResourceNames []string `marker:",optional"` + // Verbs specifies the (lowercase) kubernetes API verbs that this rule encompasses. + Verbs []string + // URL specifies the non-resource URLs that this rule encompasses. + URLs []string `marker:"urls,optional"` + // Namespace specifies the scope of the Rule. + // If not set, the Rule belongs to the generated ClusterRole. + // If set, the Rule belongs to a Role, whose namespace is specified by this field. + Namespace string `marker:",optional"` +} + +// ruleKey represents the resources and non-resources a Rule applies. +type ruleKey struct { + Groups string + Resources string + ResourceNames string + URLs string +} + +func (key ruleKey) String() string { + return fmt.Sprintf("%s + %s + %s + %s", key.Groups, key.Resources, key.ResourceNames, key.URLs) +} + +// ruleKeys implements sort.Interface +type ruleKeys []ruleKey + +func (keys ruleKeys) Len() int { return len(keys) } +func (keys ruleKeys) Swap(i, j int) { keys[i], keys[j] = keys[j], keys[i] } +func (keys ruleKeys) Less(i, j int) bool { return keys[i].String() < keys[j].String() } + +// key normalizes the Rule and returns a ruleKey object. +func (r *Rule) key() ruleKey { + r.normalize() + return ruleKey{ + Groups: strings.Join(r.Groups, "&"), + Resources: strings.Join(r.Resources, "&"), + ResourceNames: strings.Join(r.ResourceNames, "&"), + URLs: strings.Join(r.URLs, "&"), + } +} + +// addVerbs adds new verbs into a Rule. +// The duplicates in `r.Verbs` will be removed, and then `r.Verbs` will be sorted. +func (r *Rule) addVerbs(verbs []string) { + r.Verbs = removeDupAndSort(append(r.Verbs, verbs...)) +} + +// normalize removes duplicates from each field of a Rule, and sorts each field. +func (r *Rule) normalize() { + r.Groups = removeDupAndSort(r.Groups) + r.Resources = removeDupAndSort(r.Resources) + r.ResourceNames = removeDupAndSort(r.ResourceNames) + r.Verbs = removeDupAndSort(r.Verbs) + r.URLs = removeDupAndSort(r.URLs) +} + +// removeDupAndSort removes duplicates in strs, sorts the items, and returns a +// new slice of strings. +func removeDupAndSort(strs []string) []string { + set := make(map[string]bool) + for _, str := range strs { + if _, ok := set[str]; !ok { + set[str] = true + } + } + + var result []string + for str := range set { + result = append(result, str) + } + sort.Strings(result) + return result +} + +// ToRule converts this rule to its Kubernetes API form. +func (r *Rule) ToRule() rbacv1.PolicyRule { + // fix the group names first, since letting people type "core" is nice + for i, group := range r.Groups { + if group == "core" { + r.Groups[i] = "" + } + } + return rbacv1.PolicyRule{ + APIGroups: r.Groups, + Verbs: r.Verbs, + Resources: r.Resources, + ResourceNames: r.ResourceNames, + NonResourceURLs: r.URLs, + } +} + +// +controllertools:marker:generateHelp + +// Generator generates ClusterRole objects. +type Generator struct { + // RoleName sets the name of the generated ClusterRole. + RoleName string +} + +func (Generator) RegisterMarkers(into *markers.Registry) error { + if err := into.Register(RuleDefinition); err != nil { + return err + } + into.AddHelp(RuleDefinition, Rule{}.Help()) + return nil +} + +// GenerateRoles generate a slice of objs representing either a ClusterRole or a Role object +// The order of the objs in the returned slice is stable and determined by their namespaces. +func GenerateRoles(ctx *genall.GenerationContext, roleName string) ([]interface{}, error) { + rulesByNS := make(map[string][]*Rule) + for _, root := range ctx.Roots { + markerSet, err := markers.PackageMarkers(ctx.Collector, root) + if err != nil { + root.AddError(err) + } + + // group RBAC markers by namespace + for _, markerValue := range markerSet[RuleDefinition.Name] { + rule := markerValue.(Rule) + namespace := rule.Namespace + if _, ok := rulesByNS[namespace]; !ok { + rules := make([]*Rule, 0) + rulesByNS[namespace] = rules + } + rulesByNS[namespace] = append(rulesByNS[namespace], &rule) + } + } + + // NormalizeRules merge Rule with the same ruleKey and sort the Rules + NormalizeRules := func(rules []*Rule) []rbacv1.PolicyRule { + ruleMap := make(map[ruleKey]*Rule) + // all the Rules having the same ruleKey will be merged into the first Rule + for _, rule := range rules { + key := rule.key() + if _, ok := ruleMap[key]; !ok { + ruleMap[key] = rule + continue + } + ruleMap[key].addVerbs(rule.Verbs) + } + + // sort the Rules in rules according to their ruleKeys + keys := make([]ruleKey, 0, len(ruleMap)) + for key := range ruleMap { + keys = append(keys, key) + } + sort.Sort(ruleKeys(keys)) + + var policyRules []rbacv1.PolicyRule + for _, key := range keys { + policyRules = append(policyRules, ruleMap[key].ToRule()) + + } + return policyRules + } + + // collect all the namespaces and sort them + var namespaces []string + for ns := range rulesByNS { + namespaces = append(namespaces, ns) + } + sort.Strings(namespaces) + + // process the items in rulesByNS by the order specified in `namespaces` to make sure that the Role order is stable + var objs []interface{} + for _, ns := range namespaces { + rules := rulesByNS[ns] + policyRules := NormalizeRules(rules) + if len(policyRules) == 0 { + continue + } + if ns == "" { + objs = append(objs, rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + }, + Rules: policyRules, + }) + } else { + objs = append(objs, rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: ns, + }, + Rules: policyRules, + }) + } + } + + return objs, nil +} + +func (g Generator) Generate(ctx *genall.GenerationContext) error { + objs, err := GenerateRoles(ctx, g.RoleName) + if err != nil { + return err + } + + if len(objs) == 0 { + return nil + } + + return ctx.WriteYAML("role.yaml", objs) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/rbac/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/rbac/zz_generated.markerhelp.go new file mode 100644 index 0000000000..5b6d9c4875 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/rbac/zz_generated.markerhelp.go @@ -0,0 +1,86 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package rbac + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "generates ClusterRole objects.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "RoleName": { + Summary: "sets the name of the generated ClusterRole.", + Details: "", + }, + "HeaderFile": { + Summary: "specifies the header text (e.g. license) to prepend to generated files.", + Details: "", + }, + "Year": { + Summary: "specifies the year to substitute for \" YEAR\" in the header file.", + Details: "", + }, + }, + } +} + +func (Rule) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "RBAC", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies an RBAC rule to all access to some resources or non-resource URLs.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Groups": { + Summary: "specifies the API groups that this rule encompasses.", + Details: "", + }, + "Resources": { + Summary: "specifies the API resources that this rule encompasses.", + Details: "", + }, + "ResourceNames": { + Summary: "specifies the names of the API resources that this rule encompasses. ", + Details: "Create requests cannot be restricted by resourcename, as the object's name is not known at authorization time.", + }, + "Verbs": { + Summary: "specifies the (lowercase) kubernetes API verbs that this rule encompasses.", + Details: "", + }, + "URLs": { + Summary: "URL specifies the non-resource URLs that this rule encompasses.", + Details: "", + }, + "Namespace": { + Summary: "specifies the scope of the Rule. If not set, the Rule belongs to the generated ClusterRole. If set, the Rule belongs to a Role, whose namespace is specified by this field.", + Details: "", + }, + }, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/gen.go b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/gen.go new file mode 100644 index 0000000000..e33ec11ff5 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/gen.go @@ -0,0 +1,433 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schemapatcher + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "gopkg.in/yaml.v3" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + kyaml "sigs.k8s.io/yaml" + + crdgen "sigs.k8s.io/controller-tools/pkg/crd" + crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" + yamlop "sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml" +) + +// NB(directxman12): this code is quite fragile, but there are a sufficient +// number of corner cases that it's hard to decompose into separate tools. +// When in doubt, ping @sttts. +// +// Namely: +// - It needs to only update existing versions +// - It needs to make "stable" changes that don't mess with map key ordering +// (in order to facilitate validating that no change has occurred) +// - It needs to collapse identical schema versions into a top-level schema, +// if all versions are identical (this is a common requirement to all CRDs, +// but in this case it means simple jsonpatch wouldn't suffice) + +// TODO(directxman12): When CRD v1 rolls around, consider splitting this into a +// tool that generates a patch, and a separate tool for applying stable YAML +// patches. + +var ( + currentAPIExtVersion = apiext.SchemeGroupVersion.String() +) + +// +controllertools:marker:generateHelp + +// Generator patches existing CRDs with new schemata. +// +// It will generate output for each "CRD Version" (API version of the CRD type +// itself) , e.g. apiextensions/v1) available. +type Generator struct { + // ManifestsPath contains the CustomResourceDefinition YAML files. + ManifestsPath string `marker:"manifests"` + + // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema. + // + // 0 indicates drop the description for all fields completely. + // n indicates limit the description to at most n characters and truncate the description to + // closest sentence boundary if it exceeds n characters. + MaxDescLen *int `marker:",optional"` + + // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated + GenerateEmbeddedObjectMeta *bool `marker:",optional"` +} + +var _ genall.Generator = &Generator{} + +func (Generator) CheckFilter() loader.NodeFilter { + return crdgen.Generator{}.CheckFilter() +} + +func (Generator) RegisterMarkers(into *markers.Registry) error { + return crdmarkers.Register(into) +} + +func (g Generator) Generate(ctx *genall.GenerationContext) (result error) { + parser := &crdgen.Parser{ + Collector: ctx.Collector, + Checker: ctx.Checker, + // Indicates the parser on whether to register the ObjectMeta type or not + GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true, + } + + crdgen.AddKnownTypes(parser) + for _, root := range ctx.Roots { + parser.NeedPackage(root) + } + + metav1Pkg := crdgen.FindMetav1(ctx.Roots) + if metav1Pkg == nil { + // no objects in the roots, since nothing imported metav1 + return nil + } + + // load existing CRD manifests with group-kind and versions + partialCRDSets, err := crdsFromDirectory(ctx, g.ManifestsPath) + if err != nil { + return err + } + + // generate schemata for the types we care about, and save them to be written later. + for _, groupKind := range crdgen.FindKubeKinds(parser, metav1Pkg) { + existingSet, wanted := partialCRDSets[groupKind] + if !wanted { + continue + } + + for pkg, gv := range parser.GroupVersions { + if gv.Group != groupKind.Group { + continue + } + if _, wantedVersion := existingSet.Versions[gv.Version]; !wantedVersion { + continue + } + + typeIdent := crdgen.TypeIdent{Package: pkg, Name: groupKind.Kind} + parser.NeedFlattenedSchemaFor(typeIdent) + + fullSchema := parser.FlattenedSchemata[typeIdent] + if g.MaxDescLen != nil { + fullSchema = *fullSchema.DeepCopy() + crdgen.TruncateDescription(&fullSchema, *g.MaxDescLen) + } + + // Fix top level ObjectMeta regardless of the settings. + if _, ok := fullSchema.Properties["metadata"]; ok { + fullSchema.Properties["metadata"] = apiext.JSONSchemaProps{Type: "object"} + } + + existingSet.NewSchemata[gv.Version] = fullSchema + } + } + + // patch existing CRDs with new schemata + for _, existingSet := range partialCRDSets { + // first, figure out if we need to merge schemata together if they're *all* + // identical (meaning we also don't have any "unset" versions) + + if len(existingSet.NewSchemata) == 0 { + continue + } + + // copy over the new versions that we have, keeping old versions so + // that we can tell if a schema would be nil + var someVer string + for ver := range existingSet.NewSchemata { + someVer = ver + existingSet.Versions[ver] = struct{}{} + } + + allSame := true + firstSchema := existingSet.NewSchemata[someVer] + for ver := range existingSet.Versions { + otherSchema, hasSchema := existingSet.NewSchemata[ver] + if !hasSchema || !equality.Semantic.DeepEqual(firstSchema, otherSchema) { + allSame = false + break + } + } + + if allSame { + if err := existingSet.setGlobalSchema(); err != nil { + return fmt.Errorf("failed to set global firstSchema for %s: %w", existingSet.GroupKind, err) + } + } else { + if err := existingSet.setVersionedSchemata(); err != nil { + return fmt.Errorf("failed to set versioned schemas for %s: %w", existingSet.GroupKind, err) + } + } + } + + // write the final result out to the new location + for _, set := range partialCRDSets { + // We assume all CRD versions came from different files, since this + // is how controller-gen works. If they came from the same file, + // it'd be non-sensical, since you couldn't reasonably use kubectl + // with them against older servers. + for _, crd := range set.CRDVersions { + if err := func() error { + outWriter, err := ctx.OutputRule.Open(nil, crd.FileName) + if err != nil { + return err + } + defer outWriter.Close() + + enc := yaml.NewEncoder(outWriter) + // yaml.v2 defaults to indent=2, yaml.v3 defaults to indent=4, + // so be compatible with everything else in k8s and choose 2. + enc.SetIndent(2) + + return enc.Encode(crd.Yaml) + }(); err != nil { + return err + } + } + } + + return nil +} + +// partialCRDSet represents a set of CRDs of different apiext versions +// (v1beta1.CRD vs v1.CRD) that represent the same GroupKind. +// +// It tracks modifications to the schemata of those CRDs from this source file, +// plus some useful structured content, and keeps track of the raw YAML representation +// of the different apiext versions. +type partialCRDSet struct { + // GroupKind is the GroupKind represented by this CRD. + GroupKind schema.GroupKind + // NewSchemata are the new schemata generated from Go IDL by controller-gen. + NewSchemata map[string]apiext.JSONSchemaProps + // CRDVersions are the forms of this CRD across different apiextensions + // versions + CRDVersions []*partialCRD + // Versions are the versions of the given GroupKind in this set of CRDs. + Versions map[string]struct{} +} + +// partialCRD represents the raw YAML encoding of a given CRD instance, plus +// the versions contained therein for easy lookup. +type partialCRD struct { + // Yaml is the raw YAML structure of the CRD. + Yaml *yaml.Node + // FileName is the source name of the file that this was read from. + // + // This isn't on partialCRDSet because we could have different CRD versions + // stored in the same file (like controller-tools does by default) or in + // different files. + FileName string + + // CRDVersion is the version of the CRD object itself, from + // apiextensions (currently apiextensions/v1 or apiextensions/v1beta1). + CRDVersion string +} + +// setGlobalSchema sets the versioned schemas (as per setVersionedSchemata). +func (e *partialCRDSet) setGlobalSchema() error { + for _, crdInfo := range e.CRDVersions { + if err := crdInfo.setVersionedSchemata(e.NewSchemata); err != nil { + return err + } + } + return nil +} + +// getVersionsNode gets the YAML node of .spec.versions YAML mapping, +// if returning the node, and whether or not it was present. +func (e *partialCRD) getVersionsNode() (*yaml.Node, bool, error) { + versions, found, err := yamlop.GetNode(e.Yaml, "spec", "versions") + if err != nil { + return nil, false, err + } + if !found { + return nil, false, nil + } + if versions.Kind != yaml.SequenceNode { + return nil, true, fmt.Errorf("unexpected non-sequence versions") + } + return versions, found, nil +} + +// setVersionedSchemata sets the versioned schemata on each encoding in this set as per +// setVersionedSchemata on partialCRD. +func (e *partialCRDSet) setVersionedSchemata() error { + for _, crdInfo := range e.CRDVersions { + if err := crdInfo.setVersionedSchemata(e.NewSchemata); err != nil { + return err + } + } + return nil +} + +// setVersionedSchemata populates all existing versions with new schemata, +// wiping the schema of any version that doesn't have a listed schema. +// Any "unknown" versions are ignored. +func (e *partialCRD) setVersionedSchemata(newSchemata map[string]apiext.JSONSchemaProps) error { + var err error + if err := yamlop.DeleteNode(e.Yaml, "spec", "validation"); err != nil { + return err + } + + versions, found, err := e.getVersionsNode() + if err != nil { + return err + } + if !found { + return fmt.Errorf("unexpected missing versions") + } + + for i, verNode := range versions.Content { + nameNode, _, _ := yamlop.GetNode(verNode, "name") + if nameNode.Kind != yaml.ScalarNode || nameNode.ShortTag() != "!!str" { + return fmt.Errorf("version name was not a string at spec.versions[%d]", i) + } + name := nameNode.Value + if name == "" { + return fmt.Errorf("unexpected empty name at spec.versions[%d]", i) + } + newSchema, found := newSchemata[name] + if !found { + if err := yamlop.DeleteNode(verNode, "schema"); err != nil { + return fmt.Errorf("spec.versions[%d]: %w", i, err) + } + } else { + schemaNodeTree, err := yamlop.ToYAML(newSchema) + if err != nil { + return fmt.Errorf("failed to convert schema to YAML: %w", err) + } + schemaNodeTree = schemaNodeTree.Content[0] // get rid of the document node + yamlop.SetStyle(schemaNodeTree, 0) // clear the style so it defaults to an auto-chosen one + if err := yamlop.SetNode(verNode, *schemaNodeTree, "schema", "openAPIV3Schema"); err != nil { + return fmt.Errorf("spec.versions[%d]: %w", i, err) + } + } + } + return nil +} + +// crdsFromDirectory returns loads all CRDs from the given directory in a +// manner that preserves ordering, comments, etc in order to make patching +// minimally invasive. Returned CRDs are mapped by group-kind. +func crdsFromDirectory(ctx *genall.GenerationContext, dir string) (map[schema.GroupKind]*partialCRDSet, error) { + res := map[schema.GroupKind]*partialCRDSet{} + dirEntries, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + for _, fileInfo := range dirEntries { + // find all files that are YAML + if fileInfo.IsDir() || filepath.Ext(fileInfo.Name()) != ".yaml" { + continue + } + + rawContent, err := ctx.ReadFile(filepath.Join(dir, fileInfo.Name())) + if err != nil { + return nil, err + } + + // NB(directxman12): we could use the universal deserializer for this, but it's + // really pretty clunky, and the alternative is actually kinda easier to understand + + // ensure that this is a CRD + var typeMeta metav1.TypeMeta + if err := kyaml.Unmarshal(rawContent, &typeMeta); err != nil { + continue + } + + if typeMeta.APIVersion == "" || typeMeta.Kind != "CustomResourceDefinition" { + // If there's no API version this file probably isn't a CRD. + // Likewise we don't need to care if the Kind isn't CustomResourceDefinition. + continue + } + + if !isSupportedAPIExtGroupVer(typeMeta.APIVersion) { + return nil, fmt.Errorf("load %q: apiVersion %q not supported", filepath.Join(dir, fileInfo.Name()), typeMeta.APIVersion) + } + + // collect the group-kind and versions from the actual structured form + var actualCRD crdIsh + if err := kyaml.Unmarshal(rawContent, &actualCRD); err != nil { + continue + } + groupKind := schema.GroupKind{Group: actualCRD.Spec.Group, Kind: actualCRD.Spec.Names.Kind} + versions := make(map[string]struct{}, len(actualCRD.Spec.Versions)) + for _, ver := range actualCRD.Spec.Versions { + versions[ver.Name] = struct{}{} + } + + // then actually unmarshal in a manner that preserves ordering, etc + var yamlNodeTree yaml.Node + if err := yaml.Unmarshal(rawContent, &yamlNodeTree); err != nil { + continue + } + + // then store this CRDVersion of the CRD in a set, populating the set if necessary + if res[groupKind] == nil { + res[groupKind] = &partialCRDSet{ + GroupKind: groupKind, + NewSchemata: make(map[string]apiext.JSONSchemaProps), + Versions: make(map[string]struct{}), + } + } + for ver := range versions { + res[groupKind].Versions[ver] = struct{}{} + } + res[groupKind].CRDVersions = append(res[groupKind].CRDVersions, &partialCRD{ + Yaml: &yamlNodeTree, + FileName: fileInfo.Name(), + CRDVersion: typeMeta.APIVersion, + }) + } + return res, nil +} + +// isSupportedAPIExtGroupVer checks if the given string-form group-version +// is one of the known apiextensions versions (v1). +func isSupportedAPIExtGroupVer(groupVer string) bool { + return groupVer == currentAPIExtVersion +} + +// crdIsh is a merged blob of CRD fields that looks enough like all versions of +// CRD to extract the relevant information for partialCRDSet and partialCRD. +// +// We keep this separate so it's clear what info we need, and so we don't break +// when we switch canonical internal versions and lose old fields while gaining +// new ones (like in v1beta1 --> v1). +// +// Its use is tied directly to crdsFromDirectory, and is mostly an implementation detail of that. +type crdIsh struct { + Spec struct { + Group string `json:"group"` + Names struct { + Kind string `json:"kind"` + } `json:"names"` + Versions []struct { + Name string `json:"name"` + } `json:"versions"` + } `json:"spec"` +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/convert.go b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/convert.go new file mode 100644 index 0000000000..b0ac001580 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/convert.go @@ -0,0 +1,61 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yaml + +import ( + "encoding/json" + "fmt" + + "gopkg.in/yaml.v3" +) + +// ToYAML converts some object that serializes to JSON into a YAML node tree. +// It's useful since it pays attention to JSON tags, unlike yaml.Unmarshal or +// yaml.Node.Decode. +func ToYAML(rawObj interface{}) (*yaml.Node, error) { + if rawObj == nil { + return &yaml.Node{Kind: yaml.ScalarNode, Value: "null", Tag: "!!null"}, nil + } + + rawJSON, err := json.Marshal(rawObj) + if err != nil { + return nil, fmt.Errorf("failed to marshal object: %w", err) + } + + var out yaml.Node + if err := yaml.Unmarshal(rawJSON, &out); err != nil { + return nil, fmt.Errorf("unable to unmarshal marshalled object: %w", err) + } + return &out, nil +} + +// changeAll calls the given callback for all nodes in +// the given YAML node tree. +func changeAll(root *yaml.Node, cb func(*yaml.Node)) { + cb(root) + for _, child := range root.Content { + changeAll(child, cb) + } +} + +// SetStyle sets the style for all nodes in the given +// node tree to the given style. +func SetStyle(root *yaml.Node, style yaml.Style) { + changeAll(root, func(node *yaml.Node) { + node.Style = style + }) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/nested.go b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/nested.go new file mode 100644 index 0000000000..70d43dbeb3 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/nested.go @@ -0,0 +1,87 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yaml + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// ValueInMapping finds the value node with the corresponding string key +// in the given mapping node. If the given node is not a mapping, an +// error will be returned. +func ValueInMapping(root *yaml.Node, key string) (*yaml.Node, error) { + if root.Kind != yaml.MappingNode { + return nil, fmt.Errorf("unexpected non-mapping node") + } + + for i := 0; i < len(root.Content)/2; i++ { + keyNode := root.Content[i*2] + if keyNode.Value == key { + return root.Content[i*2+1], nil + } + } + return nil, nil +} + +// asCloseAsPossible goes as deep on the given path as possible, returning the +// last node that existed from the given path in the given tree of mapping +// nodes, as well as the rest of the path that could not be fetched, if any. +func asCloseAsPossible(root *yaml.Node, path ...string) (*yaml.Node, []string, error) { + if root == nil { + return nil, path, nil + } + if root.Kind == yaml.DocumentNode && len(root.Content) > 0 { + root = root.Content[0] + } + + currNode := root + for ; len(path) > 0; path = path[1:] { + if currNode.Kind != yaml.MappingNode { + return nil, nil, fmt.Errorf("unexpected non-mapping (%v) before path %v", currNode.Kind, path) + } + + nextNode, err := ValueInMapping(currNode, path[0]) + if err != nil { + return nil, nil, fmt.Errorf("unable to get next node in path %v: %w", path, err) + } + + if nextNode == nil { + // we're as close as possible + break + } + + currNode = nextNode + } + + return currNode, path, nil +} + +// GetNode gets the node at the given path in the given sequence of mapping +// nodes, or, if it doesn't exist, returning false. +func GetNode(root *yaml.Node, path ...string) (*yaml.Node, bool, error) { + resNode, restPath, err := asCloseAsPossible(root, path...) + if err != nil { + return nil, false, err + } + // more path means the node didn't exist + if len(restPath) != 0 { + return nil, false, nil + } + return resNode, true, nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/set.go b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/set.go new file mode 100644 index 0000000000..ede417f1cf --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/internal/yaml/set.go @@ -0,0 +1,80 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yaml + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// SetNode sets the given path to the given yaml Node, creating mapping nodes along the way. +func SetNode(root *yaml.Node, val yaml.Node, path ...string) error { + currNode, path, err := asCloseAsPossible(root, path...) + if err != nil { + return err + } + + if len(path) > 0 { + if currNode.Kind != yaml.MappingNode { + return fmt.Errorf("unexpected non-mapping before path %v", path) + } + + for ; len(path) > 0; path = path[1:] { + keyNode := yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Style: yaml.DoubleQuotedStyle, Value: path[0]} + nextNode := &yaml.Node{Kind: yaml.MappingNode} + currNode.Content = append(currNode.Content, &keyNode, nextNode) + + currNode = nextNode + } + } + + *currNode = val + return nil +} + +// DeleteNode deletes the node at the given path in the given tree of mapping nodes. +// It's a noop if the path doesn't exist. +func DeleteNode(root *yaml.Node, path ...string) error { + if len(path) == 0 { + return fmt.Errorf("must specify a path to delete") + } + pathToParent, keyToDelete := path[:len(path)-1], path[len(path)-1] + parentNode, path, err := asCloseAsPossible(root, pathToParent...) + if err != nil { + return err + } + if len(path) > 0 { + // no-op, parent node doesn't exist + return nil + } + + if parentNode.Kind != yaml.MappingNode { + return fmt.Errorf("unexpected non-mapping node") + } + + for i := 0; i < len(parentNode.Content)/2; i++ { + keyNode := parentNode.Content[i*2] + if keyNode.Value == keyToDelete { + parentNode.Content = append(parentNode.Content[:i*2], parentNode.Content[i*2+2:]...) + return nil + } + } + + // no-op, key not found in parent node + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/zz_generated.markerhelp.go new file mode 100644 index 0000000000..db9745d767 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/schemapatcher/zz_generated.markerhelp.go @@ -0,0 +1,50 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package schemapatcher + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "patches existing CRDs with new schemata. ", + Details: "It will generate output for each \"CRD Version\" (API version of the CRD type itself) , e.g. apiextensions/v1) available.", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "ManifestsPath": { + Summary: "contains the CustomResourceDefinition YAML files.", + Details: "", + }, + "MaxDescLen": { + Summary: "specifies the maximum description length for fields in CRD's OpenAPI schema. ", + Details: "0 indicates drop the description for all fields completely. n indicates limit the description to at most n characters and truncate the description to closest sentence boundary if it exceeds n characters.", + }, + "GenerateEmbeddedObjectMeta": { + Summary: "specifies if any embedded ObjectMeta in the CRD should be generated", + Details: "", + }, + }, + } +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/version/version.go b/vendor/sigs.k8s.io/controller-tools/pkg/version/version.go new file mode 100644 index 0000000000..09c8efcf40 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/version/version.go @@ -0,0 +1,49 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package version + +import ( + "fmt" + "runtime/debug" +) + +// Version returns the version of the main module +func Version() string { + info, ok := debug.ReadBuildInfo() + if !ok || info == nil || info.Main.Version == "" { + // binary has not been built with module support or doesn't contain a version. + return "(unknown)" + } + return info.Main.Version +} + +// Print prints the main module version on stdout. +// +// Print will display either: +// +// - "Version: v0.2.1" when the program has been compiled with: +// +// $ go get github.com/controller-tools/cmd/controller-gen@v0.2.1 +// +// Note: go modules requires the usage of semver compatible tags starting with +// 'v' to have nice human-readable versions. +// +// - "Version: (devel)" when the program is compiled from a local git checkout. +// +// - "Version: (unknown)" when not using go modules. +func Print() { + fmt.Printf("Version: %s\n", Version()) +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/webhook/parser.go b/vendor/sigs.k8s.io/controller-tools/pkg/webhook/parser.go new file mode 100644 index 0000000000..a76dcdcbb4 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/webhook/parser.go @@ -0,0 +1,431 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package webhook contains libraries for generating webhookconfig manifests +// from markers in Go source files. +// +// The markers take the form: +// +// +kubebuilder:webhook:webhookVersions=<[]string>,failurePolicy=,matchPolicy=,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=,path=,mutating=,sideEffects=,admissionReviewVersions=<[]string>,reinvocationPolicy= +package webhook + +import ( + "fmt" + "strings" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/markers" +) + +// The default {Mutating,Validating}WebhookConfiguration version to generate. +const ( + v1 = "v1" + defaultWebhookVersion = v1 +) + +var ( + // ConfigDefinition s a marker for defining Webhook manifests. + // Call ToWebhook on the value to get a Kubernetes Webhook. + ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{})) +) + +// supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration. +func supportedWebhookVersions() []string { + return []string{defaultWebhookVersion} +} + +// +controllertools:marker:generateHelp:category=Webhook + +// Config specifies how a webhook should be served. +// +// It specifies only the details that are intrinsic to the application serving +// it (e.g. the resources it can handle, or the path it serves on). +type Config struct { + // Mutating marks this as a mutating webhook (it's validating only if false) + // + // Mutating webhooks are allowed to change the object in their response, + // and are called *before* all validating webhooks. Mutating webhooks may + // choose to reject an object, similarly to a validating webhook. + Mutating bool + // FailurePolicy specifies what should happen if the API server cannot reach the webhook. + // + // It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject + // the object in question). + FailurePolicy string + // MatchPolicy defines how the "rules" list is used to match incoming requests. + // Allowed values are "Exact" (match only if it exactly matches the specified rule) + // or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version). + MatchPolicy string `marker:",optional"` + // SideEffects specify whether calling the webhook will have side effects. + // This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then + // the API server will not call the webhook on a dry-run request and fails instead. + // If the value is "None", then the webhook has no side effects and the API server will call it on dry-run. + // If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the + // AdmissionReview sent in the request, and avoiding side effects if that value is "true." + SideEffects string `marker:",optional"` + + // Groups specifies the API groups that this webhook receives requests for. + Groups []string + // Resources specifies the API resources that this webhook receives requests for. + Resources []string + // Verbs specifies the Kubernetes API verbs that this webhook receives requests for. + // + // Only modification-like verbs may be specified. + // May be "create", "update", "delete", "connect", or "*" (for all). + Verbs []string + // Versions specifies the API versions that this webhook receives requests for. + Versions []string + + // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots + Name string + + // Path specifies that path that the API server should connect to this webhook on. Must be + // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by + // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group + // are substituted for hyphens. For example, a validating webhook path for type + // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be + // /validate-batch-tutorial-kubebuilder-io-v1-cronjob + Path string + + // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects + // itself to generate. The only supported value is v1. Defaults to v1. + WebhookVersions []string `marker:"webhookVersions,optional"` + + // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` + // versions the Webhook expects. + AdmissionReviewVersions []string `marker:"admissionReviewVersions"` + + // ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations + // + // To allow mutating admission plugins to observe changes made by other plugins, + // built-in mutating admission plugins are re-run if a mutating webhook modifies + // an object, and mutating webhooks can specify a reinvocationPolicy to control + // whether they are reinvoked as well. + ReinvocationPolicy string `marker:"reinvocationPolicy,optional"` +} + +// verbToAPIVariant converts a marker's verb to the proper value for the API. +// Unrecognized verbs are passed through. +func verbToAPIVariant(verbRaw string) admissionregv1.OperationType { + switch strings.ToLower(verbRaw) { + case strings.ToLower(string(admissionregv1.Create)): + return admissionregv1.Create + case strings.ToLower(string(admissionregv1.Update)): + return admissionregv1.Update + case strings.ToLower(string(admissionregv1.Delete)): + return admissionregv1.Delete + case strings.ToLower(string(admissionregv1.Connect)): + return admissionregv1.Connect + case strings.ToLower(string(admissionregv1.OperationAll)): + return admissionregv1.OperationAll + default: + return admissionregv1.OperationType(verbRaw) + } +} + +// ToMutatingWebhook converts this rule to its Kubernetes API form. +func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { + if !c.Mutating { + return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name) + } + + matchPolicy, err := c.matchPolicy() + if err != nil { + return admissionregv1.MutatingWebhook{}, err + } + + return admissionregv1.MutatingWebhook{ + Name: c.Name, + Rules: c.rules(), + FailurePolicy: c.failurePolicy(), + MatchPolicy: matchPolicy, + ClientConfig: c.clientConfig(), + SideEffects: c.sideEffects(), + AdmissionReviewVersions: c.AdmissionReviewVersions, + ReinvocationPolicy: c.reinvocationPolicy(), + }, nil +} + +// ToValidatingWebhook converts this rule to its Kubernetes API form. +func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) { + if c.Mutating { + return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name) + } + + matchPolicy, err := c.matchPolicy() + if err != nil { + return admissionregv1.ValidatingWebhook{}, err + } + + return admissionregv1.ValidatingWebhook{ + Name: c.Name, + Rules: c.rules(), + FailurePolicy: c.failurePolicy(), + MatchPolicy: matchPolicy, + ClientConfig: c.clientConfig(), + SideEffects: c.sideEffects(), + AdmissionReviewVersions: c.AdmissionReviewVersions, + }, nil +} + +// rules returns the configuration of what operations on what +// resources/subresources a webhook should care about. +func (c Config) rules() []admissionregv1.RuleWithOperations { + whConfig := admissionregv1.RuleWithOperations{ + Rule: admissionregv1.Rule{ + APIGroups: c.Groups, + APIVersions: c.Versions, + Resources: c.Resources, + }, + Operations: make([]admissionregv1.OperationType, len(c.Verbs)), + } + + for i, verbRaw := range c.Verbs { + whConfig.Operations[i] = verbToAPIVariant(verbRaw) + } + + // fix the group names, since letting people type "core" is nice + for i, group := range whConfig.APIGroups { + if group == "core" { + whConfig.APIGroups[i] = "" + } + } + + return []admissionregv1.RuleWithOperations{whConfig} +} + +// failurePolicy converts the string value to the proper value for the API. +// Unrecognized values are passed through. +func (c Config) failurePolicy() *admissionregv1.FailurePolicyType { + var failurePolicy admissionregv1.FailurePolicyType + switch strings.ToLower(c.FailurePolicy) { + case strings.ToLower(string(admissionregv1.Ignore)): + failurePolicy = admissionregv1.Ignore + case strings.ToLower(string(admissionregv1.Fail)): + failurePolicy = admissionregv1.Fail + default: + failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy) + } + return &failurePolicy +} + +// matchPolicy converts the string value to the proper value for the API. +func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) { + var matchPolicy admissionregv1.MatchPolicyType + switch strings.ToLower(c.MatchPolicy) { + case strings.ToLower(string(admissionregv1.Exact)): + matchPolicy = admissionregv1.Exact + case strings.ToLower(string(admissionregv1.Equivalent)): + matchPolicy = admissionregv1.Equivalent + case "": + return nil, nil + default: + return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy) + } + return &matchPolicy, nil +} + +// clientConfig returns the client config for a webhook. +func (c Config) clientConfig() admissionregv1.WebhookClientConfig { + path := c.Path + return admissionregv1.WebhookClientConfig{ + Service: &admissionregv1.ServiceReference{ + Name: "webhook-service", + Namespace: "system", + Path: &path, + }, + } +} + +// sideEffects returns the sideEffects config for a webhook. +func (c Config) sideEffects() *admissionregv1.SideEffectClass { + var sideEffects admissionregv1.SideEffectClass + switch strings.ToLower(c.SideEffects) { + case strings.ToLower(string(admissionregv1.SideEffectClassNone)): + sideEffects = admissionregv1.SideEffectClassNone + case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)): + sideEffects = admissionregv1.SideEffectClassNoneOnDryRun + case strings.ToLower(string(admissionregv1.SideEffectClassSome)): + sideEffects = admissionregv1.SideEffectClassSome + case "": + return nil + default: + return nil + } + return &sideEffects +} + +// reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook. +func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType { + var reinvocationPolicy admissionregv1.ReinvocationPolicyType + switch strings.ToLower(c.ReinvocationPolicy) { + case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)): + reinvocationPolicy = admissionregv1.NeverReinvocationPolicy + case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)): + reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy + default: + return nil + } + return &reinvocationPolicy +} + +// webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. +func (c Config) webhookVersions() ([]string, error) { + // If WebhookVersions is not specified, we default it to `v1`. + if len(c.WebhookVersions) == 0 { + return []string{defaultWebhookVersion}, nil + } + supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...) + for _, version := range c.WebhookVersions { + if !supportedWebhookVersions.Has(version) { + return nil, fmt.Errorf("unsupported webhook version: %s", version) + } + } + return sets.NewString(c.WebhookVersions...).UnsortedList(), nil +} + +// +controllertools:marker:generateHelp + +// Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. +type Generator struct{} + +func (Generator) RegisterMarkers(into *markers.Registry) error { + if err := into.Register(ConfigDefinition); err != nil { + return err + } + into.AddHelp(ConfigDefinition, Config{}.Help()) + return nil +} + +func (Generator) Generate(ctx *genall.GenerationContext) error { + supportedWebhookVersions := supportedWebhookVersions() + mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions)) + validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions)) + for _, root := range ctx.Roots { + markerSet, err := markers.PackageMarkers(ctx.Collector, root) + if err != nil { + root.AddError(err) + } + + for _, cfg := range markerSet[ConfigDefinition.Name] { + cfg := cfg.(Config) + webhookVersions, err := cfg.webhookVersions() + if err != nil { + return err + } + if cfg.Mutating { + w, err := cfg.ToMutatingWebhook() + if err != nil { + return err + } + for _, webhookVersion := range webhookVersions { + mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w) + } + } else { + w, err := cfg.ToValidatingWebhook() + if err != nil { + return err + } + for _, webhookVersion := range webhookVersions { + validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w) + } + } + } + } + + versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions)) + for _, version := range supportedWebhookVersions { + if cfgs, ok := mutatingCfgs[version]; ok { + // The only possible version in supportedWebhookVersions is v1, + // so use it for all versioned types in this context. + objRaw := &admissionregv1.MutatingWebhookConfiguration{} + objRaw.SetGroupVersionKind(schema.GroupVersionKind{ + Group: admissionregv1.SchemeGroupVersion.Group, + Version: version, + Kind: "MutatingWebhookConfiguration", + }) + objRaw.SetName("mutating-webhook-configuration") + objRaw.Webhooks = cfgs + for i := range objRaw.Webhooks { + // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, + // return an error + if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { + return err + } + // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, + // return an error + if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { + return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") + } + } + versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) + } + + if cfgs, ok := validatingCfgs[version]; ok { + // The only possible version in supportedWebhookVersions is v1, + // so use it for all versioned types in this context. + objRaw := &admissionregv1.ValidatingWebhookConfiguration{} + objRaw.SetGroupVersionKind(schema.GroupVersionKind{ + Group: admissionregv1.SchemeGroupVersion.Group, + Version: version, + Kind: "ValidatingWebhookConfiguration", + }) + objRaw.SetName("validating-webhook-configuration") + objRaw.Webhooks = cfgs + for i := range objRaw.Webhooks { + // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, + // return an error + if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { + return err + } + // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, + // return an error + if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { + return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") + } + } + versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) + } + } + + for k, v := range versionedWebhooks { + var fileName string + if k == defaultWebhookVersion { + fileName = fmt.Sprintf("manifests.yaml") + } else { + fileName = fmt.Sprintf("manifests.%s.yaml", k) + } + if err := ctx.WriteYAML(fileName, v); err != nil { + return err + } + } + return nil +} + +func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error { + if sideEffects == nil { + return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration") + } + if *sideEffects == admissionregv1.SideEffectClassUnknown || + *sideEffects == admissionregv1.SideEffectClassSome { + return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration") + } + return nil +} diff --git a/vendor/sigs.k8s.io/controller-tools/pkg/webhook/zz_generated.markerhelp.go b/vendor/sigs.k8s.io/controller-tools/pkg/webhook/zz_generated.markerhelp.go new file mode 100644 index 0000000000..8e1ef419d9 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-tools/pkg/webhook/zz_generated.markerhelp.go @@ -0,0 +1,110 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by helpgen. DO NOT EDIT. + +package webhook + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Config) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "Webhook", + DetailedHelp: markers.DetailedHelp{ + Summary: "specifies how a webhook should be served. ", + Details: "It specifies only the details that are intrinsic to the application serving it (e.g. the resources it can handle, or the path it serves on).", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "Mutating": { + Summary: "marks this as a mutating webhook (it's validating only if false) ", + Details: "Mutating webhooks are allowed to change the object in their response, and are called *before* all validating webhooks. Mutating webhooks may choose to reject an object, similarly to a validating webhook.", + }, + "FailurePolicy": { + Summary: "specifies what should happen if the API server cannot reach the webhook. ", + Details: "It may be either \"ignore\" (to skip the webhook and continue on) or \"fail\" (to reject the object in question).", + }, + "MatchPolicy": { + Summary: "defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" (match only if it exactly matches the specified rule) or \"Equivalent\" (match a request if it modifies a resource listed in rules, even via another API group or version).", + Details: "", + }, + "SideEffects": { + Summary: "specify whether calling the webhook will have side effects. This has an impact on dry runs and `kubectl diff`: if the sideEffect is \"Unknown\" (the default) or \"Some\", then the API server will not call the webhook on a dry-run request and fails instead. If the value is \"None\", then the webhook has no side effects and the API server will call it on dry-run. If the value is \"NoneOnDryRun\", then the webhook is responsible for inspecting the \"dryRun\" property of the AdmissionReview sent in the request, and avoiding side effects if that value is \"true.\"", + Details: "", + }, + "Groups": { + Summary: "specifies the API groups that this webhook receives requests for.", + Details: "", + }, + "Resources": { + Summary: "specifies the API resources that this webhook receives requests for.", + Details: "", + }, + "Verbs": { + Summary: "specifies the Kubernetes API verbs that this webhook receives requests for. ", + Details: "Only modification-like verbs may be specified. May be \"create\", \"update\", \"delete\", \"connect\", or \"*\" (for all).", + }, + "Versions": { + Summary: "specifies the API versions that this webhook receives requests for.", + Details: "", + }, + "Name": { + Summary: "indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots", + Details: "", + }, + "Path": { + Summary: "specifies that path that the API server should connect to this webhook on. Must be prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group are substituted for hyphens. For example, a validating webhook path for type batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be /validate-batch-tutorial-kubebuilder-io-v1-cronjob", + Details: "", + }, + "WebhookVersions": { + Summary: "specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects itself to generate. The only supported value is v1. Defaults to v1.", + Details: "", + }, + "AdmissionReviewVersions": { + Summary: "is an ordered list of preferred `AdmissionReview` versions the Webhook expects.", + Details: "", + }, + "ReinvocationPolicy": { + Summary: "allows mutating webhooks to request reinvocation after other mutations ", + Details: "To allow mutating admission plugins to observe changes made by other plugins, built-in mutating admission plugins are re-run if a mutating webhook modifies an object, and mutating webhooks can specify a reinvocationPolicy to control whether they are reinvoked as well.", + }, + }, + } +} + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "generates (partial) {Mutating,Validating}WebhookConfiguration objects.", + Details: "", + }, + FieldHelp: map[string]markers.DetailedHelp{ + "HeaderFile": { + Summary: "specifies the header text (e.g. license) to prepend to generated files.", + Details: "", + }, + "Year": { + Summary: "specifies the year to substitute for \" YEAR\" in the header file.", + Details: "", + }, + }, + } +} From 47367f7f7cb1a88ab35cf2350ebd8497b9c6ffa0 Mon Sep 17 00:00:00 2001 From: Kashish Chaudhary Date: Mon, 12 Aug 2024 09:34:02 +0000 Subject: [PATCH 2/4] fixed gofmt --- cmd/operator/operator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/operator/operator.go b/cmd/operator/operator.go index 410682c7e6..1fb1b40401 100644 --- a/cmd/operator/operator.go +++ b/cmd/operator/operator.go @@ -152,7 +152,7 @@ func run(c *cli.Context) { }() } - diagControllerEnabled := c.Bool(flagEnableDiagController) + diagControllerEnabled := c.Bool(flagEnableDiagController) config, err := rest.InClusterConfig() if err != nil { @@ -292,7 +292,7 @@ func run(c *cli.Context) { log.Fatalf("Error starting watch on portworx diag controller: %v", err) } } - + if c.BoolT(flagMigration) { log.Info("Migration is enabled") migrationHandler := migration.New(&storageClusterController) From 115a2795c55e850bc760900df2181ad01cb8a4dc Mon Sep 17 00:00:00 2001 From: Kashish Chaudhary Date: Tue, 13 Aug 2024 05:42:36 +0000 Subject: [PATCH 3/4] fixup for test cases --- pkg/mock/sdkserver.go | 3 +++ pkg/util/test/util.go | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/mock/sdkserver.go b/pkg/mock/sdkserver.go index 5092ada825..fa7f4921c1 100644 --- a/pkg/mock/sdkserver.go +++ b/pkg/mock/sdkserver.go @@ -56,6 +56,9 @@ func (m *SdkServer) StartOnAddress(ip, port string) error { if m.servers.Node != nil { api.RegisterOpenStorageNodeServer(m.server, m.servers.Node) } + if m.servers.Volume != nil { + api.RegisterOpenStorageVolumeServer(m.server, m.servers.Volume) + } if m.servers.Role != nil { api.RegisterOpenStorageRoleServer(m.server, m.servers.Role) } diff --git a/pkg/util/test/util.go b/pkg/util/test/util.go index 8794098c42..fbba957dd9 100644 --- a/pkg/util/test/util.go +++ b/pkg/util/test/util.go @@ -33,6 +33,7 @@ import ( operatorops "github.com/portworx/sched-ops/k8s/operator" prometheusops "github.com/portworx/sched-ops/k8s/prometheus" rbacops "github.com/portworx/sched-ops/k8s/rbac" + "github.com/libopenstorage/operator/pkg/apis" "github.com/portworx/sched-ops/task" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/sirupsen/logrus" @@ -116,7 +117,7 @@ func NoopKubevirtManager(mockCtrl *gomock.Controller) *mock.MockKubevirtManager // adds the CRDs defined in this repository to the scheme func FakeK8sClient(initObjects ...runtime.Object) client.Client { s := scheme.Scheme - if err := corev1.AddToScheme(s); err != nil { + if err := apis.AddToScheme(s); err != nil { logrus.Error(err) } if err := monitoringv1.AddToScheme(s); err != nil { From 4477b5f1b1cbceba2fb07d8daad541f0bfaa1c96 Mon Sep 17 00:00:00 2001 From: Kashish Chaudhary Date: Tue, 13 Aug 2024 05:52:31 +0000 Subject: [PATCH 4/4] fixup! --- pkg/util/test/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/test/util.go b/pkg/util/test/util.go index fbba957dd9..8f39824fa6 100644 --- a/pkg/util/test/util.go +++ b/pkg/util/test/util.go @@ -26,6 +26,7 @@ import ( "github.com/golang/mock/gomock" "github.com/libopenstorage/openstorage/api" + "github.com/libopenstorage/operator/pkg/apis" ocp_configv1 "github.com/openshift/api/config/v1" appops "github.com/portworx/sched-ops/k8s/apps" coreops "github.com/portworx/sched-ops/k8s/core" @@ -33,7 +34,6 @@ import ( operatorops "github.com/portworx/sched-ops/k8s/operator" prometheusops "github.com/portworx/sched-ops/k8s/prometheus" rbacops "github.com/portworx/sched-ops/k8s/rbac" - "github.com/libopenstorage/operator/pkg/apis" "github.com/portworx/sched-ops/task" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/sirupsen/logrus"