github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cmd/util.go (about)

     1  // Copyright 2020 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package cmd
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	liberrors "errors"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/url"
    23  	"os"
    24  	"os/signal"
    25  	"strings"
    26  	"syscall"
    27  	"time"
    28  
    29  	"golang.org/x/net/http/httpproxy"
    30  
    31  	"github.com/BurntSushi/toml"
    32  	"github.com/pingcap/errors"
    33  	"github.com/pingcap/log"
    34  	"github.com/pingcap/ticdc/cdc"
    35  	"github.com/pingcap/ticdc/cdc/entry"
    36  	"github.com/pingcap/ticdc/cdc/kv"
    37  	"github.com/pingcap/ticdc/cdc/model"
    38  	"github.com/pingcap/ticdc/cdc/sink"
    39  	"github.com/pingcap/ticdc/pkg/config"
    40  	"github.com/pingcap/ticdc/pkg/filter"
    41  	"github.com/pingcap/ticdc/pkg/httputil"
    42  	"github.com/pingcap/ticdc/pkg/logutil"
    43  	"github.com/pingcap/ticdc/pkg/security"
    44  	"github.com/pingcap/ticdc/pkg/util"
    45  	"github.com/pingcap/tidb/store/tikv/oracle"
    46  	"github.com/spf13/cobra"
    47  	"github.com/spf13/pflag"
    48  	"go.etcd.io/etcd/clientv3/concurrency"
    49  	"go.uber.org/zap"
    50  )
    51  
    52  var (
    53  	caPath        string
    54  	certPath      string
    55  	keyPath       string
    56  	allowedCertCN string
    57  )
    58  
    59  var errOwnerNotFound = liberrors.New("owner not found")
    60  
    61  var tsGapWarning int64 = 86400 * 1000 // 1 day in milliseconds
    62  
    63  // Endpoint schemes.
    64  const (
    65  	HTTP  = "http"
    66  	HTTPS = "https"
    67  )
    68  
    69  func addSecurityFlags(flags *pflag.FlagSet, isServer bool) {
    70  	flags.StringVar(&caPath, "ca", "", "CA certificate path for TLS connection")
    71  	flags.StringVar(&certPath, "cert", "", "Certificate path for TLS connection")
    72  	flags.StringVar(&keyPath, "key", "", "Private key path for TLS connection")
    73  	if isServer {
    74  		flags.StringVar(&allowedCertCN, "cert-allowed-cn", "", "Verify caller's identity (cert Common Name). Use ',' to separate multiple CN")
    75  	}
    76  }
    77  
    78  func getCredential() *security.Credential {
    79  	var certAllowedCN []string
    80  	if len(allowedCertCN) != 0 {
    81  		certAllowedCN = strings.Split(allowedCertCN, ",")
    82  	}
    83  	return &security.Credential{
    84  		CAPath:        caPath,
    85  		CertPath:      certPath,
    86  		KeyPath:       keyPath,
    87  		CertAllowedCN: certAllowedCN,
    88  	}
    89  }
    90  
    91  // initCmd initializes the logger, the default context and returns its cancel function.
    92  func initCmd(cmd *cobra.Command, logCfg *logutil.Config) context.CancelFunc {
    93  	// Init log.
    94  	err := logutil.InitLogger(logCfg)
    95  	if err != nil {
    96  		cmd.Printf("init logger error %v\n", errors.ErrorStack(err))
    97  		os.Exit(1)
    98  	}
    99  	log.Info("init log", zap.String("file", logCfg.File), zap.String("level", logCfg.Level))
   100  
   101  	sc := make(chan os.Signal, 1)
   102  	signal.Notify(sc,
   103  		syscall.SIGHUP,
   104  		syscall.SIGINT,
   105  		syscall.SIGTERM,
   106  		syscall.SIGQUIT)
   107  
   108  	ctx, cancel := context.WithCancel(context.Background())
   109  	go func() {
   110  		sig := <-sc
   111  		log.Info("got signal to exit", zap.Stringer("signal", sig))
   112  		cancel()
   113  	}()
   114  	defaultContext = ctx
   115  	return cancel
   116  }
   117  
   118  func getAllCaptures(ctx context.Context) ([]*capture, error) {
   119  	_, raw, err := cdcEtcdCli.GetCaptures(ctx)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	ownerID, err := cdcEtcdCli.GetOwnerID(ctx, kv.CaptureOwnerKey)
   124  	if err != nil && errors.Cause(err) != concurrency.ErrElectionNoLeader {
   125  		return nil, err
   126  	}
   127  	captures := make([]*capture, 0, len(raw))
   128  	for _, c := range raw {
   129  		isOwner := c.ID == ownerID
   130  		captures = append(captures,
   131  			&capture{ID: c.ID, IsOwner: isOwner, AdvertiseAddr: c.AdvertiseAddr})
   132  	}
   133  	return captures, nil
   134  }
   135  
   136  func getOwnerCapture(ctx context.Context) (*capture, error) {
   137  	captures, err := getAllCaptures(ctx)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	for _, c := range captures {
   142  		if c.IsOwner {
   143  			return c, nil
   144  		}
   145  	}
   146  	return nil, errors.Trace(errOwnerNotFound)
   147  }
   148  
   149  func applyAdminChangefeed(ctx context.Context, job model.AdminJob, credential *security.Credential) error {
   150  	owner, err := getOwnerCapture(ctx)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	scheme := "http"
   155  	if credential.IsTLSEnabled() {
   156  		scheme = "https"
   157  	}
   158  	addr := fmt.Sprintf("%s://%s/capture/owner/admin", scheme, owner.AdvertiseAddr)
   159  	cli, err := httputil.NewClient(credential)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	forceRemoveOpt := "false"
   164  	if job.Opts != nil && job.Opts.ForceRemove {
   165  		forceRemoveOpt = "true"
   166  	}
   167  	resp, err := cli.PostForm(addr, map[string][]string{
   168  		cdc.APIOpVarAdminJob:           {fmt.Sprint(int(job.Type))},
   169  		cdc.APIOpVarChangefeedID:       {job.CfID},
   170  		cdc.APIOpForceRemoveChangefeed: {forceRemoveOpt},
   171  	})
   172  	if err != nil {
   173  		return err
   174  	}
   175  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   176  		body, err := ioutil.ReadAll(resp.Body)
   177  		if err != nil {
   178  			return errors.BadRequestf("admin changefeed failed")
   179  		}
   180  		return errors.BadRequestf("%s", string(body))
   181  	}
   182  	return nil
   183  }
   184  
   185  func applyOwnerChangefeedQuery(
   186  	ctx context.Context, cid model.ChangeFeedID, credential *security.Credential,
   187  ) (string, error) {
   188  	owner, err := getOwnerCapture(ctx)
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  	scheme := "http"
   193  	if credential.IsTLSEnabled() {
   194  		scheme = "https"
   195  	}
   196  	addr := fmt.Sprintf("%s://%s/capture/owner/changefeed/query", scheme, owner.AdvertiseAddr)
   197  	cli, err := httputil.NewClient(credential)
   198  	if err != nil {
   199  		return "", err
   200  	}
   201  	resp, err := cli.PostForm(addr, map[string][]string{
   202  		cdc.APIOpVarChangefeedID: {cid},
   203  	})
   204  	if err != nil {
   205  		return "", err
   206  	}
   207  	body, err := ioutil.ReadAll(resp.Body)
   208  	if err != nil {
   209  		return "", errors.BadRequestf("query changefeed simplified status")
   210  	}
   211  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   212  		return "", errors.BadRequestf("%s", string(body))
   213  	}
   214  	return string(body), nil
   215  }
   216  
   217  func jsonPrint(cmd *cobra.Command, v interface{}) error {
   218  	data, err := json.MarshalIndent(v, "", "  ")
   219  	if err != nil {
   220  		return err
   221  	}
   222  	cmd.Printf("%s\n", data)
   223  	return nil
   224  }
   225  
   226  func verifyStartTs(ctx context.Context, changefeedID string, startTs uint64) error {
   227  	if disableGCSafePointCheck {
   228  		return nil
   229  	}
   230  	return util.CheckSafetyOfStartTs(ctx, pdCli, changefeedID, startTs)
   231  }
   232  
   233  func verifyTargetTs(startTs, targetTs uint64) error {
   234  	if targetTs > 0 && targetTs <= startTs {
   235  		return errors.Errorf("target-ts %d must be larger than start-ts: %d", targetTs, startTs)
   236  	}
   237  	return nil
   238  }
   239  
   240  func verifyTables(credential *security.Credential, cfg *config.ReplicaConfig, startTs uint64) (ineligibleTables, eligibleTables []model.TableName, err error) {
   241  	kvStore, err := kv.CreateTiStore(cliPdAddr, credential)
   242  	if err != nil {
   243  		return nil, nil, err
   244  	}
   245  	meta, err := kv.GetSnapshotMeta(kvStore, startTs)
   246  	if err != nil {
   247  		return nil, nil, errors.Trace(err)
   248  	}
   249  
   250  	filter, err := filter.NewFilter(cfg)
   251  	if err != nil {
   252  		return nil, nil, errors.Trace(err)
   253  	}
   254  
   255  	snap, err := entry.NewSingleSchemaSnapshotFromMeta(meta, startTs, false /* explicitTables */)
   256  	if err != nil {
   257  		return nil, nil, errors.Trace(err)
   258  	}
   259  
   260  	for tID, tableName := range snap.CloneTables() {
   261  		tableInfo, exist := snap.TableByID(tID)
   262  		if !exist {
   263  			return nil, nil, errors.NotFoundf("table %d", tID)
   264  		}
   265  		if filter.ShouldIgnoreTable(tableName.Schema, tableName.Table) {
   266  			continue
   267  		}
   268  		if !tableInfo.IsEligible(false /* forceReplicate */) {
   269  			ineligibleTables = append(ineligibleTables, tableName)
   270  		} else {
   271  			eligibleTables = append(eligibleTables, tableName)
   272  		}
   273  	}
   274  	return
   275  }
   276  
   277  func verifySink(
   278  	ctx context.Context, sinkURI string, cfg *config.ReplicaConfig, opts map[string]string,
   279  ) error {
   280  	filter, err := filter.NewFilter(cfg)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	errCh := make(chan error)
   285  	s, err := sink.NewSink(ctx, "cli-verify", sinkURI, filter, cfg, opts, errCh)
   286  	if err != nil {
   287  		return err
   288  	}
   289  	err = s.Close(ctx)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	select {
   294  	case err = <-errCh:
   295  		if err != nil {
   296  			return err
   297  		}
   298  	default:
   299  	}
   300  	return nil
   301  }
   302  
   303  // verifyReplicaConfig do strictDecodeFile check and only verify the rules for now
   304  func verifyReplicaConfig(path, component string, cfg *config.ReplicaConfig) error {
   305  	err := strictDecodeFile(path, component, cfg)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	_, err = filter.VerifyRules(cfg)
   310  	return err
   311  }
   312  
   313  // strictDecodeFile decodes the toml file strictly. If any item in confFile file is not mapped
   314  // into the Config struct, issue an error and stop the server from starting.
   315  func strictDecodeFile(path, component string, cfg interface{}) error {
   316  	metaData, err := toml.DecodeFile(path, cfg)
   317  	if err != nil {
   318  		return errors.Trace(err)
   319  	}
   320  
   321  	if undecoded := metaData.Undecoded(); len(undecoded) > 0 {
   322  		var b strings.Builder
   323  		for i, item := range undecoded {
   324  			if i != 0 {
   325  				b.WriteString(", ")
   326  			}
   327  			b.WriteString(item.String())
   328  		}
   329  		err = errors.Errorf("component %s's config file %s contained unknown configuration options: %s",
   330  			component, path, b.String())
   331  	}
   332  
   333  	return errors.Trace(err)
   334  }
   335  
   336  // logHTTPProxies logs HTTP proxy relative environment variables.
   337  func logHTTPProxies() {
   338  	fields := proxyFields()
   339  	if len(fields) > 0 {
   340  		log.Info("using proxy config", fields...)
   341  	}
   342  }
   343  
   344  func proxyFields() []zap.Field {
   345  	proxyCfg := httpproxy.FromEnvironment()
   346  	fields := make([]zap.Field, 0, 3)
   347  	if proxyCfg.HTTPProxy != "" {
   348  		fields = append(fields, zap.String("http_proxy", proxyCfg.HTTPProxy))
   349  	}
   350  	if proxyCfg.HTTPSProxy != "" {
   351  		fields = append(fields, zap.String("https_proxy", proxyCfg.HTTPSProxy))
   352  	}
   353  	if proxyCfg.NoProxy != "" {
   354  		fields = append(fields, zap.String("no_proxy", proxyCfg.NoProxy))
   355  	}
   356  	return fields
   357  }
   358  
   359  func confirmLargeDataGap(ctx context.Context, cmd *cobra.Command, startTs uint64) error {
   360  	if noConfirm {
   361  		return nil
   362  	}
   363  	currentPhysical, _, err := pdCli.GetTS(ctx)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	tsGap := currentPhysical - oracle.ExtractPhysical(startTs)
   368  	if tsGap > tsGapWarning {
   369  		cmd.Printf("Replicate lag (%s) is larger than 1 days, "+
   370  			"large data may cause OOM, confirm to continue at your own risk [Y/N]\n",
   371  			time.Duration(tsGap)*time.Millisecond,
   372  		)
   373  		var yOrN string
   374  		_, err := fmt.Scan(&yOrN)
   375  		if err != nil {
   376  			return err
   377  		}
   378  		if strings.ToLower(strings.TrimSpace(yOrN)) != "y" {
   379  			return errors.NewNoStackError("abort changefeed create or resume")
   380  		}
   381  	}
   382  	return nil
   383  }
   384  
   385  // verifyPdEndpoint verifies whether the pd endpoint is a valid http or https URL.
   386  // The certificate is required when using https.
   387  func verifyPdEndpoint(pdEndpoint string, useTLS bool) error {
   388  	u, err := url.Parse(pdEndpoint)
   389  	if err != nil {
   390  		return errors.Annotate(err, "parse PD endpoint")
   391  	}
   392  	if (u.Scheme != HTTP && u.Scheme != HTTPS) || u.Host == "" {
   393  		return errors.New("PD endpoint should be a valid http or https URL")
   394  	}
   395  
   396  	if useTLS {
   397  		if u.Scheme == HTTP {
   398  			return errors.New("PD endpoint scheme should be https")
   399  		}
   400  	} else {
   401  		if u.Scheme == HTTPS {
   402  			return errors.New("PD endpoint scheme is https, please provide certificate")
   403  		}
   404  	}
   405  	return nil
   406  }