package data import ( "bufio" "encoding/xml" "github.com/ClickHouse/clickhouse-diagnostics/internal/platform/utils" "github.com/pkg/errors" "gopkg.in/yaml.v3" "io/ioutil" "os" "path" "path/filepath" "regexp" ) type File interface { Copy(destPath string, removeSensitive bool) error FilePath() string } type SimpleFile struct { Path string } // Copy supports removeSensitive for other file types but for a simple file this doesn't do anything func (s SimpleFile) Copy(destPath string, removeSensitive bool) error { // simple copy easiest if err := utils.CopyFile(s.FilePath(), destPath); err != nil { return errors.Wrapf(err, "unable to copy file %s", s.FilePath()) } return nil } func (s SimpleFile) FilePath() string { return s.Path } func NewFileFrame(name string, filePaths []string) FileFrame { i := 0 files := make([]File, len(filePaths)) for i, path := range filePaths { files[i] = SimpleFile{ Path: path, } } return FileFrame{ name: name, i: &i, files: files, } } type FileFrame struct { name string i *int files []File } func (f FileFrame) Next() ([]interface{}, bool, error) { if len(f.files) == *(f.i) { return nil, false, nil } file := f.files[*f.i] *f.i++ value := make([]interface{}, 1) value[0] = file return value, true, nil } func (f FileFrame) Columns() []string { return []string{"files"} } func (f FileFrame) Name() string { return f.name } // DirectoryFileFrame represents a set of files under a directory type DirectoryFileFrame struct { FileFrame Directory string } func NewFileDirectoryFrame(directory string, exts []string) (DirectoryFileFrame, []error) { filePaths, errs := utils.ListFilesInDirectory(directory, exts) files := make([]File, len(filePaths)) for i, path := range filePaths { files[i] = SimpleFile{ Path: path, } } i := 0 return DirectoryFileFrame{ Directory: directory, FileFrame: FileFrame{ files: files, i: &i, }, }, errs } func (f DirectoryFileFrame) Next() ([]interface{}, bool, error) { if len(f.files) == *(f.i) { return nil, false, nil } file := f.files[*f.i] *f.i++ value := make([]interface{}, 1) value[0] = file return value, true, nil } func (f DirectoryFileFrame) Columns() []string { return []string{"files"} } func (f DirectoryFileFrame) Name() string { return f.Directory } type ConfigFile interface { File FindLogPaths() ([]string, error) FindIncludedConfig() (ConfigFile, error) IsIncluded() bool } type ConfigFileFrame struct { i *int Directory string files []ConfigFile } func (f ConfigFileFrame) Next() ([]interface{}, bool, error) { if len(f.files) == *(f.i) { return nil, false, nil } file := f.files[*f.i] *f.i++ value := make([]interface{}, 1) value[0] = file return value, true, nil } func (f ConfigFileFrame) Name() string { return f.Directory } func NewConfigFileFrame(directory string) (ConfigFileFrame, []error) { files, errs := utils.ListFilesInDirectory(directory, []string{"*.xml", "*.yaml", "*.yml"}) // we can't predict the length because of include files var configs []ConfigFile for _, path := range files { var configFile ConfigFile switch ext := filepath.Ext(path); ext { case ".xml": configFile = XmlConfigFile{ Path: path, Included: false, } case ".yml": configFile = YamlConfigFile{ Path: path, Included: false, } case ".yaml": configFile = YamlConfigFile{ Path: path, } } if configFile != nil { configs = append(configs, configFile) // add any included configs iConf, err := configFile.FindIncludedConfig() if err != nil { errs = append(errs, err) } else { if iConf.FilePath() != "" { configs = append(configs, iConf) } } } } i := 0 return ConfigFileFrame{ i: &i, Directory: directory, files: configs, }, errs } func (f ConfigFileFrame) Columns() []string { return []string{"config"} } func (f ConfigFileFrame) FindLogPaths() (logPaths []string, errors []error) { for _, configFile := range f.files { paths, err := configFile.FindLogPaths() if err != nil { errors = append(errors, err) } else { logPaths = append(logPaths, paths...) } } return logPaths, errors } type XmlConfigFile struct { Path string Included bool } // these patterns will be used to remove sensitive content - matches of the pattern will be replaced with the key var xmlSensitivePatterns = map[string]*regexp.Regexp{ "Replaced": regexp.MustCompile(`(.*)`), "Replaced": regexp.MustCompile(`(.*)`), "Replaced": regexp.MustCompile(`(.*)`), "Replaced": regexp.MustCompile(`(.*)`), "Replaced": regexp.MustCompile(`(.*)`), } func (x XmlConfigFile) Copy(destPath string, removeSensitive bool) error { if !removeSensitive { // simple copy easiest if err := utils.CopyFile(x.FilePath(), destPath); err != nil { return errors.Wrapf(err, "unable to copy file %s", x.FilePath()) } return nil } return sensitiveFileCopy(x.FilePath(), destPath, xmlSensitivePatterns) } func (x XmlConfigFile) FilePath() string { return x.Path } func (x XmlConfigFile) IsIncluded() bool { return x.Included } type XmlLoggerConfig struct { XMLName xml.Name `xml:"logger"` ErrorLog string `xml:"errorlog"` Log string `xml:"log"` } type YandexXMLConfig struct { XMLName xml.Name `xml:"yandex"` Clickhouse XmlLoggerConfig `xml:"logger"` IncludeFrom string `xml:"include_from"` } type XmlConfig struct { XMLName xml.Name `xml:"clickhouse"` Clickhouse XmlLoggerConfig `xml:"logger"` IncludeFrom string `xml:"include_from"` } func (x XmlConfigFile) UnmarshallConfig() (XmlConfig, error) { inputFile, err := ioutil.ReadFile(x.Path) if err != nil { return XmlConfig{}, err } var cConfig XmlConfig err = xml.Unmarshal(inputFile, &cConfig) if err == nil { return XmlConfig{ Clickhouse: cConfig.Clickhouse, IncludeFrom: cConfig.IncludeFrom, }, nil } // attempt to marshall as yandex file var yConfig YandexXMLConfig err = xml.Unmarshal(inputFile, &yConfig) if err != nil { return XmlConfig{}, err } return XmlConfig{ Clickhouse: yConfig.Clickhouse, IncludeFrom: yConfig.IncludeFrom, }, nil } func (x XmlConfigFile) FindLogPaths() ([]string, error) { var paths []string config, err := x.UnmarshallConfig() if err != nil { return nil, err } if config.Clickhouse.Log != "" { paths = append(paths, config.Clickhouse.Log) } if config.Clickhouse.ErrorLog != "" { paths = append(paths, config.Clickhouse.ErrorLog) } return paths, nil } func (x XmlConfigFile) FindIncludedConfig() (ConfigFile, error) { if x.Included { //cant recurse return XmlConfigFile{}, nil } config, err := x.UnmarshallConfig() if err != nil { return XmlConfigFile{}, err } // we need to convert this if config.IncludeFrom != "" { if filepath.IsAbs(config.IncludeFrom) { return XmlConfigFile{Path: config.IncludeFrom, Included: true}, nil } confDir := filepath.Dir(x.FilePath()) return XmlConfigFile{Path: path.Join(confDir, config.IncludeFrom), Included: true}, nil } return XmlConfigFile{}, nil } type YamlConfigFile struct { Path string Included bool } var ymlSensitivePatterns = map[string]*regexp.Regexp{ "password: 'Replaced'": regexp.MustCompile(`password:\s*.*$`), "password_sha256_hex: 'Replaced'": regexp.MustCompile(`password_sha256_hex:\s*.*$`), "access_key_id: 'Replaced'": regexp.MustCompile(`access_key_id:\s*.*$`), "secret_access_key: 'Replaced'": regexp.MustCompile(`secret_access_key:\s*.*$`), "secret: 'Replaced'": regexp.MustCompile(`secret:\s*.*$`), } func (y YamlConfigFile) Copy(destPath string, removeSensitive bool) error { if !removeSensitive { // simple copy easiest if err := utils.CopyFile(y.FilePath(), destPath); err != nil { return errors.Wrapf(err, "unable to copy file %s", y.FilePath()) } return nil } return sensitiveFileCopy(y.FilePath(), destPath, ymlSensitivePatterns) } func (y YamlConfigFile) FilePath() string { return y.Path } func (y YamlConfigFile) IsIncluded() bool { return y.Included } type YamlLoggerConfig struct { Log string ErrorLog string } type YamlConfig struct { Logger YamlLoggerConfig Include_From string } func (y YamlConfigFile) FindLogPaths() ([]string, error) { var paths []string inputFile, err := ioutil.ReadFile(y.Path) if err != nil { return nil, err } var config YamlConfig err = yaml.Unmarshal(inputFile, &config) if err != nil { return nil, err } if config.Logger.Log != "" { paths = append(paths, config.Logger.Log) } if config.Logger.ErrorLog != "" { paths = append(paths, config.Logger.ErrorLog) } return paths, nil } func (y YamlConfigFile) FindIncludedConfig() (ConfigFile, error) { if y.Included { //cant recurse return YamlConfigFile{}, nil } inputFile, err := ioutil.ReadFile(y.Path) if err != nil { return YamlConfigFile{}, err } var config YamlConfig err = yaml.Unmarshal(inputFile, &config) if err != nil { return YamlConfigFile{}, err } if config.Include_From != "" { if filepath.IsAbs(config.Include_From) { return YamlConfigFile{Path: config.Include_From, Included: true}, nil } confDir := filepath.Dir(y.FilePath()) return YamlConfigFile{Path: path.Join(confDir, config.Include_From), Included: true}, nil } return YamlConfigFile{}, nil } func sensitiveFileCopy(sourcePath string, destPath string, patterns map[string]*regexp.Regexp) error { destDir := filepath.Dir(destPath) if err := os.MkdirAll(destDir, os.ModePerm); err != nil { return errors.Wrapf(err, "unable to create directory %s", destDir) } // currently, we don't unmarshall into a struct - we want to preserve structure and comments. Possibly could // be handled but for simplicity we do a line parse for now inputFile, err := os.Open(sourcePath) if err != nil { return err } defer inputFile.Close() outputFile, err := os.Create(destPath) if err != nil { return err } defer outputFile.Close() writer := bufio.NewWriter(outputFile) scanner := bufio.NewScanner(inputFile) for scanner.Scan() { line := scanner.Text() for repl, pattern := range patterns { line = pattern.ReplaceAllString(line, repl) } _, err = writer.WriteString(line + "\n") if err != nil { return err } } writer.Flush() return nil }