diff --git a/pkg/cmd/gtctl/cluster/create/create.go b/pkg/cmd/gtctl/cluster/create/create.go index d98b228d..c3cfa7b6 100644 --- a/pkg/cmd/gtctl/cluster/create/create.go +++ b/pkg/cmd/gtctl/cluster/create/create.go @@ -19,6 +19,8 @@ import ( "fmt" "io/ioutil" "os" + "os/signal" + "syscall" "time" "github.com/spf13/cobra" @@ -28,6 +30,7 @@ import ( "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/common" "github.com/GreptimeTeam/gtctl/pkg/deployer" "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal" + bmconfig "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" "github.com/GreptimeTeam/gtctl/pkg/deployer/k8s" "github.com/GreptimeTeam/gtctl/pkg/logger" "github.com/GreptimeTeam/gtctl/pkg/status" @@ -80,6 +83,9 @@ func NewCreateClusterCommand(l logger.Logger) *cobra.Command { ctx = context.TODO() ) + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer stop() + clusterDeployer, err := newDeployer(l, clusterName, &options) if err != nil { return err @@ -127,7 +133,7 @@ func NewCreateClusterCommand(l logger.Logger) *cobra.Command { fmt.Printf("\x1b[32m%s\x1b[0m", fmt.Sprintf("To view dashboard by accessing: %s\n", logger.Bold("http://localhost:4000/dashboard/"))) // Wait for all the child processes to exit. - if err := d.Wait(ctx); err != nil { + if err := d.Wait(ctx, stop); err != nil { return err } } @@ -177,7 +183,7 @@ func newDeployer(l logger.Logger, clusterName string, options *createClusterCliO } if options.Config != "" { - var config baremetal.Config + var config bmconfig.Config data, err := ioutil.ReadFile(options.Config) if err != nil { return nil, err diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index 5ae1735a..eb70a466 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -29,6 +29,7 @@ import ( "runtime" "strings" + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" "github.com/GreptimeTeam/gtctl/pkg/logger" "github.com/GreptimeTeam/gtctl/pkg/utils" ) @@ -82,7 +83,7 @@ func NewArtifactManager(workingDir string, l logger.Logger, alwaysDownload bool) } // BinaryPath returns the path of the binary of the given type and version. -func (am *ArtifactManager) BinaryPath(typ ArtifactType, artifact *Artifact) (string, error) { +func (am *ArtifactManager) BinaryPath(typ ArtifactType, artifact *config.Artifact) (string, error) { if artifact.Local != "" { return artifact.Local, nil } @@ -95,7 +96,7 @@ func (am *ArtifactManager) BinaryPath(typ ArtifactType, artifact *Artifact) (str } // PrepareArtifact will download the artifact from the given URL and uncompressed it. -func (am *ArtifactManager) PrepareArtifact(typ ArtifactType, artifact *Artifact) error { +func (am *ArtifactManager) PrepareArtifact(typ ArtifactType, artifact *config.Artifact) error { // If you use the local artifact, we don't need to download it. if artifact.Local != "" { return nil diff --git a/pkg/deployer/baremetal/component/cluster.go b/pkg/deployer/baremetal/component/cluster.go new file mode 100644 index 00000000..e73c1458 --- /dev/null +++ b/pkg/deployer/baremetal/component/cluster.go @@ -0,0 +1,105 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package component + +import ( + "bufio" + "context" + "os" + "os/exec" + "path" + "strconv" + "sync" + + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" + "github.com/GreptimeTeam/gtctl/pkg/logger" +) + +// BareMetalCluster describes all the components need to be deployed under bare-metal mode. +type BareMetalCluster struct { + MetaSrv BareMetalClusterComponent + DataNodes BareMetalClusterComponent + Frontend BareMetalClusterComponent + Etcd BareMetalClusterComponent +} + +// BareMetalClusterComponent is the basic unit of running GreptimeDB Cluster in bare-metal mode. +type BareMetalClusterComponent interface { + // Start starts cluster component by executing binary. + Start(ctx context.Context, binary string) error + + // BuildArgs build up args for cluster component. + BuildArgs(ctx context.Context, params ...interface{}) []string + + // IsRunning returns the status of current cluster component. + IsRunning(ctx context.Context) bool + + // Delete deletes resources that allocated in the system for current component. + Delete(ctx context.Context) error +} + +func NewGreptimeDBCluster(config *config.Cluster, dataDir, logsDir, pidsDir string, + wait *sync.WaitGroup, logger logger.Logger) *BareMetalCluster { + return &BareMetalCluster{ + MetaSrv: newMetaSrv(config.Meta, logsDir, pidsDir, wait, logger), + DataNodes: newDataNodes(config.Datanode, config.Meta.ServerAddr, dataDir, logsDir, pidsDir, wait, logger), + Frontend: newFrontend(config.Frontend, config.Meta.ServerAddr, logsDir, pidsDir, wait, logger), + Etcd: newEtcd(dataDir, logsDir, pidsDir, wait, logger), + } +} + +func runBinary(ctx context.Context, binary string, args []string, logDir string, pidDir string, + wait *sync.WaitGroup, logger logger.Logger) error { + cmd := exec.Command(binary, args...) + + // output to binary. + logFile := path.Join(logDir, "log") + outputFile, err := os.Create(logFile) + if err != nil { + return err + } + + outputFileWriter := bufio.NewWriter(outputFile) + cmd.Stdout = outputFileWriter + cmd.Stderr = outputFileWriter + + logger.V(3).Infof("run binary '%s' with args: '%v', log: '%s', pid: '%s'", binary, args, logDir, pidDir) + + if err := cmd.Start(); err != nil { + return err + } + + pidFile := path.Join(pidDir, "pid") + f, err := os.Create(pidFile) + if err != nil { + return err + } + + _, err = f.Write([]byte(strconv.Itoa(cmd.Process.Pid))) + if err != nil { + return err + } + + go func() { + defer wait.Done() + wait.Add(1) + // TODO(sh2) caught up the `signal: interrupt` error and ignore + if err := cmd.Wait(); err != nil { + logger.Errorf("binary '%s' exited with error: %v", binary, err) + } + }() + + return nil +} diff --git a/pkg/deployer/baremetal/component/datanode.go b/pkg/deployer/baremetal/component/datanode.go new file mode 100644 index 00000000..df8d0083 --- /dev/null +++ b/pkg/deployer/baremetal/component/datanode.go @@ -0,0 +1,190 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package component + +import ( + "context" + "fmt" + "net" + "net/http" + "path" + "strconv" + "sync" + "time" + + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" + "github.com/GreptimeTeam/gtctl/pkg/logger" + "github.com/GreptimeTeam/gtctl/pkg/utils" +) + +type dataNodes struct { + config *config.Datanode + metaSrvAddr string + + dataDir string + logsDir string + pidsDir string + wait *sync.WaitGroup + logger logger.Logger + + dataHome string + dataNodeLogDirs []string + dataNodePidDirs []string + dataNodeDataDirs []string +} + +func newDataNodes(config *config.Datanode, metaSrvAddr, dataDir, logsDir, pidsDir string, + wait *sync.WaitGroup, logger logger.Logger) BareMetalClusterComponent { + return &dataNodes{ + config: config, + metaSrvAddr: metaSrvAddr, + dataDir: dataDir, + logsDir: logsDir, + pidsDir: pidsDir, + wait: wait, + logger: logger, + } +} + +func (d *dataNodes) Start(ctx context.Context, binary string) error { + dataHome := path.Join(d.dataDir, "home") + if err := utils.CreateDirIfNotExists(dataHome); err != nil { + return err + } + d.dataHome = dataHome + + for i := 0; i < d.config.Replicas; i++ { + dirName := fmt.Sprintf("datanode.%d", i) + + datanodeLogDir := path.Join(d.logsDir, dirName) + if err := utils.CreateDirIfNotExists(datanodeLogDir); err != nil { + return err + } + d.dataNodeLogDirs = append(d.dataNodeLogDirs, datanodeLogDir) + + datanodePidDir := path.Join(d.pidsDir, dirName) + if err := utils.CreateDirIfNotExists(datanodePidDir); err != nil { + return err + } + d.dataNodePidDirs = append(d.dataNodePidDirs, datanodePidDir) + + walDir := path.Join(d.dataDir, dirName, "wal") + if err := utils.CreateDirIfNotExists(walDir); err != nil { + return err + } + d.dataNodeDataDirs = append(d.dataNodeDataDirs, path.Join(d.dataDir, dirName)) + + if err := runBinary(ctx, binary, d.BuildArgs(ctx, i, walDir), datanodeLogDir, datanodePidDir, d.wait, d.logger); err != nil { + return err + } + } + + // FIXME(zyy17): Should add a timeout here. + ticker := time.Tick(500 * time.Millisecond) + +TICKER: + for { + select { + case <-ticker: + if d.IsRunning(ctx) { + break TICKER + } + } + } + + return nil +} + +func (d *dataNodes) BuildArgs(ctx context.Context, params ...interface{}) []string { + logLevel := d.config.LogLevel + if logLevel == "" { + logLevel = "info" + } + + nodeID_, walDir := params[0], params[1] + nodeID := nodeID_.(int) + + args := []string{ + fmt.Sprintf("--log-level=%s", logLevel), + "datanode", "start", + fmt.Sprintf("--node-id=%d", nodeID), + fmt.Sprintf("--metasrv-addr=%s", d.metaSrvAddr), + fmt.Sprintf("--rpc-addr=%s", generateDatanodeAddr(d.config.RPCAddr, nodeID)), + fmt.Sprintf("--http-addr=%s", generateDatanodeAddr(d.config.HTTPAddr, nodeID)), + fmt.Sprintf("--data-home=%s", d.dataHome), + fmt.Sprintf("--wal-dir=%s", walDir), + } + return args +} + +func (d *dataNodes) IsRunning(ctx context.Context) bool { + for i := 0; i < d.config.Replicas; i++ { + addr := generateDatanodeAddr(d.config.HTTPAddr, i) + _, httpPort, err := net.SplitHostPort(addr) + if err != nil { + d.logger.V(5).Infof("failed to split host port: %s", err) + return false + } + + rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) + if err != nil { + d.logger.V(5).Infof("failed to get datanode health: %s", err) + return false + } + + if rsp.StatusCode != http.StatusOK { + return false + } + + if err = rsp.Body.Close(); err != nil { + return false + } + } + + return true +} + +func (d *dataNodes) Delete(ctx context.Context) error { + if err := utils.DeleteDirIfExists(d.dataHome); err != nil { + return err + } + + for _, dir := range d.dataNodeLogDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + + for _, dir := range d.dataNodePidDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + + for _, dir := range d.dataNodeDataDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + + return nil +} + +func generateDatanodeAddr(addr string, nodeID int) string { + // Already checked in validation. + host, port, _ := net.SplitHostPort(addr) + portInt, _ := strconv.Atoi(port) + return net.JoinHostPort(host, strconv.Itoa(portInt+nodeID)) +} diff --git a/pkg/deployer/baremetal/component/etcd.go b/pkg/deployer/baremetal/component/etcd.go new file mode 100644 index 00000000..72d0b58f --- /dev/null +++ b/pkg/deployer/baremetal/component/etcd.go @@ -0,0 +1,84 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package component + +import ( + "context" + "path" + "sync" + + "github.com/GreptimeTeam/gtctl/pkg/logger" + "github.com/GreptimeTeam/gtctl/pkg/utils" +) + +type etcd struct { + dataDir string + logsDir string + pidsDir string + wait *sync.WaitGroup + logger logger.Logger + + etcdDirs []string +} + +func newEtcd(dataDir, logsDir, pidsDir string, + wait *sync.WaitGroup, logger logger.Logger) BareMetalClusterComponent { + return &etcd{ + dataDir: dataDir, + logsDir: logsDir, + pidsDir: pidsDir, + wait: wait, + logger: logger, + } +} + +func (e *etcd) Start(ctx context.Context, binary string) error { + var ( + etcdDataDir = path.Join(e.dataDir, "etcd") + etcdLogDir = path.Join(e.logsDir, "etcd") + etcdPidDir = path.Join(e.pidsDir, "etcd") + etcdDirs = []string{etcdDataDir, etcdLogDir, etcdPidDir} + ) + for _, dir := range etcdDirs { + if err := utils.CreateDirIfNotExists(dir); err != nil { + return err + } + } + e.etcdDirs = etcdDirs + + if err := runBinary(ctx, binary, e.BuildArgs(ctx, etcdDataDir), etcdLogDir, etcdPidDir, e.wait, e.logger); err != nil { + return err + } + + return nil +} + +func (e *etcd) BuildArgs(ctx context.Context, params ...interface{}) []string { + return []string{"--data-dir", params[0].(string)} +} + +func (e *etcd) IsRunning(ctx context.Context) bool { + // Have not implemented the healthy checker now. + return false +} + +func (e *etcd) Delete(ctx context.Context) error { + for _, dir := range e.etcdDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + return nil +} diff --git a/pkg/deployer/baremetal/component/frontend.go b/pkg/deployer/baremetal/component/frontend.go new file mode 100644 index 00000000..30ef2399 --- /dev/null +++ b/pkg/deployer/baremetal/component/frontend.go @@ -0,0 +1,98 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package component + +import ( + "context" + "fmt" + "path" + "sync" + + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" + "github.com/GreptimeTeam/gtctl/pkg/logger" + "github.com/GreptimeTeam/gtctl/pkg/utils" +) + +type frontend struct { + config *config.Frontend + metaSrvAddr string + + logsDir string + pidsDir string + wait *sync.WaitGroup + logger logger.Logger + + frontendDirs []string +} + +func newFrontend(config *config.Frontend, metaSrvAddr, logsDir, pidsDir string, + wait *sync.WaitGroup, logger logger.Logger) BareMetalClusterComponent { + return &frontend{ + config: config, + metaSrvAddr: metaSrvAddr, + logsDir: logsDir, + pidsDir: pidsDir, + wait: wait, + logger: logger, + } +} + +func (f *frontend) Start(ctx context.Context, binary string) error { + var ( + frontendLogDir = path.Join(f.logsDir, "frontend") + frontendPidDir = path.Join(f.pidsDir, "frontend") + frontendDirs = []string{frontendLogDir, frontendPidDir} + ) + for _, dir := range frontendDirs { + if err := utils.CreateDirIfNotExists(dir); err != nil { + return err + } + } + f.frontendDirs = frontendDirs + + if err := runBinary(ctx, binary, f.BuildArgs(ctx), frontendLogDir, frontendPidDir, f.wait, f.logger); err != nil { + return err + } + + return nil +} + +func (f *frontend) BuildArgs(ctx context.Context, params ...interface{}) []string { + logLevel := f.config.LogLevel + if logLevel == "" { + logLevel = "info" + } + args := []string{ + fmt.Sprintf("--log-level=%s", logLevel), + "frontend", "start", + fmt.Sprintf("--metasrv-addr=%s", f.metaSrvAddr), + } + return args +} + +func (f *frontend) IsRunning(ctx context.Context) bool { + // Have not implemented the healthy checker now. + return false +} + +func (f *frontend) Delete(ctx context.Context) error { + for _, dir := range f.frontendDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + f.frontendDirs = nil + return nil +} diff --git a/pkg/deployer/baremetal/component/meta.go b/pkg/deployer/baremetal/component/meta.go new file mode 100644 index 00000000..295da570 --- /dev/null +++ b/pkg/deployer/baremetal/component/meta.go @@ -0,0 +1,125 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package component + +import ( + "context" + "fmt" + "net" + "net/http" + "path" + "sync" + "time" + + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" + "github.com/GreptimeTeam/gtctl/pkg/logger" + "github.com/GreptimeTeam/gtctl/pkg/utils" +) + +type metaSrv struct { + config *config.Meta + + logsDir string + pidsDir string + wait *sync.WaitGroup + logger logger.Logger + + metaSrvDirs []string +} + +func newMetaSrv(config *config.Meta, logsDir, pidsDir string, + wait *sync.WaitGroup, logger logger.Logger) BareMetalClusterComponent { + return &metaSrv{ + config: config, + logsDir: logsDir, + pidsDir: pidsDir, + wait: wait, + logger: logger, + } +} + +func (m *metaSrv) Start(ctx context.Context, binary string) error { + var ( + metaSrvLogDir = path.Join(m.logsDir, "metasrv") + metaSrvPidDir = path.Join(m.pidsDir, "metasrv") + metaSrvDirs = []string{metaSrvLogDir, metaSrvPidDir} + ) + for _, dir := range metaSrvDirs { + if err := utils.CreateDirIfNotExists(dir); err != nil { + return err + } + } + m.metaSrvDirs = metaSrvDirs + + if err := runBinary(ctx, binary, m.BuildArgs(ctx), metaSrvLogDir, metaSrvPidDir, m.wait, m.logger); err != nil { + return err + } + + // FIXME(zyy17): Should add a timeout here. + ticker := time.Tick(500 * time.Millisecond) + +TICKER: + for { + select { + case <-ticker: + if m.IsRunning(ctx) { + break TICKER + } + } + } + + return nil +} + +func (m *metaSrv) BuildArgs(ctx context.Context, params ...interface{}) []string { + logLevel := m.config.LogLevel + if logLevel == "" { + logLevel = "info" + } + args := []string{ + fmt.Sprintf("--log-level=%s", logLevel), + "metasrv", "start", + "--store-addr", m.config.StoreAddr, + "--server-addr", m.config.ServerAddr, + "--http-addr", m.config.HTTPAddr, + } + return args +} + +func (m *metaSrv) IsRunning(ctx context.Context) bool { + _, httpPort, err := net.SplitHostPort(m.config.HTTPAddr) + if err != nil { + m.logger.V(5).Infof("failed to split host port: %s", err) + return false + } + + rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) + if err != nil { + m.logger.V(5).Infof("failed to get metasrv health: %s", err) + return false + } + defer rsp.Body.Close() + + return rsp.StatusCode == http.StatusOK +} + +func (m *metaSrv) Delete(ctx context.Context) error { + for _, dir := range m.metaSrvDirs { + if err := utils.DeleteDirIfExists(dir); err != nil { + return err + } + } + return nil +} diff --git a/pkg/deployer/baremetal/config.go b/pkg/deployer/baremetal/config.go deleted file mode 100644 index f05e0ece..00000000 --- a/pkg/deployer/baremetal/config.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2023 Greptime Team -// -// 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, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package baremetal - -import ( - "fmt" - - "github.com/go-playground/validator/v10" -) - -var validate *validator.Validate - -// Config is the desired state of a GreptimeDB cluster on bare metal. -// -// The field of Config that with `validate` tag will be validated -// against its requirement. Each filed has only one requirement. -// -// Each field of Config can also have its own exported method `Validate`. -type Config struct { - Cluster *Cluster `yaml:"cluster" validate:"required"` - Etcd *Etcd `yaml:"etcd" validate:"required"` -} - -type Cluster struct { - Artifact *Artifact `yaml:"artifact" validate:"required"` - Frontend *Frontend `yaml:"frontend" validate:"required"` - Meta *Meta `yaml:"meta" validate:"required"` - Datanode *Datanode `yaml:"datanode" validate:"required"` -} - -type Frontend struct { - GRPCAddr string `yaml:"grpcAddr" validate:"omitempty,hostname_port"` - HTTPAddr string `yaml:"httpAddr" validate:"omitempty,hostname_port"` - PostgresAddr string `yaml:"postgresAddr" validate:"omitempty,hostname_port"` - MetaAddr string `yaml:"metaAddr" validate:"omitempty,hostname_port"` - - LogLevel string `yaml:"logLevel"` -} - -type Datanode struct { - Replicas int `yaml:"replicas" validate:"gt=0"` - NodeID int `yaml:"nodeID" validate:"gte=0"` - - RPCAddr string `yaml:"rpcAddr" validate:"required,hostname_port"` - HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` - - DataDir string `yaml:"dataDir" validate:"omitempty,dirpath"` - WalDir string `yaml:"walDir" validate:"omitempty,dirpath"` - ProcedureDir string `yaml:"procedureDir" validate:"omitempty,dirpath"` - - LogLevel string `yaml:"logLevel"` -} - -type Meta struct { - StoreAddr string `yaml:"storeAddr" validate:"hostname_port"` - ServerAddr string `yaml:"serverAddr" validate:"hostname_port"` - BindAddr string `yaml:"bindAddr" validate:"omitempty,hostname_port"` - HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` - - LogLevel string `yaml:"logLevel"` -} - -type Etcd struct { - Artifact *Artifact `yaml:"artifact" validate:"required"` -} - -type Artifact struct { - // Local is the local path of binary(greptime or etcd). - Local string `yaml:"local" validate:"omitempty,file"` - - // Version is the release version of binary(greptime or etcd). - // Usually, it points to the version of binary of GitHub release. - Version string `yaml:"version"` -} - -// ValidateConfig validate config in bare-metal mode. -func ValidateConfig(config *Config) error { - if config == nil { - return fmt.Errorf("no config to validate") - } - - validate = validator.New() - - // Register custom validation method for Artifact. - validate.RegisterStructValidation(ValidateArtifact, Artifact{}) - - err := validate.Struct(config) - if err != nil { - return err - } - - return nil -} - -func ValidateArtifact(sl validator.StructLevel) { - artifact := sl.Current().Interface().(Artifact) - if len(artifact.Version) == 0 && len(artifact.Local) == 0 { - sl.ReportError(sl.Current().Interface(), "Artifact", "Version/Local", "", "") - } -} - -func defaultConfig() *Config { - return &Config{ - Cluster: &Cluster{ - Artifact: &Artifact{ - Version: DefaultGreptimeVersion, - }, - Frontend: &Frontend{}, - Meta: &Meta{ - StoreAddr: "127.0.0.1:2379", - ServerAddr: "0.0.0.0:3002", - HTTPAddr: "0.0.0.0:14001", - }, - Datanode: &Datanode{ - Replicas: 3, - RPCAddr: "0.0.0.0:14100", - HTTPAddr: "0.0.0.0:14300", - }, - }, - Etcd: &Etcd{ - Artifact: &Artifact{ - Version: DefaultEtcdVersion, - }, - }, - } -} diff --git a/pkg/deployer/baremetal/config/common.go b/pkg/deployer/baremetal/config/common.go new file mode 100644 index 00000000..96e08868 --- /dev/null +++ b/pkg/deployer/baremetal/config/common.go @@ -0,0 +1,68 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +// Config is the desired state of a GreptimeDB cluster on bare metal. +// +// The field of Config that with `validate` tag will be validated +// against its requirement. Each filed has only one requirement. +// +// Each field of Config can also have its own exported method `Validate`. +type Config struct { + Cluster *Cluster `yaml:"cluster" validate:"required"` + Etcd *Etcd `yaml:"etcd" validate:"required"` +} + +type Cluster struct { + Artifact *Artifact `yaml:"artifact" validate:"required"` + Frontend *Frontend `yaml:"frontend" validate:"required"` + Meta *Meta `yaml:"meta" validate:"required"` + Datanode *Datanode `yaml:"datanode" validate:"required"` +} + +type Artifact struct { + // Local is the local path of binary(greptime or etcd). + Local string `yaml:"local" validate:"omitempty,file"` + + // Version is the release version of binary(greptime or etcd). + // Usually, it points to the version of binary of GitHub release. + Version string `yaml:"version"` +} + +func DefaultConfig() *Config { + return &Config{ + Cluster: &Cluster{ + Artifact: &Artifact{ + Version: DefaultGreptimeVersion, + }, + Frontend: &Frontend{}, + Meta: &Meta{ + StoreAddr: "127.0.0.1:2379", + ServerAddr: "0.0.0.0:3002", + HTTPAddr: "0.0.0.0:14001", + }, + Datanode: &Datanode{ + Replicas: 3, + RPCAddr: "0.0.0.0:14100", + HTTPAddr: "0.0.0.0:14300", + }, + }, + Etcd: &Etcd{ + Artifact: &Artifact{ + Version: DefaultEtcdVersion, + }, + }, + } +} diff --git a/pkg/deployer/baremetal/constants.go b/pkg/deployer/baremetal/config/constants.go similarity index 97% rename from pkg/deployer/baremetal/constants.go rename to pkg/deployer/baremetal/config/constants.go index a680d5f1..e351c1c3 100644 --- a/pkg/deployer/baremetal/constants.go +++ b/pkg/deployer/baremetal/config/constants.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package baremetal +package config const ( // GtctlDir is the root directory that contains states of cluster info. diff --git a/pkg/deployer/baremetal/config/datanode.go b/pkg/deployer/baremetal/config/datanode.go new file mode 100644 index 00000000..755cfda1 --- /dev/null +++ b/pkg/deployer/baremetal/config/datanode.go @@ -0,0 +1,29 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +type Datanode struct { + Replicas int `yaml:"replicas" validate:"gt=0"` + NodeID int `yaml:"nodeID" validate:"gte=0"` + + RPCAddr string `yaml:"rpcAddr" validate:"required,hostname_port"` + HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` + + DataDir string `yaml:"dataDir" validate:"omitempty,dirpath"` + WalDir string `yaml:"walDir" validate:"omitempty,dirpath"` + ProcedureDir string `yaml:"procedureDir" validate:"omitempty,dirpath"` + + LogLevel string `yaml:"logLevel"` +} diff --git a/pkg/deployer/baremetal/config/etcd.go b/pkg/deployer/baremetal/config/etcd.go new file mode 100644 index 00000000..762bd48c --- /dev/null +++ b/pkg/deployer/baremetal/config/etcd.go @@ -0,0 +1,19 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +type Etcd struct { + Artifact *Artifact `yaml:"artifact" validate:"required"` +} diff --git a/pkg/deployer/baremetal/config/frontend.go b/pkg/deployer/baremetal/config/frontend.go new file mode 100644 index 00000000..7db3bee6 --- /dev/null +++ b/pkg/deployer/baremetal/config/frontend.go @@ -0,0 +1,24 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +type Frontend struct { + GRPCAddr string `yaml:"grpcAddr" validate:"omitempty,hostname_port"` + HTTPAddr string `yaml:"httpAddr" validate:"omitempty,hostname_port"` + PostgresAddr string `yaml:"postgresAddr" validate:"omitempty,hostname_port"` + MetaAddr string `yaml:"metaAddr" validate:"omitempty,hostname_port"` + + LogLevel string `yaml:"logLevel"` +} diff --git a/pkg/deployer/baremetal/config/meta.go b/pkg/deployer/baremetal/config/meta.go new file mode 100644 index 00000000..b6bfe9d6 --- /dev/null +++ b/pkg/deployer/baremetal/config/meta.go @@ -0,0 +1,24 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +type Meta struct { + StoreAddr string `yaml:"storeAddr" validate:"hostname_port"` + ServerAddr string `yaml:"serverAddr" validate:"hostname_port"` + BindAddr string `yaml:"bindAddr" validate:"omitempty,hostname_port"` + HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` + + LogLevel string `yaml:"logLevel"` +} diff --git a/pkg/deployer/baremetal/deployer.go b/pkg/deployer/baremetal/deployer.go index dae1b01c..094705e1 100644 --- a/pkg/deployer/baremetal/deployer.go +++ b/pkg/deployer/baremetal/deployer.go @@ -15,31 +15,31 @@ package baremetal import ( - "bufio" "context" "fmt" - "net" - "net/http" "os" "os/exec" "path" - "strconv" "strings" "sync" "time" . "github.com/GreptimeTeam/gtctl/pkg/deployer" + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/component" + "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" "github.com/GreptimeTeam/gtctl/pkg/logger" "github.com/GreptimeTeam/gtctl/pkg/utils" ) type Deployer struct { logger logger.Logger - config *Config + config *config.Config am *ArtifactManager wg sync.WaitGroup + bm *component.BareMetalCluster workingDir string + clusterDir string logsDir string pidsDir string dataDir string @@ -54,7 +54,7 @@ type Option func(*Deployer) func NewDeployer(l logger.Logger, clusterName string, opts ...Option) (Interface, error) { d := &Deployer{ logger: l, - config: defaultConfig(), + config: config.DefaultConfig(), } for _, opt := range opts { @@ -71,7 +71,7 @@ func NewDeployer(l logger.Logger, clusterName string, opts ...Option) (Interface if err != nil { return nil, err } - d.workingDir = path.Join(homeDir, GtctlDir) + d.workingDir = path.Join(homeDir, config.GtctlDir) if err := utils.CreateDirIfNotExists(d.workingDir); err != nil { return nil, err } @@ -86,10 +86,48 @@ func NewDeployer(l logger.Logger, clusterName string, opts ...Option) (Interface return nil, err } + d.bm = component.NewGreptimeDBCluster(d.config.Cluster, d.dataDir, d.logsDir, d.pidsDir, &d.wg, d.logger) + return d, nil } -func WithConfig(config *Config) Option { +func (d *Deployer) createClusterDirs(clusterName string) error { + var ( + // ${HOME}/${GtctlDir}/${ClusterName} + clusterDir = path.Join(d.workingDir, clusterName) + + // ${HOME}/${GtctlDir}/${ClusterName}/logs. + logsDir = path.Join(clusterDir, "logs") + + // ${HOME}/${GtctlDir}/${ClusterName}/data. + dataDir = path.Join(clusterDir, "data") + + // ${HOME}/${GtctlDir}/${ClusterName}/pids. + pidsDir = path.Join(clusterDir, "pids") + ) + + dirs := []string{ + clusterDir, + logsDir, + dataDir, + pidsDir, + } + + for _, dir := range dirs { + if err := utils.CreateDirIfNotExists(dir); err != nil { + return err + } + } + + d.clusterDir = clusterDir + d.logsDir = logsDir + d.dataDir = dataDir + d.pidsDir = pidsDir + + return nil +} + +func WithConfig(config *config.Config) Option { // TODO(zyy17): Should merge the default configuration. return func(d *Deployer) { d.config = config @@ -126,15 +164,15 @@ func (d *Deployer) CreateGreptimeDBCluster(ctx context.Context, clusterName stri return err } - if err := d.startMetasrv(binary); err != nil { + if err := d.bm.MetaSrv.Start(ctx, binary); err != nil { return err } - if err := d.startDatanodes(binary, d.config.Cluster.Datanode.Replicas); err != nil { + if err := d.bm.DataNodes.Start(ctx, binary); err != nil { return err } - if err := d.startFrontend(binary); err != nil { + if err := d.bm.Frontend.Start(ctx, binary); err != nil { return err } @@ -149,6 +187,18 @@ func (d *Deployer) DeleteGreptimeDBCluster(ctx context.Context, name string, opt return fmt.Errorf("unsupported operation") } +// deleteGreptimeDBClusterForeground delete the whole cluster if it runs in foreground. +func (d *Deployer) deleteGreptimeDBClusterForeground(ctx context.Context) error { + // It is really unnecessary to delete each components resources in the cluster + // since it is running in the foreground. + // So deleting the whole cluster resources will be fine. + if err := utils.DeleteDirIfExists(d.clusterDir); err != nil { + return err + } + + return nil +} + func (d *Deployer) CreateEtcdCluster(ctx context.Context, clusterName string, options *CreateEtcdClusterOptions) error { if err := d.am.PrepareArtifact(EtcdArtifactType, d.config.Etcd.Artifact); err != nil { return err @@ -159,19 +209,7 @@ func (d *Deployer) CreateEtcdCluster(ctx context.Context, clusterName string, op return err } - var ( - etcdDataDir = path.Join(d.dataDir, "etcd") - etcdLogDir = path.Join(d.logsDir, "etcd") - etcdPidDir = path.Join(d.pidsDir, "etcd") - etcdDirs = []string{etcdDataDir, etcdLogDir, etcdPidDir} - ) - for _, dir := range etcdDirs { - if err := utils.CreateDirIfNotExists(dir); err != nil { - return err - } - } - - if err := d.runBinary(bin, d.buildEtcdArgs(etcdDataDir), etcdLogDir, etcdPidDir); err != nil { + if err = d.bm.Etcd.Start(ctx, bin); err != nil { return err } @@ -228,7 +266,7 @@ func (d *Deployer) checkEtcdHealth(etcdBin string) error { time.Sleep(1 * time.Second) } - return fmt.Errorf("Etcd is not ready in 10 second! You can find its logs in %s", path.Join(d.logsDir, "etcd")) + return fmt.Errorf("etcd is not ready in 10 second! You can find its logs in %s", path.Join(d.logsDir, "etcd")) } func (d *Deployer) DeleteEtcdCluster(ctx context.Context, name string, options *DeleteEtcdClusterOption) error { @@ -240,270 +278,23 @@ func (d *Deployer) CreateGreptimeDBOperator(ctx context.Context, name string, op return fmt.Errorf("only support for k8s Deployer") } -func (d *Deployer) Wait(ctx context.Context) error { +func (d *Deployer) Wait(ctx context.Context, stop context.CancelFunc) error { d.wg.Wait() - return nil -} - -func (d *Deployer) Config() *Config { - return d.config -} - -func (d *Deployer) createClusterDirs(clusterName string) error { - var ( - // ${HOME}/${GtctlDir}/${ClusterName}/logs. - logsDir = path.Join(d.workingDir, clusterName, "logs") - - // ${HOME}/${GtctlDir}/${ClusterName}/data. - dataDir = path.Join(d.workingDir, clusterName, "data") - - // ${HOME}/${GtctlDir}/${ClusterName}/pids. - pidsDir = path.Join(d.workingDir, clusterName, "pids") - ) - - dirs := []string{ - // ${HOME}/${GtctlDir}/${ClusterName}. - path.Join(d.workingDir, clusterName), - - logsDir, - dataDir, - pidsDir, - } - - for _, dir := range dirs { - if err := utils.CreateDirIfNotExists(dir); err != nil { - return err - } - } - - d.logsDir = logsDir - d.dataDir = dataDir - d.pidsDir = pidsDir - - return nil -} - -func (d *Deployer) runBinary(binary string, args []string, logDir string, pidDir string) error { - cmd := exec.Command(binary, args...) - - // output to binary. - logFile := path.Join(logDir, "log") - outputFile, err := os.Create(logFile) - if err != nil { - return err - } - - outputFileWriter := bufio.NewWriter(outputFile) - cmd.Stdout = outputFileWriter - cmd.Stderr = outputFileWriter - - d.logger.V(3).Infof("run binary '%s' with args: '%v', log: '%s', pid: '%s'", binary, args, logDir, pidDir) - - if err := cmd.Start(); err != nil { - return err - } - - pidFile := path.Join(pidDir, "pid") - f, err := os.Create(pidFile) - if err != nil { - return err - } - - _, err = f.Write([]byte(strconv.Itoa(cmd.Process.Pid))) - if err != nil { - return err - } - go func() { - defer d.wg.Done() - d.wg.Add(1) - if err := cmd.Wait(); err != nil { - d.logger.Errorf("binary '%s' exited with error: %v", binary, err) - } - }() - - return nil -} + d.logger.V(3).Info("Cluster shutting down. Cleaning allocated resources.") -func (d *Deployer) startMetasrv(binary string) error { - var ( - metasrvLogDir = path.Join(d.logsDir, "metasrv") - metasrvPidDir = path.Join(d.pidsDir, "metasrv") - metasrvDirs = []string{metasrvLogDir, metasrvPidDir} - ) - for _, dir := range metasrvDirs { - if err := utils.CreateDirIfNotExists(dir); err != nil { + select { + case <-ctx.Done(): + stop() + // Delete cluster after closing, which can only happens in the foreground. + if err := d.deleteGreptimeDBClusterForeground(ctx); err != nil { return err } } - if err := d.runBinary(binary, d.buildMetasrvArgs(), metasrvLogDir, metasrvPidDir); err != nil { - return err - } - - // FIXME(zyy17): Should add a timeout here. - for { - if d.isMetasrvRunning() { - break - } - } - return nil } -func (d *Deployer) isMetasrvRunning() bool { - _, httpPort, err := net.SplitHostPort(d.config.Cluster.Meta.HTTPAddr) - if err != nil { - d.logger.V(5).Infof("failed to split host port: %s", err) - return false - } - - rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) - if err != nil { - d.logger.V(5).Infof("failed to get metasrv health: %s", err) - return false - } - defer rsp.Body.Close() - - return rsp.StatusCode == http.StatusOK -} - -func (d *Deployer) startDatanodes(binary string, datanodeNum int) error { - dataHome := path.Join(d.dataDir, "home") - if err := utils.CreateDirIfNotExists(dataHome); err != nil { - return err - } - - for i := 0; i < datanodeNum; i++ { - dirName := fmt.Sprintf("datanode.%d", i) - - datanodeLogDir := path.Join(d.logsDir, dirName) - if err := utils.CreateDirIfNotExists(datanodeLogDir); err != nil { - return err - } - - datanodePidDir := path.Join(d.pidsDir, dirName) - if err := utils.CreateDirIfNotExists(datanodePidDir); err != nil { - return err - } - - walDir := path.Join(d.dataDir, dirName, "wal") - if err := utils.CreateDirIfNotExists(walDir); err != nil { - return err - } - - if err := d.runBinary(binary, d.buildDatanodeArgs(i, dataHome, walDir), datanodeLogDir, datanodePidDir); err != nil { - return err - } - } - - // FIXME(zyy17): Should add a timeout here. - for { - if d.isDatanodesRunning() { - break - } - } - - return nil -} - -func (d *Deployer) isDatanodesRunning() bool { - for i := 0; i < d.config.Cluster.Datanode.Replicas; i++ { - addr := d.generateDatanodeAddr(d.config.Cluster.Datanode.HTTPAddr, i) - _, httpPort, err := net.SplitHostPort(addr) - if err != nil { - d.logger.V(5).Infof("failed to split host port: %s", err) - return false - } - - rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) - if err != nil { - d.logger.V(5).Infof("failed to get datanode health: %s", err) - return false - } - defer rsp.Body.Close() - - if rsp.StatusCode != http.StatusOK { - return false - } - } - - return true -} - -func (d *Deployer) startFrontend(binary string) error { - var ( - frontendLogDir = path.Join(d.logsDir, "frontend") - frontendPidDir = path.Join(d.pidsDir, "frontend") - frontendDirs = []string{frontendLogDir, frontendPidDir} - ) - for _, dir := range frontendDirs { - if err := utils.CreateDirIfNotExists(dir); err != nil { - return err - } - } - - if err := d.runBinary(binary, d.buildFrontendArgs(), frontendLogDir, frontendPidDir); err != nil { - return err - } - - return nil -} - -func (d *Deployer) buildEtcdArgs(dataDir string) []string { - return []string{"--data-dir", dataDir} -} - -func (d *Deployer) buildMetasrvArgs() []string { - logLevel := d.config.Cluster.Meta.LogLevel - if logLevel == "" { - logLevel = "info" - } - args := []string{ - fmt.Sprintf("--log-level=%s", logLevel), - "metasrv", "start", - "--store-addr", d.config.Cluster.Meta.StoreAddr, - "--server-addr", d.config.Cluster.Meta.ServerAddr, - "--http-addr", d.config.Cluster.Meta.HTTPAddr, - } - return args -} - -func (d *Deployer) buildDatanodeArgs(nodeID int, dataHome string, walDir string) []string { - logLevel := d.config.Cluster.Datanode.LogLevel - if logLevel == "" { - logLevel = "info" - } - args := []string{ - fmt.Sprintf("--log-level=%s", logLevel), - "datanode", "start", - fmt.Sprintf("--node-id=%d", nodeID), - fmt.Sprintf("--metasrv-addr=%s", d.config.Cluster.Meta.ServerAddr), - fmt.Sprintf("--rpc-addr=%s", d.generateDatanodeAddr(d.config.Cluster.Datanode.RPCAddr, nodeID)), - fmt.Sprintf("--http-addr=%s", d.generateDatanodeAddr(d.config.Cluster.Datanode.HTTPAddr, nodeID)), - fmt.Sprintf("--data-home=%s", dataHome), - fmt.Sprintf("--wal-dir=%s", walDir), - } - return args -} - -func (d *Deployer) buildFrontendArgs() []string { - logLevel := d.config.Cluster.Frontend.LogLevel - if logLevel == "" { - logLevel = "info" - } - args := []string{ - fmt.Sprintf("--log-level=%s", logLevel), - "frontend", "start", - fmt.Sprintf("--metasrv-addr=%s", d.config.Cluster.Meta.ServerAddr), - } - return args -} - -// TODO(zyy17): We can support port range in the future. -func (d *Deployer) generateDatanodeAddr(addr string, nodeID int) string { - // Already checked in validation. - host, port, _ := net.SplitHostPort(addr) - portInt, _ := strconv.Atoi(port) - return net.JoinHostPort(host, strconv.Itoa(portInt+nodeID)) +func (d *Deployer) Config() *config.Config { + return d.config } diff --git a/pkg/deployer/baremetal/validate.go b/pkg/deployer/baremetal/validate.go new file mode 100644 index 00000000..8a5dd657 --- /dev/null +++ b/pkg/deployer/baremetal/validate.go @@ -0,0 +1,51 @@ +// Copyright 2023 Greptime Team +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package baremetal + +import ( + "fmt" + + "github.com/go-playground/validator/v10" + + bmconfig "github.com/GreptimeTeam/gtctl/pkg/deployer/baremetal/config" +) + +var validate *validator.Validate + +// ValidateConfig validate config in bare-metal mode. +func ValidateConfig(config *bmconfig.Config) error { + if config == nil { + return fmt.Errorf("no config to validate") + } + + validate = validator.New() + + // Register custom validation method for Artifact. + validate.RegisterStructValidation(ValidateArtifact, bmconfig.Artifact{}) + + err := validate.Struct(config) + if err != nil { + return err + } + + return nil +} + +func ValidateArtifact(sl validator.StructLevel) { + artifact := sl.Current().Interface().(bmconfig.Artifact) + if len(artifact.Version) == 0 && len(artifact.Local) == 0 { + sl.ReportError(sl.Current().Interface(), "Artifact", "Version/Local", "", "") + } +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index f34de9a9..d5d28b69 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -26,6 +26,13 @@ func CreateDirIfNotExists(dir string) (err error) { return nil } +func DeleteDirIfExists(dir string) (err error) { + if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + func IsFileExists(filepath string) (bool, error) { info, err := os.Stat(filepath) if os.IsNotExist(err) {