mirror of https://github.com/chaos-mesh/chaosd.git
combine chaosd and chaos binary
Signed-off-by: cwen0 <cwenyin0@gmail.com>
This commit is contained in:
parent
034392ae80
commit
48dfc808aa
20
Makefile
20
Makefile
|
|
@ -37,9 +37,15 @@ failpoint-disable: $(GOBIN)/failpoint-ctl
|
||||||
$(GOBIN)/failpoint-ctl:
|
$(GOBIN)/failpoint-ctl:
|
||||||
$(GO) get github.com/pingcap/failpoint/failpoint-ctl@v0.0.0-20200210140405-f8f9fb234798
|
$(GO) get github.com/pingcap/failpoint/failpoint-ctl@v0.0.0-20200210140405-f8f9fb234798
|
||||||
|
|
||||||
|
$(GOBIN)/revive:
|
||||||
|
$(GO) get github.com/mgechev/revive@v1.0.2-0.20200225072153-6219ca02fffb
|
||||||
|
|
||||||
|
$(GOBIN)/goimports:
|
||||||
|
$(GO) get golang.org/x/tools/cmd/goimports@v0.0.0-20200309202150-20ab64c0d93f
|
||||||
|
|
||||||
build: binary
|
build: binary
|
||||||
|
|
||||||
binary: chaosd chaos bin/pause bin/suicide
|
binary: chaosd bin/pause bin/suicide
|
||||||
|
|
||||||
taily-build:
|
taily-build:
|
||||||
if [ "$(shell docker ps --filter=name=$@ -q)" = "" ]; then \
|
if [ "$(shell docker ps --filter=name=$@ -q)" = "" ]; then \
|
||||||
|
|
@ -61,10 +67,7 @@ image-binary: image-build-base
|
||||||
endif
|
endif
|
||||||
|
|
||||||
chaosd:
|
chaosd:
|
||||||
$(CGOENV) go build -ldflags '$(LDFLAGS)' -o bin/chaosd ./cmd/chaosd/main.go
|
$(CGOENV) go build -ldflags '$(LDFLAGS)' -o bin/chaosd ./cmd/chaos/main.go
|
||||||
|
|
||||||
chaos:
|
|
||||||
$(GOENV) go build -ldflags '$(LDFLAGS)' -o bin/chaos ./cmd/chaos/main.go
|
|
||||||
|
|
||||||
image-build-base:
|
image-build-base:
|
||||||
DOCKER_BUILDKIT=0 docker build --ulimit nofile=65536:65536 -t pingcap/chaos-build-base ${DOCKER_BUILD_ARGS} images/build-base
|
DOCKER_BUILDKIT=0 docker build --ulimit nofile=65536:65536 -t pingcap/chaos-build-base ${DOCKER_BUILD_ARGS} images/build-base
|
||||||
|
|
@ -78,7 +81,6 @@ bin/pause: ./hack/pause.c
|
||||||
bin/suicide: ./hack/suicide.c
|
bin/suicide: ./hack/suicide.c
|
||||||
cc ./hack/suicide.c -o bin/suicide
|
cc ./hack/suicide.c -o bin/suicide
|
||||||
|
|
||||||
|
|
||||||
image-chaos-mesh-protoc:
|
image-chaos-mesh-protoc:
|
||||||
docker build -t pingcap/chaos-daemon-protoc ${DOCKER_BUILD_ARGS} ./images/protoc
|
docker build -t pingcap/chaos-daemon-protoc ${DOCKER_BUILD_ARGS} ./images/protoc
|
||||||
|
|
||||||
|
|
@ -107,6 +109,10 @@ groupimports: $(GOBIN)/goimports
|
||||||
vet:
|
vet:
|
||||||
$(CGOENV) go vet ./...
|
$(CGOENV) go vet ./...
|
||||||
|
|
||||||
|
lint: $(GOBIN)/revive
|
||||||
|
@echo "linting"
|
||||||
|
$< -formatter friendly -config revive.toml $$($(PACKAGE_LIST))
|
||||||
|
|
||||||
boilerplate:
|
boilerplate:
|
||||||
./hack/verify-boilerplate.sh
|
./hack/verify-boilerplate.sh
|
||||||
|
|
||||||
|
|
@ -115,4 +121,4 @@ tidy:
|
||||||
GO111MODULE=on go mod tidy
|
GO111MODULE=on go mod tidy
|
||||||
git diff -U --exit-code go.mod go.sum
|
git diff -U --exit-code go.mod go.sum
|
||||||
|
|
||||||
.PHONY: all build check fmt vet tidy binary chaosd chaos image-binary image-chaosd
|
.PHONY: all build check fmt vet lint tidy binary chaosd chaos image-binary image-chaosd
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2020 Chaos Mesh 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,
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
func NewDestroyCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "destroy UID",
|
||||||
|
Short: "Destroy a chaos experiment",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: destroyCommandF,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroyCommandF(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -23,10 +23,26 @@ import (
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/core"
|
"github.com/chaos-mesh/chaos-daemon/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type processFlag struct {
|
||||||
process string
|
process string
|
||||||
|
single int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
killProcessAction = "kill"
|
||||||
|
stopProcessAction = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (f *processFlag) valid(action string) error {
|
||||||
|
if len(f.process) == 0 {
|
||||||
|
return errors.New("process not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pFlag *processFlag
|
||||||
|
|
||||||
func NewProcessAttackCommand() *cobra.Command {
|
func NewProcessAttackCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "process <subcommand>",
|
Use: "process <subcommand>",
|
||||||
|
|
@ -44,11 +60,14 @@ func NewProcessAttackCommand() *cobra.Command {
|
||||||
func NewProcessKillCommand() *cobra.Command {
|
func NewProcessKillCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "kill [options]",
|
Use: "kill [options]",
|
||||||
Short: "kill process, default signal SIGKILL",
|
Short: "kill process, default signal 9",
|
||||||
Run: processKillCommandFunc,
|
Run: processKillCommandFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&process, "process", "p", "", "The process name or the process ID")
|
cmd.Flags().StringVarP(&pFlag.process, "process", "p", "", "The process name or the process ID")
|
||||||
|
cmd.Flags().IntVarP(&pFlag.single, "single", "s", 9, "The signal number to send")
|
||||||
|
cmd.Flags().StringVarP(&conf.Runtime, "runtime", "r", "docker", "current container runtime")
|
||||||
|
cmd.Flags().StringVarP(&conf.Platform, "platform", "f", "local", "platform to deploy, default: local, supported platform: local, kubernetes")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
@ -61,42 +80,41 @@ func NewProcessStopCommand() *cobra.Command {
|
||||||
Run: processStopCommandFunc,
|
Run: processStopCommandFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&process, "process", "p", "", "The process name or the process ID")
|
cmd.Flags().StringVarP(&pFlag.process, "process", "p", "", "The process name or the process ID")
|
||||||
|
pFlag.single = int(syscall.SIGSTOP)
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&conf.Runtime, "runtime", "r", "docker", "current container runtime")
|
||||||
|
cmd.Flags().StringVarP(&conf.Platform, "platform", "f", "local", "platform to deploy, default: local, supported platform: local, kubernetes")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func processKillCommandFunc(cmd *cobra.Command, args []string) {
|
func processKillCommandFunc(cmd *cobra.Command, args []string) {
|
||||||
processAttackF(cmd, syscall.SIGKILL)
|
if err := pFlag.valid(killProcessAction); err != nil {
|
||||||
|
ExitWithError(ExitBadArgs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
processAttackF(cmd, pFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processStopCommandFunc(cmd *cobra.Command, args []string) {
|
func processStopCommandFunc(cmd *cobra.Command, args []string) {
|
||||||
processAttackF(cmd, syscall.SIGSTOP)
|
if err := pFlag.valid(stopProcessAction); err != nil {
|
||||||
}
|
ExitWithError(ExitBadArgs, err)
|
||||||
|
|
||||||
func processAttackF(cmd *cobra.Command, sig syscall.Signal) {
|
|
||||||
if len(process) == 0 {
|
|
||||||
ExitWithError(ExitBadArgs, errors.New("process not provided"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := mustClientFromCmd(cmd)
|
processAttackF(cmd, pFlag)
|
||||||
|
}
|
||||||
|
|
||||||
resp, apiErr, err := cli.CreateProcessAttack(&core.ProcessCommand{
|
func processAttackF(cmd *cobra.Command, f *processFlag) {
|
||||||
Process: process,
|
chaos := mustChaosdFromCmd(cmd, conf)
|
||||||
Signal: sig,
|
|
||||||
|
uid, err := chaos.ProcessAttack(&core.ProcessCommand{
|
||||||
|
Process: pFlag.process,
|
||||||
|
Signal: syscall.Signal(f.single),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ExitWithError(ExitError, err)
|
ExitWithError(ExitError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiErr != nil {
|
NormalExit(fmt.Sprintf("Attack process %s successfully, uid: %s", f.process, uid))
|
||||||
ExitWithMsg(ExitError, fmt.Sprintf("Failed to attack process %s, %s", process, apiErr.Message))
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Status != 200 {
|
|
||||||
ExitWithMsg(ExitError, fmt.Sprintf("Failed to attack process %s, %s", process, resp.Message))
|
|
||||||
}
|
|
||||||
|
|
||||||
NormalExit(fmt.Sprintf("Attack process %s successfully, uid: %s", process, resp.UID))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,11 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package main
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
||||||
"github.com/pingcap/errors"
|
|
||||||
"github.com/pingcap/log"
|
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
|
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/bpm"
|
"github.com/chaos-mesh/chaos-daemon/pkg/bpm"
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/config"
|
"github.com/chaos-mesh/chaos-daemon/pkg/config"
|
||||||
|
|
@ -32,27 +25,36 @@ import (
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/version"
|
"github.com/chaos-mesh/chaos-daemon/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func NewServerCommand() *cobra.Command {
|
||||||
cfg := config.NewConfig()
|
cmd := &cobra.Command{
|
||||||
err := cfg.Parse(os.Args[1:])
|
Use: "server <option>",
|
||||||
|
Short: "run Chaosd Server",
|
||||||
|
Run: serverCommandFunc,
|
||||||
|
}
|
||||||
|
|
||||||
switch errors.Cause(err) {
|
cmd.Flags().IntVarP(&conf.ListenPort, "port", "p", 31767, "listen port of the Chaosd Server")
|
||||||
case nil:
|
cmd.Flags().StringVarP(&conf.ListenHost, "host", "h", "0.0.0.0", "listen host of the Chaosd Server")
|
||||||
case flag.ErrHelp:
|
cmd.Flags().StringVarP(&conf.Runtime, "runtime", "r", "docker", "current container runtime")
|
||||||
os.Exit(0)
|
cmd.Flags().BoolVar(&conf.EnablePprof, "enable-pprof", true, "enable pprof")
|
||||||
default:
|
cmd.Flags().IntVar(&conf.PprofPort, "pprof-port", 31766, "listen port of the pprof server")
|
||||||
log.Fatal("parse cmd flags error", zap.Error(err))
|
cmd.Flags().StringVarP(&conf.Platform, "platform", "f", "local", "platform to deploy, default: local, supported platform: local, kubernetes")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
var conf *config.Config
|
||||||
|
|
||||||
|
func serverCommandFunc(cmd *cobra.Command, args []string) {
|
||||||
|
if err := conf.Validate(); err != nil {
|
||||||
|
ExitWithError(ExitBadArgs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
version.PrintVersionInfo("Chaosd Server")
|
version.PrintVersionInfo("Chaosd Server")
|
||||||
if cfg.Version {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
app := fx.New(
|
app := fx.New(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
func() *config.Config {
|
func() *config.Config {
|
||||||
return cfg
|
return conf
|
||||||
},
|
},
|
||||||
container.NewCRIClient,
|
container.NewCRIClient,
|
||||||
bpm.NewBackgroundProcessManager,
|
bpm.NewBackgroundProcessManager,
|
||||||
|
|
@ -16,7 +16,13 @@ package command
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/bpm"
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/client"
|
"github.com/chaos-mesh/chaos-daemon/pkg/client"
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/config"
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/container"
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/server/chaosd"
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/store/dbstore"
|
||||||
|
"github.com/chaos-mesh/chaos-daemon/pkg/store/experiment"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustClientFromCmd(cmd *cobra.Command) *client.Client {
|
func mustClientFromCmd(cmd *cobra.Command) *client.Client {
|
||||||
|
|
@ -29,3 +35,17 @@ func mustClientFromCmd(cmd *cobra.Command) *client.Client {
|
||||||
Addr: url,
|
Addr: url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustChaosdFromCmd(cmd *cobra.Command, conf *config.Config) *chaosd.Server {
|
||||||
|
db, err := dbstore.DryDBStore()
|
||||||
|
if err != nil {
|
||||||
|
ExitWithError(ExitError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli, err := container.NewCRIClient(conf)
|
||||||
|
if err != nil {
|
||||||
|
ExitWithError(ExitError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chaosd.NewServer(conf, experiment.NewStore(db), cli, bpm.NewBackgroundProcessManager())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,9 @@ var (
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&cmdFlags.URL, "url", "u", "http://127.0.0.1:31767", "chaosd address")
|
rootCmd.PersistentFlags().StringVarP(&cmdFlags.URL, "url", "u", "http://127.0.0.1:31767", "chaosd address")
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
|
command.NewServerCommand(),
|
||||||
command.NewAttackCommand(),
|
command.NewAttackCommand(),
|
||||||
|
command.NewDestroyCommand(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ RUN update-alternatives --set iptables /usr/sbin/iptables-legacy
|
||||||
ENV RUST_BACKTRACE 1
|
ENV RUST_BACKTRACE 1
|
||||||
|
|
||||||
COPY --from=pingcap/chaos-binary /bin/chaosd /usr/local/bin/chaosd
|
COPY --from=pingcap/chaos-binary /bin/chaosd /usr/local/bin/chaosd
|
||||||
COPY --from=pingcap/chaos-binary /bin/chaos /usr/local/bin/chaos
|
|
||||||
COPY --from=pingcap/chaos-binary /bin/toda /usr/local/bin/toda
|
COPY --from=pingcap/chaos-binary /bin/toda /usr/local/bin/toda
|
||||||
COPY --from=pingcap/chaos-binary /bin/pause /usr/local/bin/pause
|
COPY --from=pingcap/chaos-binary /bin/pause /usr/local/bin/pause
|
||||||
COPY --from=pingcap/chaos-binary /bin/suicide /usr/local/bin/suicide
|
COPY --from=pingcap/chaos-binary /bin/suicide /usr/local/bin/suicide
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ type ProcessCommand struct {
|
||||||
// Newest bool
|
// Newest bool
|
||||||
// Oldest bool
|
// Oldest bool
|
||||||
// Exact bool
|
// Exact bool
|
||||||
// Duration string
|
|
||||||
// Interval int64
|
|
||||||
// KillChildren bool
|
// KillChildren bool
|
||||||
// User string
|
// User string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,13 @@ import (
|
||||||
|
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/server/chaosd"
|
"github.com/chaos-mesh/chaos-daemon/pkg/server/chaosd"
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/server/grpcserver"
|
"github.com/chaos-mesh/chaos-daemon/pkg/server/grpcserver"
|
||||||
"github.com/chaos-mesh/chaos-daemon/pkg/server/httpserver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Module = fx.Options(
|
var Module = fx.Options(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
chaosd.NewServer,
|
chaosd.NewServer,
|
||||||
grpcserver.NewServer,
|
grpcserver.NewServer,
|
||||||
httpserver.NewServer,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
fx.Invoke(grpcserver.Register),
|
fx.Invoke(grpcserver.Register),
|
||||||
fx.Invoke(httpserver.Register),
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,17 @@ func NewDBStore(lc fx.Lifecycle) (*DB, error) {
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DryDBStore() (*DB, error) {
|
||||||
|
gormDB, err := gorm.Open("sqlite3", path.Join(utils.GetProgramPath(), dataFile))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to open DB", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := &DB{
|
||||||
|
gormDB,
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
ignoreGeneratedHeader = false
|
||||||
|
severity = "error"
|
||||||
|
confidence = 0.8
|
||||||
|
errorCode = 1
|
||||||
|
warningCode = 0
|
||||||
|
|
||||||
|
[rule.blank-imports]
|
||||||
|
[rule.context-as-argument]
|
||||||
|
[rule.dot-imports]
|
||||||
|
[rule.error-return]
|
||||||
|
[rule.error-strings]
|
||||||
|
[rule.error-naming]
|
||||||
|
[rule.if-return]
|
||||||
|
[rule.package-comments]
|
||||||
|
[rule.range]
|
||||||
|
[rule.receiver-naming]
|
||||||
|
[rule.indent-error-flow]
|
||||||
|
[rule.empty-block]
|
||||||
|
[rule.superfluous-else]
|
||||||
|
[rule.modifies-parameter]
|
||||||
|
|
||||||
|
# Add these once issues are fixed
|
||||||
|
#[rule.var-naming]
|
||||||
|
# severity = "warning"
|
||||||
|
#[rule.confusing-naming]
|
||||||
|
# severity = "warning"
|
||||||
|
[rule.confusing-results]
|
||||||
|
severity = "warning"
|
||||||
|
[rule.flag-parameter]
|
||||||
|
severity = "warning"
|
||||||
|
#[rule.unused-parameter]
|
||||||
|
# severity = "warning"
|
||||||
|
|
||||||
|
# Decide whether we want these rules
|
||||||
|
# [rule.exported]
|
||||||
|
|
||||||
|
# Already checked by megacheck
|
||||||
|
# [rule.unreachable-code]
|
||||||
|
|
||||||
|
# Adding these will slow down the linter
|
||||||
|
# They are already provided by megacheck
|
||||||
|
# [rule.unexported-return]
|
||||||
|
# [rule.time-naming]
|
||||||
|
# [rule.errorf]
|
||||||
|
|
||||||
|
# Adding these will slow down the linter
|
||||||
|
# Not sure if they are already provided by megacheck
|
||||||
|
# [rule.var-declaration]
|
||||||
|
# [rule.context-keys-type]
|
||||||
Loading…
Reference in New Issue