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 }