github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/namespace/update.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package namespace
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"net/http"
    28  	"path"
    29  	"reflect"
    30  
    31  	clusterclient "github.com/m3db/m3/src/cluster/client"
    32  	"github.com/m3db/m3/src/cluster/placementhandler/handleroptions"
    33  	nsproto "github.com/m3db/m3/src/dbnode/generated/proto/namespace"
    34  	"github.com/m3db/m3/src/dbnode/namespace"
    35  	"github.com/m3db/m3/src/query/api/v1/route"
    36  	"github.com/m3db/m3/src/query/generated/proto/admin"
    37  	"github.com/m3db/m3/src/query/util/logging"
    38  	xerrors "github.com/m3db/m3/src/x/errors"
    39  	"github.com/m3db/m3/src/x/instrument"
    40  	xhttp "github.com/m3db/m3/src/x/net/http"
    41  
    42  	"github.com/gogo/protobuf/jsonpb"
    43  	"go.uber.org/zap"
    44  )
    45  
    46  var (
    47  	// M3DBUpdateURL is the url for the M3DB namespace update handler.
    48  	M3DBUpdateURL = path.Join(route.Prefix, M3DBServiceNamespacePathName)
    49  
    50  	// UpdateHTTPMethod is the HTTP method used with this resource.
    51  	UpdateHTTPMethod = http.MethodPut
    52  
    53  	fieldNameRetentionOptions   = "RetentionOptions"
    54  	fieldNameRetentionPeriod    = "RetentionPeriodNanos"
    55  	fieldNameRuntimeOptions     = "RuntimeOptions"
    56  	fieldNameAggregationOptions = "AggregationOptions"
    57  	fieldNameExtendedOptions    = "ExtendedOptions"
    58  
    59  	errEmptyNamespaceName      = errors.New("must specify namespace name")
    60  	errEmptyNamespaceOptions   = errors.New("update options cannot be empty")
    61  	errNamespaceFieldImmutable = errors.New("namespace option field is immutable")
    62  
    63  	allowedUpdateOptionsFields = map[string]struct{}{
    64  		fieldNameRetentionOptions:   {},
    65  		fieldNameRuntimeOptions:     {},
    66  		fieldNameAggregationOptions: {},
    67  		fieldNameExtendedOptions:    {},
    68  	}
    69  )
    70  
    71  // UpdateHandler is the handler for namespace updates.
    72  type UpdateHandler Handler
    73  
    74  // NewUpdateHandler returns a new instance of UpdateHandler.
    75  func NewUpdateHandler(
    76  	client clusterclient.Client,
    77  	instrumentOpts instrument.Options,
    78  ) *UpdateHandler {
    79  	return &UpdateHandler{
    80  		client:         client,
    81  		instrumentOpts: instrumentOpts,
    82  	}
    83  }
    84  
    85  func (h *UpdateHandler) ServeHTTP(
    86  	svc handleroptions.ServiceNameAndDefaults,
    87  	w http.ResponseWriter,
    88  	r *http.Request,
    89  ) {
    90  	ctx := r.Context()
    91  	logger := logging.WithContext(ctx, h.instrumentOpts)
    92  
    93  	md, rErr := h.parseRequest(r)
    94  	if rErr != nil {
    95  		logger.Warn("unable to parse request", zap.Error(rErr))
    96  		xhttp.WriteError(w, rErr)
    97  		return
    98  	}
    99  
   100  	opts := handleroptions.NewServiceOptions(svc, r.Header, nil)
   101  	nsRegistry, err := h.Update(md, opts)
   102  	if err != nil {
   103  		logger.Error("unable to update namespace", zap.Error(err))
   104  		xhttp.WriteError(w, err)
   105  		return
   106  	}
   107  
   108  	resp := &admin.NamespaceGetResponse{
   109  		Registry: &nsRegistry,
   110  	}
   111  
   112  	xhttp.WriteProtoMsgJSONResponse(w, resp, logger)
   113  }
   114  
   115  func (h *UpdateHandler) parseRequest(r *http.Request) (*admin.NamespaceUpdateRequest, error) {
   116  	defer r.Body.Close()
   117  	rBody, err := xhttp.DurationToNanosBytes(r.Body)
   118  	if err != nil {
   119  		return nil, xerrors.NewInvalidParamsError(err)
   120  	}
   121  
   122  	updateReq := new(admin.NamespaceUpdateRequest)
   123  	if err := jsonpb.Unmarshal(bytes.NewReader(rBody), updateReq); err != nil {
   124  		return nil, xerrors.NewInvalidParamsError(err)
   125  	}
   126  
   127  	if err := validateUpdateRequest(updateReq); err != nil {
   128  		err := fmt.Errorf("unable to validate update request: %w", err)
   129  		return nil, xerrors.NewInvalidParamsError(err)
   130  	}
   131  
   132  	return updateReq, nil
   133  }
   134  
   135  // Ensure that only fields we allow to be updated (e.g. retention period) are
   136  // non-zero. Uses reflection to be resilient against adding more immutable
   137  // fields to namespaceOptions but forgetting to validate them here.
   138  func validateUpdateRequest(req *admin.NamespaceUpdateRequest) error {
   139  	if req.Name == "" {
   140  		return errEmptyNamespaceName
   141  	}
   142  
   143  	if req.Options == nil {
   144  		return errEmptyNamespaceOptions
   145  	}
   146  
   147  	optsVal := reflect.ValueOf(*req.Options)
   148  	allNonZeroFields := true
   149  	for i := 0; i < optsVal.NumField(); i++ {
   150  		field := optsVal.Field(i)
   151  		fieldName := optsVal.Type().Field(i).Name
   152  		if field.IsZero() {
   153  			continue
   154  		}
   155  
   156  		allNonZeroFields = false
   157  
   158  		_, ok := allowedUpdateOptionsFields[fieldName]
   159  		if !ok {
   160  			return fmt.Errorf("%s: %w", fieldName, errNamespaceFieldImmutable)
   161  		}
   162  	}
   163  
   164  	if allNonZeroFields {
   165  		return errEmptyNamespaceOptions
   166  	}
   167  
   168  	if opts := req.Options.RetentionOptions; opts != nil {
   169  		optsVal := reflect.ValueOf(*opts)
   170  		for i := 0; i < optsVal.NumField(); i++ {
   171  			field := optsVal.Field(i)
   172  			fieldName := optsVal.Type().Field(i).Name
   173  			if !field.IsZero() && fieldName != fieldNameRetentionPeriod {
   174  				return fmt.Errorf("%s.%s: %w", fieldNameRetentionOptions, fieldName, errNamespaceFieldImmutable)
   175  			}
   176  		}
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  // Update updates a namespace.
   183  func (h *UpdateHandler) Update(
   184  	updateReq *admin.NamespaceUpdateRequest,
   185  	opts handleroptions.ServiceOptions,
   186  ) (nsproto.Registry, error) {
   187  	var emptyReg nsproto.Registry
   188  
   189  	store, err := h.client.Store(opts.KVOverrideOptions())
   190  	if err != nil {
   191  		return emptyReg, err
   192  	}
   193  
   194  	currentMetadata, version, err := Metadata(store)
   195  	if err != nil {
   196  		return emptyReg, err
   197  	}
   198  
   199  	newMetadata := make(map[string]namespace.Metadata)
   200  	for _, ns := range currentMetadata {
   201  		newMetadata[ns.ID().String()] = ns
   202  	}
   203  
   204  	ns, ok := newMetadata[updateReq.Name]
   205  	if !ok {
   206  		return emptyReg, xhttp.NewError(
   207  			fmt.Errorf("namespace not found: err=%s", updateReq.Name),
   208  			http.StatusNotFound)
   209  	}
   210  
   211  	// Replace targeted namespace with modified retention.
   212  	if newRetentionOpts := updateReq.Options.RetentionOptions; newRetentionOpts != nil {
   213  		if newNanos := newRetentionOpts.RetentionPeriodNanos; newNanos != 0 {
   214  			dur := namespace.FromNanos(newNanos)
   215  			retentionOpts := ns.Options().RetentionOptions().
   216  				SetRetentionPeriod(dur)
   217  			opts := ns.Options().
   218  				SetRetentionOptions(retentionOpts)
   219  			ns, err = namespace.NewMetadata(ns.ID(), opts)
   220  			if err != nil {
   221  				return emptyReg, xerrors.NewInvalidParamsError(fmt.Errorf(
   222  					"error constructing new metadata: %w", err))
   223  			}
   224  		}
   225  	}
   226  
   227  	// Update runtime options.
   228  	if newRuntimeOpts := updateReq.Options.RuntimeOptions; newRuntimeOpts != nil {
   229  		runtimeOpts := ns.Options().RuntimeOptions()
   230  		if v := newRuntimeOpts.WriteIndexingPerCPUConcurrency; v != nil {
   231  			runtimeOpts = runtimeOpts.SetWriteIndexingPerCPUConcurrency(&v.Value)
   232  		}
   233  		if v := newRuntimeOpts.FlushIndexingPerCPUConcurrency; v != nil {
   234  			runtimeOpts = runtimeOpts.SetFlushIndexingPerCPUConcurrency(&v.Value)
   235  		}
   236  		opts := ns.Options().
   237  			SetRuntimeOptions(runtimeOpts)
   238  		ns, err = namespace.NewMetadata(ns.ID(), opts)
   239  		if err != nil {
   240  			return emptyReg, xerrors.NewInvalidParamsError(fmt.Errorf(
   241  				"error constructing new metadata: %w", err))
   242  		}
   243  	}
   244  
   245  	// Update extended options.
   246  	if newExtendedOptions := updateReq.Options.ExtendedOptions; newExtendedOptions != nil {
   247  		newExtOpts, err := namespace.ToExtendedOptions(newExtendedOptions)
   248  		if err != nil {
   249  			return emptyReg, xerrors.NewInvalidParamsError(err)
   250  		}
   251  		opts := ns.Options().SetExtendedOptions(newExtOpts)
   252  		ns, err = namespace.NewMetadata(ns.ID(), opts)
   253  		if err != nil {
   254  			return emptyReg, xerrors.NewInvalidParamsError(fmt.Errorf(
   255  				"error constructing new metadata: %w", err))
   256  		}
   257  	}
   258  	if protoAggOpts := updateReq.Options.AggregationOptions; protoAggOpts != nil {
   259  		newAggOpts, err := namespace.ToAggregationOptions(protoAggOpts)
   260  		if err != nil {
   261  			return emptyReg, xerrors.NewInvalidParamsError(fmt.Errorf(
   262  				"error constructing construction aggregationOptions: %w", err))
   263  		}
   264  		if !ns.Options().AggregationOptions().Equal(newAggOpts) {
   265  			opts := ns.Options().SetAggregationOptions(newAggOpts)
   266  			ns, err = namespace.NewMetadata(ns.ID(), opts)
   267  			if err != nil {
   268  				return emptyReg, xerrors.NewInvalidParamsError(fmt.Errorf(
   269  					"error constructing new metadata: %w", err))
   270  			}
   271  		}
   272  	}
   273  
   274  	// Update the namespace in case an update occurred.
   275  	newMetadata[updateReq.Name] = ns
   276  
   277  	// Set the new slice and update.
   278  	newMDs := make([]namespace.Metadata, 0, len(newMetadata))
   279  	for _, elem := range newMetadata {
   280  		newMDs = append(newMDs, elem)
   281  	}
   282  
   283  	if err = validateNamespaceAggregationOptions(newMDs); err != nil {
   284  		return emptyReg, xerrors.NewInvalidParamsError(err)
   285  	}
   286  
   287  	nsMap, err := namespace.NewMap(newMDs)
   288  	if err != nil {
   289  		return emptyReg, xerrors.NewInvalidParamsError(err)
   290  	}
   291  
   292  	protoRegistry, err := namespace.ToProto(nsMap)
   293  	if err != nil {
   294  		return emptyReg, fmt.Errorf("error constructing namespace protobuf: %w", err)
   295  	}
   296  
   297  	_, err = store.CheckAndSet(M3DBNodeNamespacesKey, version, protoRegistry)
   298  	if err != nil {
   299  		return emptyReg, fmt.Errorf("failed to update namespace: %w", err)
   300  	}
   301  
   302  	return *protoRegistry, nil
   303  }