| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package framework |
| |
| import ( |
| "context" |
| "crypto/rand" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "regexp" |
| "sort" |
| "strings" |
| "sync" |
| "time" |
| |
| "github.com/hashicorp/go-kms-wrapping/entropy/v2" |
| |
| jsonpatch "github.com/evanphx/json-patch/v5" |
| "github.com/hashicorp/errwrap" |
| log "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/go-secure-stdlib/parseutil" |
| "github.com/hashicorp/vault/sdk/helper/consts" |
| "github.com/hashicorp/vault/sdk/helper/errutil" |
| "github.com/hashicorp/vault/sdk/helper/license" |
| "github.com/hashicorp/vault/sdk/helper/logging" |
| "github.com/hashicorp/vault/sdk/logical" |
| ) |
| |
| // Backend is an implementation of logical.Backend that allows |
| // the implementer to code a backend using a much more programmer-friendly |
| // framework that handles a lot of the routing and validation for you. |
| // |
| // This is recommended over implementing logical.Backend directly. |
| type Backend struct { |
| // Help is the help text that is shown when a help request is made |
| // on the root of this resource. The root help is special since we |
| // show all the paths that can be requested. |
| Help string |
| |
| // Paths are the various routes that the backend responds to. |
| // This cannot be modified after construction (i.e. dynamically changing |
| // paths, including adding or removing, is not allowed once the |
| // backend is in use). |
| // |
| // PathsSpecial is the list of path patterns that denote the paths above |
| // that require special privileges. |
| Paths []*Path |
| PathsSpecial *logical.Paths |
| |
| // Secrets is the list of secret types that this backend can |
| // return. It is used to automatically generate proper responses, |
| // and ease specifying callbacks for revocation, renewal, etc. |
| Secrets []*Secret |
| |
| // InitializeFunc is the callback, which if set, will be invoked via |
| // Initialize() just after a plugin has been mounted. |
| InitializeFunc InitializeFunc |
| |
| // PeriodicFunc is the callback, which if set, will be invoked when the |
| // periodic timer of RollbackManager ticks. This can be used by |
| // backends to do anything it wishes to do periodically. |
| // |
| // PeriodicFunc can be invoked to, say periodically delete stale |
| // entries in backend's storage, while the backend is still being used. |
| // (Note the difference between this action and `Clean`, which is |
| // invoked just before the backend is unmounted). |
| PeriodicFunc periodicFunc |
| |
| // WALRollback is called when a WAL entry (see wal.go) has to be rolled |
| // back. It is called with the data from the entry. |
| // |
| // WALRollbackMinAge is the minimum age of a WAL entry before it is attempted |
| // to be rolled back. This should be longer than the maximum time it takes |
| // to successfully create a secret. |
| WALRollback WALRollbackFunc |
| WALRollbackMinAge time.Duration |
| |
| // Clean is called on unload to clean up e.g any existing connections |
| // to the backend, if required. |
| Clean CleanupFunc |
| |
| // Invalidate is called when a key is modified, if required. |
| Invalidate InvalidateFunc |
| |
| // AuthRenew is the callback to call when a RenewRequest for an |
| // authentication comes in. By default, renewal won't be allowed. |
| // See the built-in AuthRenew helpers in lease.go for common callbacks. |
| AuthRenew OperationFunc |
| |
| // BackendType is the logical.BackendType for the backend implementation |
| BackendType logical.BackendType |
| |
| // RunningVersion is the optional version that will be self-reported |
| RunningVersion string |
| |
| logger log.Logger |
| system logical.SystemView |
| events logical.EventSender |
| once sync.Once |
| pathsRe []*regexp.Regexp |
| } |
| |
| // periodicFunc is the callback called when the RollbackManager's timer ticks. |
| // This can be utilized by the backends to do anything it wants. |
| type periodicFunc func(context.Context, *logical.Request) error |
| |
| // OperationFunc is the callback called for an operation on a path. |
| type OperationFunc func(context.Context, *logical.Request, *FieldData) (*logical.Response, error) |
| |
| // ExistenceFunc is the callback called for an existence check on a path. |
| type ExistenceFunc func(context.Context, *logical.Request, *FieldData) (bool, error) |
| |
| // WALRollbackFunc is the callback for rollbacks. |
| type WALRollbackFunc func(context.Context, *logical.Request, string, interface{}) error |
| |
| // CleanupFunc is the callback for backend unload. |
| type CleanupFunc func(context.Context) |
| |
| // InvalidateFunc is the callback for backend key invalidation. |
| type InvalidateFunc func(context.Context, string) |
| |
| // InitializeFunc is the callback, which if set, will be invoked via |
| // Initialize() just after a plugin has been mounted. |
| type InitializeFunc func(context.Context, *logical.InitializationRequest) error |
| |
| // PatchPreprocessorFunc is used by HandlePatchOperation in order to shape |
| // the input as defined by request handler prior to JSON marshaling |
| type PatchPreprocessorFunc func(map[string]interface{}) (map[string]interface{}, error) |
| |
| // ErrNoEvents is returned when attempting to send an event, but when the event |
| // sender was not passed in during `backend.Setup()`. |
| var ErrNoEvents = errors.New("no event sender configured") |
| |
| // Initialize is the logical.Backend implementation. |
| func (b *Backend) Initialize(ctx context.Context, req *logical.InitializationRequest) error { |
| if b.InitializeFunc != nil { |
| return b.InitializeFunc(ctx, req) |
| } |
| return nil |
| } |
| |
| // HandleExistenceCheck is the logical.Backend implementation. |
| func (b *Backend) HandleExistenceCheck(ctx context.Context, req *logical.Request) (checkFound bool, exists bool, err error) { |
| b.once.Do(b.init) |
| |
| // Ensure we are only doing this when one of the correct operations is in play |
| switch req.Operation { |
| case logical.CreateOperation: |
| case logical.UpdateOperation: |
| default: |
| return false, false, fmt.Errorf("incorrect operation type %v for an existence check", req.Operation) |
| } |
| |
| // Find the matching route |
| path, captures := b.route(req.Path) |
| if path == nil { |
| return false, false, logical.ErrUnsupportedPath |
| } |
| |
| if path.ExistenceCheck == nil { |
| return false, false, nil |
| } |
| |
| checkFound = true |
| |
| // Build up the data for the route, with the URL taking priority |
| // for the fields over the PUT data. |
| raw := make(map[string]interface{}, len(path.Fields)) |
| for k, v := range req.Data { |
| raw[k] = v |
| } |
| for k, v := range captures { |
| raw[k] = v |
| } |
| |
| fd := FieldData{ |
| Raw: raw, |
| Schema: path.Fields, |
| } |
| |
| err = fd.Validate() |
| if err != nil { |
| return false, false, errutil.UserError{Err: err.Error()} |
| } |
| |
| // Call the callback with the request and the data |
| exists, err = path.ExistenceCheck(ctx, req, &fd) |
| return |
| } |
| |
| // HandleRequest is the logical.Backend implementation. |
| func (b *Backend) HandleRequest(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| b.once.Do(b.init) |
| |
| // Check for special cased global operations. These don't route |
| // to a specific Path. |
| switch req.Operation { |
| case logical.RenewOperation: |
| fallthrough |
| case logical.RevokeOperation: |
| return b.handleRevokeRenew(ctx, req) |
| case logical.RollbackOperation: |
| return b.handleRollback(ctx, req) |
| } |
| |
| // If the path is empty and it is a help operation, handle that. |
| if req.Path == "" && req.Operation == logical.HelpOperation { |
| return b.handleRootHelp(req) |
| } |
| |
| // Find the matching route |
| path, captures := b.route(req.Path) |
| if path == nil { |
| return nil, logical.ErrUnsupportedPath |
| } |
| |
| // Check if a feature is required and if the license has that feature |
| if path.FeatureRequired != license.FeatureNone { |
| hasFeature := b.system.HasFeature(path.FeatureRequired) |
| if !hasFeature { |
| return nil, logical.CodedError(401, "Feature Not Enabled") |
| } |
| } |
| |
| // Build up the data for the route, with the URL taking priority |
| // for the fields over the PUT data. |
| raw := make(map[string]interface{}, len(path.Fields)) |
| var ignored []string |
| for k, v := range req.Data { |
| raw[k] = v |
| if !path.TakesArbitraryInput && path.Fields[k] == nil { |
| ignored = append(ignored, k) |
| } |
| } |
| |
| var replaced []string |
| for k, v := range captures { |
| if raw[k] != nil { |
| replaced = append(replaced, k) |
| } |
| raw[k] = v |
| } |
| |
| // Look up the callback for this operation, preferring the |
| // path.Operations definition if present. |
| var callback OperationFunc |
| |
| if path.Operations != nil { |
| if op, ok := path.Operations[req.Operation]; ok { |
| |
| // Check whether this operation should be forwarded |
| if sysView := b.System(); sysView != nil { |
| replState := sysView.ReplicationState() |
| props := op.Properties() |
| |
| if props.ForwardPerformanceStandby && replState.HasState(consts.ReplicationPerformanceStandby) { |
| return nil, logical.ErrReadOnly |
| } |
| |
| if props.ForwardPerformanceSecondary && !sysView.LocalMount() && replState.HasState(consts.ReplicationPerformanceSecondary) { |
| return nil, logical.ErrReadOnly |
| } |
| } |
| |
| callback = op.Handler() |
| } |
| } else { |
| callback = path.Callbacks[req.Operation] |
| } |
| ok := callback != nil |
| |
| if !ok { |
| if req.Operation == logical.HelpOperation { |
| callback = path.helpCallback(b) |
| ok = true |
| } |
| } |
| if !ok { |
| return nil, logical.ErrUnsupportedOperation |
| } |
| |
| fd := FieldData{ |
| Raw: raw, |
| Schema: path.Fields, |
| } |
| |
| if req.Operation != logical.HelpOperation { |
| err := fd.Validate() |
| if err != nil { |
| return logical.ErrorResponse(fmt.Sprintf("Field validation failed: %s", err.Error())), nil |
| } |
| } |
| |
| resp, err := callback(ctx, req, &fd) |
| if err != nil { |
| return resp, err |
| } |
| |
| switch resp { |
| case nil: |
| default: |
| // If fields supplied in the request are not present in the field schema |
| // of the path, add a warning to the response indicating that those |
| // parameters will be ignored. |
| sort.Strings(ignored) |
| |
| if len(ignored) != 0 { |
| resp.AddWarning(fmt.Sprintf("Endpoint ignored these unrecognized parameters: %v", ignored)) |
| } |
| // If fields supplied in the request is being overwritten by the values |
| // supplied in the API request path, add a warning to the response |
| // indicating that those parameters will be replaced. |
| if len(replaced) != 0 { |
| resp.AddWarning(fmt.Sprintf("Endpoint replaced the value of these parameters with the values captured from the endpoint's path: %v", replaced)) |
| } |
| } |
| |
| return resp, nil |
| } |
| |
| // HandlePatchOperation acts as an abstraction for performing JSON merge patch |
| // operations (see https://datatracker.ietf.org/doc/html/rfc7396) for HTTP |
| // PATCH requests. It is responsible for properly processing and marshalling |
| // the input and existing resource prior to performing the JSON merge operation |
| // using the MergePatch function from the json-patch library. The preprocessor |
| // is an arbitrary func that can be provided to further process the input. The |
| // MergePatch function accepts and returns byte arrays. Null values will unset |
| // fields defined within the input's FieldData (as if they were never specified) |
| // and remove user-specified keys that exist within a map field. |
| func HandlePatchOperation(input *FieldData, resource map[string]interface{}, preprocessor PatchPreprocessorFunc) ([]byte, error) { |
| var err error |
| |
| if resource == nil { |
| return nil, fmt.Errorf("resource does not exist") |
| } |
| |
| inputMap := map[string]interface{}{} |
| |
| for key := range input.Raw { |
| if _, ok := input.Schema[key]; !ok { |
| // Only accept fields in the schema |
| continue |
| } |
| |
| // Ensure data types are handled properly according to the FieldSchema |
| val, ok, err := input.GetOkErr(key) |
| if err != nil { |
| return nil, err |
| } |
| |
| if ok { |
| inputMap[key] = val |
| } |
| } |
| |
| if preprocessor != nil { |
| inputMap, err = preprocessor(inputMap) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| marshaledResource, err := json.Marshal(resource) |
| if err != nil { |
| return nil, err |
| } |
| |
| marshaledInput, err := json.Marshal(inputMap) |
| if err != nil { |
| return nil, err |
| } |
| |
| modified, err := jsonpatch.MergePatch(marshaledResource, marshaledInput) |
| if err != nil { |
| return nil, err |
| } |
| |
| return modified, nil |
| } |
| |
| // SpecialPaths is the logical.Backend implementation. |
| func (b *Backend) SpecialPaths() *logical.Paths { |
| return b.PathsSpecial |
| } |
| |
| // Cleanup is used to release resources and prepare to stop the backend |
| func (b *Backend) Cleanup(ctx context.Context) { |
| if b.Clean != nil { |
| b.Clean(ctx) |
| } |
| } |
| |
| // InvalidateKey is used to clear caches and reset internal state on key changes |
| func (b *Backend) InvalidateKey(ctx context.Context, key string) { |
| if b.Invalidate != nil { |
| b.Invalidate(ctx, key) |
| } |
| } |
| |
| // Setup is used to initialize the backend with the initial backend configuration |
| func (b *Backend) Setup(ctx context.Context, config *logical.BackendConfig) error { |
| b.logger = config.Logger |
| b.system = config.System |
| b.events = config.EventsSender |
| return nil |
| } |
| |
| // GetRandomReader returns an io.Reader to use for generating key material in |
| // backends. If the backend has access to an external entropy source it will |
| // return that, otherwise it returns crypto/rand.Reader. |
| func (b *Backend) GetRandomReader() io.Reader { |
| if sourcer, ok := b.System().(entropy.Sourcer); ok { |
| return entropy.NewReader(sourcer) |
| } |
| |
| return rand.Reader |
| } |
| |
| // Logger can be used to get the logger. If no logger has been set, |
| // the logs will be discarded. |
| func (b *Backend) Logger() log.Logger { |
| if b.logger != nil { |
| return b.logger |
| } |
| |
| return logging.NewVaultLoggerWithWriter(ioutil.Discard, log.NoLevel) |
| } |
| |
| // System returns the backend's system view. |
| func (b *Backend) System() logical.SystemView { |
| return b.system |
| } |
| |
| // Type returns the backend type |
| func (b *Backend) Type() logical.BackendType { |
| return b.BackendType |
| } |
| |
| // Version returns the plugin version information |
| func (b *Backend) PluginVersion() logical.PluginVersion { |
| return logical.PluginVersion{ |
| Version: b.RunningVersion, |
| } |
| } |
| |
| // Route looks up the path that would be used for a given path string. |
| func (b *Backend) Route(path string) *Path { |
| result, _ := b.route(path) |
| return result |
| } |
| |
| // Secret is used to look up the secret with the given type. |
| func (b *Backend) Secret(k string) *Secret { |
| for _, s := range b.Secrets { |
| if s.Type == k { |
| return s |
| } |
| } |
| |
| return nil |
| } |
| |
| func (b *Backend) init() { |
| b.pathsRe = make([]*regexp.Regexp, len(b.Paths)) |
| for i, p := range b.Paths { |
| if len(p.Pattern) == 0 { |
| panic(fmt.Sprintf("Routing pattern cannot be blank")) |
| } |
| // Automatically anchor the pattern |
| if p.Pattern[0] != '^' { |
| p.Pattern = "^" + p.Pattern |
| } |
| if p.Pattern[len(p.Pattern)-1] != '$' { |
| p.Pattern = p.Pattern + "$" |
| } |
| b.pathsRe[i] = regexp.MustCompile(p.Pattern) |
| } |
| } |
| |
| func (b *Backend) route(path string) (*Path, map[string]string) { |
| b.once.Do(b.init) |
| |
| for i, re := range b.pathsRe { |
| matches := re.FindStringSubmatch(path) |
| if matches == nil { |
| continue |
| } |
| |
| // We have a match, determine the mapping of the captures and |
| // store that for returning. |
| var captures map[string]string |
| path := b.Paths[i] |
| if captureNames := re.SubexpNames(); len(captureNames) > 1 { |
| captures = make(map[string]string, len(captureNames)) |
| for i, name := range captureNames { |
| if name != "" { |
| captures[name] = matches[i] |
| } |
| } |
| } |
| |
| return path, captures |
| } |
| |
| return nil, nil |
| } |
| |
| func (b *Backend) handleRootHelp(req *logical.Request) (*logical.Response, error) { |
| // Build a mapping of the paths and get the paths alphabetized to |
| // make the output prettier. |
| pathsMap := make(map[string]*Path) |
| paths := make([]string, 0, len(b.Paths)) |
| for i, p := range b.pathsRe { |
| paths = append(paths, p.String()) |
| pathsMap[p.String()] = b.Paths[i] |
| } |
| sort.Strings(paths) |
| |
| // Build the path data |
| pathData := make([]rootHelpTemplatePath, 0, len(paths)) |
| for _, route := range paths { |
| p := pathsMap[route] |
| pathData = append(pathData, rootHelpTemplatePath{ |
| Path: route, |
| Help: strings.TrimSpace(p.HelpSynopsis), |
| }) |
| } |
| |
| help, err := executeTemplate(rootHelpTemplate, &rootHelpTemplateData{ |
| Help: strings.TrimSpace(b.Help), |
| Paths: pathData, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Plugins currently don't have a direct knowledge of their own "type" |
| // (e.g. "kv", "cubbyhole"). It defaults to the name of the executable but |
| // can be overridden when the plugin is mounted. Since we need this type to |
| // form the request & response full names, we are passing it as an optional |
| // request parameter to the plugin's root help endpoint. If specified in |
| // the request, the type will be used as part of the request/response body |
| // names in the OAS document. |
| requestResponsePrefix := req.GetString("requestResponsePrefix") |
| |
| // Build OpenAPI response for the entire backend |
| vaultVersion := "unknown" |
| if b.System() != nil { |
| env, err := b.System().PluginEnv(context.Background()) |
| if err != nil { |
| return nil, err |
| } |
| vaultVersion = env.VaultVersion |
| } |
| |
| doc := NewOASDocument(vaultVersion) |
| if err := documentPaths(b, requestResponsePrefix, doc); err != nil { |
| b.Logger().Warn("error generating OpenAPI", "error", err) |
| } |
| |
| return logical.HelpResponse(help, nil, doc), nil |
| } |
| |
| func (b *Backend) handleRevokeRenew(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| // Special case renewal of authentication for credential backends |
| if req.Operation == logical.RenewOperation && req.Auth != nil { |
| return b.handleAuthRenew(ctx, req) |
| } |
| |
| if req.Secret == nil { |
| return nil, fmt.Errorf("request has no secret") |
| } |
| |
| rawSecretType, ok := req.Secret.InternalData["secret_type"] |
| if !ok { |
| return nil, fmt.Errorf("secret is unsupported by this backend") |
| } |
| secretType, ok := rawSecretType.(string) |
| if !ok { |
| return nil, fmt.Errorf("secret is unsupported by this backend") |
| } |
| |
| secret := b.Secret(secretType) |
| if secret == nil { |
| return nil, fmt.Errorf("secret is unsupported by this backend") |
| } |
| |
| switch req.Operation { |
| case logical.RenewOperation: |
| return secret.HandleRenew(ctx, req) |
| case logical.RevokeOperation: |
| return secret.HandleRevoke(ctx, req) |
| default: |
| return nil, fmt.Errorf("invalid operation for revoke/renew: %q", req.Operation) |
| } |
| } |
| |
| // handleRollback invokes the PeriodicFunc set on the backend. It also does a |
| // WAL rollback operation. |
| func (b *Backend) handleRollback(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| // Response is not expected from the periodic operation. |
| var resp *logical.Response |
| |
| merr := new(multierror.Error) |
| if b.PeriodicFunc != nil { |
| if err := b.PeriodicFunc(ctx, req); err != nil { |
| merr = multierror.Append(merr, err) |
| } |
| } |
| |
| if b.WALRollback != nil { |
| var err error |
| resp, err = b.handleWALRollback(ctx, req) |
| if err != nil { |
| merr = multierror.Append(merr, err) |
| } |
| } |
| return resp, merr.ErrorOrNil() |
| } |
| |
| func (b *Backend) handleAuthRenew(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| if b.AuthRenew == nil { |
| return logical.ErrorResponse("this auth type doesn't support renew"), nil |
| } |
| |
| return b.AuthRenew(ctx, req, nil) |
| } |
| |
| func (b *Backend) handleWALRollback(ctx context.Context, req *logical.Request) (*logical.Response, error) { |
| if b.WALRollback == nil { |
| return nil, logical.ErrUnsupportedOperation |
| } |
| |
| var merr error |
| keys, err := ListWAL(ctx, req.Storage) |
| if err != nil { |
| return logical.ErrorResponse(err.Error()), nil |
| } |
| if len(keys) == 0 { |
| return nil, nil |
| } |
| |
| // Calculate the minimum time that the WAL entries could be |
| // created in order to be rolled back. |
| age := b.WALRollbackMinAge |
| if age == 0 { |
| age = 10 * time.Minute |
| } |
| minAge := time.Now().Add(-1 * age) |
| if _, ok := req.Data["immediate"]; ok { |
| minAge = time.Now().Add(1000 * time.Hour) |
| } |
| |
| for _, k := range keys { |
| entry, err := GetWAL(ctx, req.Storage, k) |
| if err != nil { |
| merr = multierror.Append(merr, err) |
| continue |
| } |
| if entry == nil { |
| continue |
| } |
| |
| // If the entry isn't old enough, then don't roll it back |
| if !time.Unix(entry.CreatedAt, 0).Before(minAge) { |
| continue |
| } |
| |
| // Attempt a WAL rollback |
| err = b.WALRollback(ctx, req, entry.Kind, entry.Data) |
| if err != nil { |
| err = errwrap.Wrapf(fmt.Sprintf("error rolling back %q entry: {{err}}", entry.Kind), err) |
| } |
| if err == nil { |
| err = DeleteWAL(ctx, req.Storage, k) |
| } |
| if err != nil { |
| merr = multierror.Append(merr, err) |
| } |
| } |
| |
| if merr == nil { |
| return nil, nil |
| } |
| |
| return logical.ErrorResponse(merr.Error()), nil |
| } |
| |
| func (b *Backend) SendEvent(ctx context.Context, eventType logical.EventType, event *logical.EventData) error { |
| if b.events == nil { |
| return ErrNoEvents |
| } |
| return b.events.Send(ctx, eventType, event) |
| } |
| |
| // FieldSchema is a basic schema to describe the format of a path field. |
| type FieldSchema struct { |
| Type FieldType |
| Default interface{} |
| Description string |
| |
| // The Required and Deprecated members are only used by openapi, and are not actually |
| // used by the framework. |
| Required bool |
| Deprecated bool |
| |
| // Query indicates this field will be sent as a query parameter: |
| // |
| // /v1/foo/bar?some_param=some_value |
| // |
| // It doesn't affect handling of the value, but may be used for documentation. |
| Query bool |
| |
| // AllowedValues is an optional list of permitted values for this field. |
| // This constraint is not (yet) enforced by the framework, but the list is |
| // output as part of OpenAPI generation and may effect documentation and |
| // dynamic UI generation. |
| AllowedValues []interface{} |
| |
| // DisplayAttrs provides hints for UI and documentation generators. They |
| // will be included in OpenAPI output if set. |
| DisplayAttrs *DisplayAttributes |
| } |
| |
| // DefaultOrZero returns the default value if it is set, or otherwise |
| // the zero value of the type. |
| func (s *FieldSchema) DefaultOrZero() interface{} { |
| if s.Default != nil { |
| switch s.Type { |
| case TypeDurationSecond, TypeSignedDurationSecond: |
| resultDur, err := parseutil.ParseDurationSecond(s.Default) |
| if err != nil { |
| return s.Type.Zero() |
| } |
| return int(resultDur.Seconds()) |
| |
| default: |
| return s.Default |
| } |
| } |
| |
| return s.Type.Zero() |
| } |
| |
| // Zero returns the correct zero-value for a specific FieldType |
| func (t FieldType) Zero() interface{} { |
| switch t { |
| case TypeString, TypeNameString, TypeLowerCaseString: |
| return "" |
| case TypeInt: |
| return 0 |
| case TypeInt64: |
| return int64(0) |
| case TypeBool: |
| return false |
| case TypeMap: |
| return map[string]interface{}{} |
| case TypeKVPairs: |
| return map[string]string{} |
| case TypeDurationSecond, TypeSignedDurationSecond: |
| return 0 |
| case TypeSlice: |
| return []interface{}{} |
| case TypeStringSlice, TypeCommaStringSlice: |
| return []string{} |
| case TypeCommaIntSlice: |
| return []int{} |
| case TypeHeader: |
| return http.Header{} |
| case TypeFloat: |
| return 0.0 |
| case TypeTime: |
| return time.Time{} |
| default: |
| panic("unknown type: " + t.String()) |
| } |
| } |
| |
| type rootHelpTemplateData struct { |
| Help string |
| Paths []rootHelpTemplatePath |
| } |
| |
| type rootHelpTemplatePath struct { |
| Path string |
| Help string |
| } |
| |
| const rootHelpTemplate = ` |
| ## DESCRIPTION |
| |
| {{.Help}} |
| |
| ## PATHS |
| |
| The following paths are supported by this backend. To view help for |
| any of the paths below, use the help command with any route matching |
| the path pattern. Note that depending on the policy of your auth token, |
| you may or may not be able to access certain paths. |
| |
| {{range .Paths}}{{indent 4 .Path}} |
| {{indent 8 .Help}} |
| |
| {{end}} |
| |
| ` |