diff --git a/.golangci.yml b/.golangci.yml index 56368746..2f54be58 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,6 +55,7 @@ linters: - wg sync.WaitGroup - h Host - h os.Host + - h configurer.Host - h mkeconfig.Host - h *mkeconfig.Host - ok bool diff --git a/cmd/common.go b/cmd/common.go index b11955ee..6fee6c1e 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -9,10 +9,10 @@ import ( "github.com/Mirantis/launchpad/pkg/analytics" "github.com/Mirantis/launchpad/pkg/constant" mcclog "github.com/Mirantis/launchpad/pkg/log" + mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/product/mke/phase" "github.com/Mirantis/launchpad/version" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" "github.com/mitchellh/go-homedir" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" @@ -64,7 +64,7 @@ var ( confirmFlag = &cli.BoolFlag{ Name: "confirm", - Usage: "Ask confirmation for all commands", + Usage: "Ask confirmation before running each command on a host", Value: false, } @@ -122,7 +122,6 @@ func initLogger(ctx *cli.Context) error { // stdout hook on by default of course. log.AddHook(mcclog.NewStdoutHook()) - rig.SetLogger(log.StandardLogger()) return nil } @@ -159,8 +158,8 @@ func upgradeCheckResult(ctx *cli.Context) error { } func initExec(ctx *cli.Context) error { - exec.Confirm = ctx.Bool("confirm") - exec.DisableRedact = ctx.Bool("disable-redact") + cmd.DisableRedact = ctx.Bool("disable-redact") + mkeconfig.ConfirmCommands = ctx.Bool("confirm") return nil } diff --git a/cmd/exec.go b/cmd/exec.go index 4f9d56ae..acba2cba 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/Mirantis/launchpad/pkg/config" - "github.com/kballard/go-shellquote" + "github.com/k0sproject/rig/v2/sh/shellescape" "github.com/urfave/cli/v2" ) @@ -63,7 +63,7 @@ func NewExecCommand() *cli.Command { args := ctx.Args().Slice() - err = product.Exec(ctx.StringSlice("target"), ctx.Bool("interactive"), ctx.Bool("first"), ctx.Bool("all"), ctx.Bool("parallel"), ctx.String("role"), ctx.String("os"), shellquote.Join(args...)) + err = product.Exec(ctx.StringSlice("target"), ctx.Bool("interactive"), ctx.Bool("first"), ctx.Bool("all"), ctx.Bool("parallel"), ctx.String("role"), ctx.String("os"), shellescape.Join(args...)) if err != nil { return fmt.Errorf("failed to execute command: %w", err) } diff --git a/go.mod b/go.mod index 19ce067d..09fa9e89 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module github.com/Mirantis/launchpad -go 1.26 +go 1.26.0 require ( - al.essio.dev/pkg/shellescape v1.6.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/a8m/envsubst v1.4.3 github.com/avast/retry-go v3.0.0+incompatible @@ -14,11 +13,9 @@ require ( github.com/gruntwork-io/terratest v0.56.0 github.com/hashicorp/go-version v1.9.0 github.com/k0sproject/dig v0.4.0 - github.com/k0sproject/rig v0.21.2 - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // unmaintained, we should drop it + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect; unmaintained, we should drop it github.com/logrusorgru/aurora/v4 v4.0.0 github.com/mattn/go-isatty v0.0.21 - github.com/mattn/go-shellwords v1.0.13 github.com/mitchellh/go-homedir v1.1.0 github.com/schollz/progressbar/v3 v3.19.0 github.com/segmentio/analytics-go/v3 v3.3.0 @@ -34,10 +31,15 @@ require ( k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 ) +require ( + github.com/k0sproject/rig/v2 v2.1.0 + github.com/samber/slog-logrus/v2 v2.5.4 +) + require ( dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/Azure/go-ntlmssp v0.1.0 // indirect + github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect @@ -46,7 +48,6 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -117,7 +118,6 @@ require ( github.com/jinzhu/copier v0.4.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kevinburke/ssh_config v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -148,6 +148,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/samber/lo v1.53.0 // indirect + github.com/samber/slog-common v0.21.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/segmentio/backo-go v1.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -163,16 +165,16 @@ require ( github.com/zclconf/go-cty v1.18.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.42.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index ae603911..719ae080 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= -al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -10,8 +8,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= -github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= +github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= +github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= @@ -34,8 +32,6 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= @@ -201,8 +197,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -271,12 +265,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/k0sproject/dig v0.4.0 h1:yBxFUUxNXAMGBg6b7c6ypxdx/o3RmhoI5v5ABOw5tn0= github.com/k0sproject/dig v0.4.0/go.mod h1:rlZ7N7ZEcB4Fi96TPXkZ4dqyAiDWOGLapyL9YpZ7Qz4= -github.com/k0sproject/rig v0.21.2 h1:1a4pt5j37wSZ+LH85NqltmBKHIxhZm7RD5jOig8Z+Xs= -github.com/k0sproject/rig v0.21.2/go.mod h1:bdRDs2dJngbYAEWd6+irYlzUqqBeVXVBcSelpaIvuCw= +github.com/k0sproject/rig/v2 v2.1.0 h1:Xt7FtMlyyknnpAVN8HKP4JZmZsDWZz39pw+FrwEcpl4= +github.com/k0sproject/rig/v2 v2.1.0/go.mod h1:4bp1yGRyoANCEe9aFAAJzttFvTRWMPnbleYBQUaY38c= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= -github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -308,8 +300,6 @@ github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLG github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= -github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A= @@ -384,6 +374,12 @@ github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.21.0 h1:Wo2hTly1Br5RjYqX/BTWJJeDnTE85oWk/7vqlpZuAUc= +github.com/samber/slog-common v0.21.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0= +github.com/samber/slog-logrus/v2 v2.5.4 h1:ACS0VWNDJcpFRICkgzRvBAI8ms/LH3S7KrOhAB3SQ0g= +github.com/samber/slog-logrus/v2 v2.5.4/go.mod h1:JBnv/7Gn0ef/iVy2RuRnA2qYIAc0ttlr6/9L/me8jVI= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= @@ -495,25 +491,25 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -522,27 +518,27 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjNe+P2XiTmPsLawi/pCbSPfxt6lTfw= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 9eff832e..2c40b086 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,7 +29,7 @@ import ( "github.com/Mirantis/launchpad/pkg/product" "github.com/Mirantis/launchpad/pkg/product/mke" "github.com/a8m/envsubst" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) @@ -71,7 +71,7 @@ func ProductFromYAML(data []byte) (product.Product, error) { //nolint:ireturn } cfg := string(plain) - if !exec.DisableRedact { + if !cmd.DisableRedact { re := regexp.MustCompile(`(username|password)([:= ]) ?\S+`) cfg = re.ReplaceAllString(cfg, "$1$2[REDACTED]") } diff --git a/pkg/configurer/centos/centos.go b/pkg/configurer/centos/centos.go index 464395d7..ab46776d 100644 --- a/pkg/configurer/centos/centos.go +++ b/pkg/configurer/centos/centos.go @@ -1,9 +1,9 @@ package centos import ( + "github.com/Mirantis/launchpad/pkg/configurer" "github.com/Mirantis/launchpad/pkg/configurer/enterpriselinux" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" ) // Configurer is the CentOS specific implementation of a host configurer. @@ -12,9 +12,9 @@ type Configurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "centos" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "centos" }, func() any { return Configurer{} diff --git a/pkg/configurer/common.go b/pkg/configurer/common.go index acc50ee8..ce76a97b 100644 --- a/pkg/configurer/common.go +++ b/pkg/configurer/common.go @@ -6,8 +6,7 @@ import ( "fmt" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig/log" - "github.com/k0sproject/rig/os" + log "github.com/sirupsen/logrus" ) type rebootable interface { @@ -17,7 +16,7 @@ type rebootable interface { type DockerConfigurer struct{} // GetDockerInfo gets docker info from the host. -func (c DockerConfigurer) GetDockerInfo(h os.Host) (commonconfig.DockerInfo, error) { +func (c DockerConfigurer) GetDockerInfo(h Host) (commonconfig.DockerInfo, error) { command := "docker info --format \"{{json . }}\"" log.Debugf("%s attempting to gather info with `%s`", h, command) info, err := h.ExecOutput(command) diff --git a/pkg/configurer/enterpriselinux/el.go b/pkg/configurer/enterpriselinux/el.go index 9c65f545..34b235c4 100644 --- a/pkg/configurer/enterpriselinux/el.go +++ b/pkg/configurer/enterpriselinux/el.go @@ -2,24 +2,21 @@ package enterpriselinux import ( "fmt" + "io/fs" "strings" "github.com/Mirantis/launchpad/pkg/configurer" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os" - "github.com/k0sproject/rig/os/linux" log "github.com/sirupsen/logrus" ) // Configurer is the EL family specific implementation of a host configurer. type Configurer struct { - linux.EnterpriseLinux configurer.LinuxConfigurer } // PrepareHost prepares the machine host by installing the needed base packages, and fixing any container issues. -func (c Configurer) PrepareHost(h os.Host) error { +func (c Configurer) PrepareHost(h configurer.Host) error { if err := c.InstallPackage(h, "curl", "socat", "iptables", "iputils", "gzip", "openssh"); err != nil { return fmt.Errorf("failed to install base packages: %w", err) } @@ -33,7 +30,7 @@ func (c Configurer) PrepareHost(h os.Host) error { } // InstallMCR install Docker EE engine on Linux. -func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) InstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { ver, verErr := configurer.ResolveLinux(h) if verErr != nil { return fmt.Errorf("could not discover Linux version information") @@ -59,7 +56,7 @@ gpgkey=%s ` elRepo := fmt.Sprintf(elRepoTemplate, baseURL, gpgURL) - if err := c.WriteFile(h, elRepoFilePath, elRepo, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(elRepoFilePath, []byte(elRepo), fs.FileMode(0o600)); err != nil { return fmt.Errorf("could not write Yum repo file for MCR") } @@ -77,7 +74,7 @@ gpgkey=%s } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -95,8 +92,8 @@ func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) return fmt.Errorf("stop containerd: %w", err) } - if err := h.Exec("yum remove -y docker-ee docker-ee-cli", exec.Sudo(h)); err != nil { - return fmt.Errorf("remove docker-ee yum package: %w", err) + if err := c.RemovePackage(h, "docker-ee", "docker-ee-cli"); err != nil { + return fmt.Errorf("remove docker-ee package: %w", err) } } @@ -104,7 +101,7 @@ func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) } // function to check if the host is an AWS instance - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html -func (c Configurer) isAWSInstance(h os.Host) bool { +func (c Configurer) isAWSInstance(h configurer.Host) bool { found, err := h.ExecOutput("curl -s -m 5 http://169.254.169.254/latest/dynamic/instance-identity/document | grep region") if err != nil { log.Debugf("%s: curl on local-linked AWS id document failed: %v", h, err) diff --git a/pkg/configurer/enterpriselinux/rhel.go b/pkg/configurer/enterpriselinux/rhel.go index c2597e85..97bb5de0 100644 --- a/pkg/configurer/enterpriselinux/rhel.go +++ b/pkg/configurer/enterpriselinux/rhel.go @@ -1,8 +1,8 @@ package enterpriselinux import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // Rhel RedHat Enterprise Linux. @@ -11,9 +11,9 @@ type Rhel struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "rhel" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "rhel" }, func() any { return Rhel{} diff --git a/pkg/configurer/enterpriselinux/rockylinux.go b/pkg/configurer/enterpriselinux/rockylinux.go index 8b7147ad..0d3cc2d8 100644 --- a/pkg/configurer/enterpriselinux/rockylinux.go +++ b/pkg/configurer/enterpriselinux/rockylinux.go @@ -1,8 +1,8 @@ package enterpriselinux import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // RockyLinux support. @@ -11,9 +11,9 @@ type RockyLinux struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "rocky" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "rocky" }, func() any { return RockyLinux{} diff --git a/pkg/configurer/host.go b/pkg/configurer/host.go new file mode 100644 index 00000000..364c147c --- /dev/null +++ b/pkg/configurer/host.go @@ -0,0 +1,16 @@ +package configurer + +import ( + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/cmd" + "github.com/k0sproject/rig/v2/remotefs" +) + +// Host is the interface that configurer methods use to interact with a remote +// host. It is satisfied by *rig.Client (and therefore by the product host +// types, which embed rig.CompositeConfig and *rig.Client). +type Host interface { + cmd.SimpleRunner + Sudo() *rig.Client + FS() remotefs.FS +} diff --git a/pkg/configurer/linux.go b/pkg/configurer/linux.go index 26be435a..da6dc6d9 100644 --- a/pkg/configurer/linux.go +++ b/pkg/configurer/linux.go @@ -1,21 +1,23 @@ package configurer import ( + "context" "errors" "fmt" + "io/fs" "path" "path/filepath" "regexp" - "strconv" "strings" - escape "al.essio.dev/pkg/shellescape" "github.com/Mirantis/launchpad/pkg/constant" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" "github.com/Mirantis/launchpad/pkg/util/iputil" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os" + "github.com/k0sproject/rig/v2/cmd" + rigos "github.com/k0sproject/rig/v2/os" + "github.com/k0sproject/rig/v2/remotefs" + "github.com/k0sproject/rig/v2/sh" + "github.com/k0sproject/rig/v2/sh/shellescape" log "github.com/sirupsen/logrus" ) @@ -30,17 +32,119 @@ var ErrLinuxMCRInstall = errors.New("failed to install MCR on linux") // LinuxConfigurer is a generic linux host configurer. type LinuxConfigurer struct { - riglinux os.Linux DockerConfigurer } +// Pwd returns the current working directory of the session. +func (c LinuxConfigurer) Pwd(h Host) string { + pwd, err := h.ExecOutput("pwd 2> /dev/null") + if err != nil { + return "" + } + return pwd +} + +// IsContainer returns true if the host is actually a container. +func (c LinuxConfigurer) IsContainer(h Host) bool { + return h.Exec("grep 'container=docker' /proc/1/environ 2> /dev/null") == nil +} + +// FixContainer makes a container work like a real host. +func (c LinuxConfigurer) FixContainer(h Host) error { + if err := h.Sudo().Exec("mount --make-rshared / 2> /dev/null"); err != nil { + return fmt.Errorf("failed to mount / as rshared: %w", err) + } + return nil +} + +// SELinuxEnabled is true when SELinux is enabled. +func (c LinuxConfigurer) SELinuxEnabled(h Host) bool { + return h.Sudo().Exec("getenforce | grep -iq enforcing 2> /dev/null") == nil +} + +// Reboot reboots the host. +func (c LinuxConfigurer) Reboot(h Host) error { + if err := h.Sudo().Exec("shutdown --reboot 0 2> /dev/null"); err != nil { + return fmt.Errorf("failed to reboot: %w", err) + } + return nil +} + +// InstallPackage installs the given packages using the host's package manager. +func (c LinuxConfigurer) InstallPackage(h Host, packages ...string) error { + pm := h.Sudo().PackageManager() + if err := pm.Update(context.Background()); err != nil { + return fmt.Errorf("failed to update package sources: %w", err) + } + if err := pm.Install(context.Background(), packages...); err != nil { + return fmt.Errorf("failed to install packages: %w", err) + } + return nil +} + +// RemovePackage removes the given packages using the host's package manager. +func (c LinuxConfigurer) RemovePackage(h Host, packages ...string) error { + if err := h.Sudo().PackageManager().Remove(context.Background(), packages...); err != nil { + return fmt.Errorf("failed to remove packages: %w", err) + } + return nil +} + +// StartService starts a service on the host. +func (c LinuxConfigurer) StartService(h Host, name string) error { + svc, err := h.Sudo().Service(name) + if err != nil { + return fmt.Errorf("failed to resolve service %s: %w", name, err) + } + if err := svc.Start(context.Background()); err != nil { + return fmt.Errorf("failed to start service %s: %w", name, err) + } + return nil +} + +// StopService stops a service on the host. +func (c LinuxConfigurer) StopService(h Host, name string) error { + svc, err := h.Sudo().Service(name) + if err != nil { + return fmt.Errorf("failed to resolve service %s: %w", name, err) + } + if err := svc.Stop(context.Background()); err != nil { + return fmt.Errorf("failed to stop service %s: %w", name, err) + } + return nil +} + +// RestartService restarts a service on the host. +func (c LinuxConfigurer) RestartService(h Host, name string) error { + svc, err := h.Sudo().Service(name) + if err != nil { + return fmt.Errorf("failed to resolve service %s: %w", name, err) + } + if err := svc.Restart(context.Background()); err != nil { + return fmt.Errorf("failed to restart service %s: %w", name, err) + } + return nil +} + +// EnableService enables a service on the host. +func (c LinuxConfigurer) EnableService(h Host, name string) error { + svc, err := h.Sudo().Service(name) + if err != nil { + return fmt.Errorf("failed to resolve service %s: %w", name, err) + } + if err := svc.Enable(context.Background()); err != nil { + return fmt.Errorf("failed to enable service %s: %w", name, err) + } + return nil +} + // MCRConfigPath returns the configuration file path. func (c LinuxConfigurer) MCRConfigPath() string { return "/etc/docker/daemon.json" } -// Install MCR License. -func (c LinuxConfigurer) InstallMCRLicense(h os.Host, lic string) error { +// InstallMCRLicense installs the MCR license file. +func (c LinuxConfigurer) InstallMCRLicense(h Host, lic string) error { // Use default docker root dir if not specified in docker info dockerRootDir := constant.LinuxDefaultDockerRoot @@ -50,18 +154,18 @@ func (c LinuxConfigurer) InstallMCRLicense(h os.Host, lic string) error { } licPath := filepath.Join(dockerRootDir, LinuxDockerLicenseFile) - if err := c.riglinux.WriteFile(h, licPath, lic, "400"); err != nil { + if err := h.Sudo().FS().WriteFile(licPath, []byte(lic), fs.FileMode(0o400)); err != nil { return fmt.Errorf("license write (linux); %w", err) } return nil } -// InstallMCR install and Docker EE engine on Linux, assuming that you already have the repos setup. -func (c LinuxConfigurer) EnableMCR(h os.Host, _ commonconfig.MCRConfig) error { - if err := c.riglinux.EnableService(h, "docker"); err != nil { +// EnableMCR enables and starts the Docker EE engine on Linux, assuming that you already have the repos setup. +func (c LinuxConfigurer) EnableMCR(h Host, _ commonconfig.MCRConfig) error { + if err := c.EnableService(h, "docker"); err != nil { return fmt.Errorf("init manager could not enable docker-ee, %w", err) } - if err := c.riglinux.StartService(h, "docker"); err != nil { + if err := c.StartService(h, "docker"); err != nil { return fmt.Errorf("init manager could not start docker-ee, %w", err) } @@ -69,16 +173,16 @@ func (c LinuxConfigurer) EnableMCR(h os.Host, _ commonconfig.MCRConfig) error { } // RestartMCR restarts Docker EE engine. -func (c LinuxConfigurer) RestartMCR(h os.Host) error { - if err := c.riglinux.RestartService(h, "docker"); err != nil { +func (c LinuxConfigurer) RestartMCR(h Host) error { + if err := c.RestartService(h, "docker"); err != nil { return fmt.Errorf("restart docker service: %w", err) } return nil } // ResolveInternalIP resolves internal ip from private interface. -func (c LinuxConfigurer) ResolveInternalIP(h os.Host, privateInterface, publicIP string) (string, error) { - output, err := h.ExecOutput(fmt.Sprintf("%s ip -o addr show dev %s scope global", SbinPath, privateInterface)) +func (c LinuxConfigurer) ResolveInternalIP(h Host, privateInterface, publicIP string) (string, error) { + output, err := h.ExecOutput(SbinPath + " " + sh.Command("ip", "-o", "addr", "show", "dev", privateInterface, "scope", "global")) if err != nil { return "", fmt.Errorf("%w: failed to find private interface with name %s: %s. Make sure you've set correct 'privateInterface' for the host in config", err, privateInterface, output) } @@ -118,7 +222,7 @@ func (c LinuxConfigurer) DockerCommandf(template string, args ...any) string { } // ValidateLocalhost returns an error if "localhost" is not a local address. -func (c LinuxConfigurer) ValidateLocalhost(h os.Host) error { +func (c LinuxConfigurer) ValidateLocalhost(h Host) error { if err := h.Exec("ping -c 1 -w 1 localhost"); err != nil { return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host: %w", err) } @@ -126,12 +230,12 @@ func (c LinuxConfigurer) ValidateLocalhost(h os.Host) error { } // CheckPrivilege returns an error if the user does not have passwordless sudo enabled. -func (c LinuxConfigurer) CheckPrivilege(_ os.Host) error { +func (c LinuxConfigurer) CheckPrivilege(_ Host) error { return nil } // LocalAddresses returns a list of local addresses. -func (c LinuxConfigurer) LocalAddresses(h os.Host) ([]string, error) { +func (c LinuxConfigurer) LocalAddresses(h Host) ([]string, error) { output, err := h.ExecOutput("hostname --all-ip-addresses") if err != nil { return nil, fmt.Errorf("failed to get local addresses: %w", err) @@ -146,7 +250,7 @@ type reconnectable interface { } // AuthorizeDocker adds the current user to the docker group. -func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { +func (c LinuxConfigurer) AuthorizeDocker(h Host) error { if h.Exec(`[ "$(id -u)" = 0 ]`) == nil { log.Debugf("%s: current user is uid 0 - no need to authorize", h) return nil @@ -162,7 +266,7 @@ func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { return nil //nolint:nilerr } - if err := h.Exec("usermod -aG docker $USER", exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec("usermod -aG docker $USER"); err != nil { return fmt.Errorf("failed to add the current user to the 'docker' group: %w", err) } @@ -183,31 +287,68 @@ func (c LinuxConfigurer) AuthorizeDocker(h os.Host) error { } // AuthenticateDocker performs a docker login on the host. -func (c LinuxConfigurer) AuthenticateDocker(h os.Host, user, pass, imageRepo string) error { - if err := h.Exec(c.DockerCommandf("login -u %s --password-stdin %s", escape.Quote(user), imageRepo), exec.Stdin(pass), exec.RedactString(user, pass)); err != nil { +func (c LinuxConfigurer) AuthenticateDocker(h Host, user, pass, imageRepo string) error { + if err := h.Exec(c.DockerCommandf("login -u %s --password-stdin %s", shellescape.Quote(user), imageRepo), cmd.StdinString(pass), cmd.Redact(user), cmd.Redact(pass)); err != nil { return fmt.Errorf("failed to login to the docker registry: %w", err) } return nil } +// envKeyRegexp matches valid environment variable names: they must start with a +// letter or underscore and contain only letters, digits and underscores. This +// prevents keys with spaces or shell metacharacters from breaking (or being +// abused via) the /etc/environment and export steps. +var envKeyRegexp = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) + // UpdateEnvironment updates the hosts's environment variables. -func (c LinuxConfigurer) UpdateEnvironment(h os.Host, env map[string]string) error { - if err := c.riglinux.UpdateEnvironment(h, env); err != nil { - return fmt.Errorf("failed updating the env: %w", err) +func (c LinuxConfigurer) UpdateEnvironment(h Host, env map[string]string) error { + fsys := h.Sudo().FS() + for k, v := range env { + if !envKeyRegexp.MatchString(k) { + return fmt.Errorf("invalid environment variable key %q: must match %s", k, envKeyRegexp.String()) + } + if strings.ContainsRune(v, '\n') { + return fmt.Errorf("invalid environment variable value for key %q: must not contain newline", k) + } + patch := remotefs.ReplaceOrAppend(remotefs.ByPrefix(k+"="), fmt.Sprintf("%s=%s", k, v)) + if err := remotefs.PatchFile(fsys, "/etc/environment", []remotefs.Patch{patch}, remotefs.WithCreate(fs.FileMode(0o644))); err != nil { + return fmt.Errorf("failed updating the env: %w", err) + } + } + + // Export the values into the current session environment using the + // in-memory values with proper shell escaping. + var export strings.Builder + for k, v := range env { + fmt.Fprintf(&export, "export %s=%s\n", k, shellescape.Quote(v)) } + if export.Len() > 0 { + if err := h.Sudo().Exec(export.String()); err != nil { + return fmt.Errorf("failed to update environment: %w", err) + } + } + return c.ConfigureDockerProxy(h, env) } // CleanupEnvironment removes environment variable configuration. -func (c LinuxConfigurer) CleanupEnvironment(h os.Host, env map[string]string) error { - if err := c.riglinux.CleanupEnvironment(h, env); err != nil { +func (c LinuxConfigurer) CleanupEnvironment(h Host, env map[string]string) error { + if len(env) == 0 { + return nil + } + fsys := h.Sudo().FS() + patches := make([]remotefs.Patch, 0, len(env)) + for k := range env { + patches = append(patches, remotefs.DeleteMatching(remotefs.ByPrefix(k+"="))) + } + if err := remotefs.PatchFile(fsys, "/etc/environment", patches, remotefs.WithCreate(fs.FileMode(0o644))); err != nil { return fmt.Errorf("failed cleaning the env: %w", err) } return nil } // ConfigureDockerProxy creates a docker systemd configuration for the proxy environment variables. -func (c LinuxConfigurer) ConfigureDockerProxy(h os.Host, env map[string]string) error { +func (c LinuxConfigurer) ConfigureDockerProxy(h Host, env map[string]string) error { proxyenvs := make(map[string]string) for k, v := range env { @@ -224,17 +365,16 @@ func (c LinuxConfigurer) ConfigureDockerProxy(h os.Host, env map[string]string) dir := "/etc/systemd/system/docker.service.d" cfg := path.Join(dir, "http-proxy.conf") - err := c.riglinux.MkDir(h, dir, exec.Sudo(h)) - if err != nil { + if err := h.Sudo().FS().MkdirAll(dir, fs.FileMode(0o755)); err != nil { return fmt.Errorf("failed to create %s: %w", dir, err) } content := "[Service]\n" for k, v := range proxyenvs { - content += fmt.Sprintf("Environment=\"%s=%s\"\n", escape.Quote(k), escape.Quote(v)) + content += fmt.Sprintf("Environment=\"%s=%s\"\n", shellescape.Quote(k), shellescape.Quote(v)) } - if err := c.riglinux.WriteFile(h, cfg, content, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(cfg, []byte(content), fs.FileMode(0o600)); err != nil { return fmt.Errorf("failed to create %s: %w", cfg, err) } @@ -244,7 +384,7 @@ func (c LinuxConfigurer) ConfigureDockerProxy(h os.Host, env map[string]string) var errDetectPrivateInterface = errors.New("failed to detect a private network interface, define the host privateInterface manually") // ResolvePrivateInterface tries to find a private network interface. -func (c LinuxConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { +func (c LinuxConfigurer) ResolvePrivateInterface(h Host) (string, error) { output, err := h.ExecOutput(fmt.Sprintf(`%s; (ip route list scope global | grep -P "\b(172|10|192\.168)\.") || (ip route list | grep -m1 default)`, SbinPath)) if err != nil { return "", fmt.Errorf("%w: %w", errDetectPrivateInterface, err) @@ -257,23 +397,8 @@ func (c LinuxConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { return string(match[1]), nil } -// HTTPStatus makes a HTTP GET request to the url and returns the status code or an error. -func (c LinuxConfigurer) HTTPStatus(h os.Host, url string) (int, error) { - log.Debugf("%s: requesting %s", h, url) - output, err := h.ExecOutput(fmt.Sprintf(`curl -kso /dev/null -w "%%{http_code}" "%s"`, url)) - if err != nil { - return -1, fmt.Errorf("failed to perform http request: %w", err) - } - status, err := strconv.Atoi(output) - if err != nil { - return -1, fmt.Errorf("invalid http response: %w", err) - } - - return status, nil -} - // CleanupLingeringMCR removes left over MCR files after Launchpad reset. -func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig.DockerInfo) { +func (c LinuxConfigurer) CleanupLingeringMCR(h Host, dockerInfo commonconfig.DockerInfo) { // Use default docker root dir if not specified in docker info dockerRootDir := constant.LinuxDefaultDockerRoot dockerExecRootDir := constant.LinuxDefaultDockerExecRoot @@ -285,7 +410,7 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig. } // https://docs.docker.com/config/daemon/ - if !c.riglinux.FileExist(h, dockerDaemonPath) { + if !h.Sudo().FS().FileExist(dockerDaemonPath) { // Check if the default Rootless Docker daemon config file exists log.Debugf("%s: attempting to detect Rootless docker installation", h) // Extract the value from the xdgConfigHome environment variable @@ -299,11 +424,11 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig. } } - dockerDaemonString, err := c.riglinux.ReadFile(h, dockerDaemonPath) + dockerDaemonData, err := fs.ReadFile(h.Sudo().FS(), dockerDaemonPath) if err != nil { log.Debugf("%s: couldn't read the Docker Daemon config file %s: %s", h, dockerDaemonPath, err) } - dockerConfig, err := c.GetDockerDaemonConfig(dockerDaemonString) + dockerConfig, err := c.GetDockerDaemonConfig(string(dockerDaemonData)) if err != nil { log.Debugf("%s: failed to create DockerDaemon config %s: %s", h, dockerConfig, err) } @@ -326,7 +451,7 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig. // /var/run/ Exec-root folder execRootNetnsUnmount := path.Join(dockerExecRootDir, "netns/default") - if err := h.Exec(fmt.Sprintf("umount %s", execRootNetnsUnmount), exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(fmt.Sprintf("umount %s", execRootNetnsUnmount)); err != nil { log.Debugf("%s: failed to umount %s: %s", h, execRootNetnsUnmount, err) } @@ -343,14 +468,14 @@ func (c LinuxConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig. c.attemptPathSudoDelete(h, "/lib/systemd/system/cri-dockerd-mke.socket") } -func (c LinuxConfigurer) attemptPathSudoDelete(h os.Host, path string) { - fileInfo, err := c.riglinux.Stat(h, path, exec.Sudo(h)) +func (c LinuxConfigurer) attemptPathSudoDelete(h Host, path string) { + fileInfo, err := h.Sudo().FS().Stat(path) if err != nil { log.Debugf("%s: error getting file information for %s: %s", h, path, err) return } - if !c.riglinux.FileExist(h, path) { + if !h.Sudo().FS().FileExist(path) { log.Infof("%s: file %s doesn't exist", h, path) return } @@ -360,7 +485,7 @@ func (c LinuxConfigurer) attemptPathSudoDelete(h os.Host, path string) { command = fmt.Sprintf("rm -rf %s", path) } - if err := h.Exec(command, exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(command); err != nil { log.Infof("%s: failed to remove %s: %s", h, path, err) return } @@ -369,23 +494,11 @@ func (c LinuxConfigurer) attemptPathSudoDelete(h os.Host, path string) { var errAbort = errors.New("base os detected but version resolving failed") -// ResolveLinux stolen from k0sproject/rig. -// -// We need os-release info in various scenarios, but rig doesn't really expose it. -func ResolveLinux(h os.Host) (rig.OSVersion, error) { - if err := h.Exec("uname | grep -q Linux"); err != nil { - return rig.OSVersion{}, fmt.Errorf("not a linux host (%w)", err) - } - - output, err := h.ExecOutput("cat /etc/os-release || cat /usr/lib/os-release") - if err != nil { - // at this point it is known that this is a linux host, so any error from here on should signal the resolver to not try the next - return rig.OSVersion{}, fmt.Errorf("%w: unable to read os-release file: %w", errAbort, err) - } - - var version rig.OSVersion - if err := rig.ParseOSReleaseFile(output, &version); err != nil { - return rig.OSVersion{}, errors.Join(errAbort, err) +// ResolveLinux resolves the OS release information for a linux host. +func ResolveLinux(h Host) (*rigos.Release, error) { + release, ok := rigos.ResolveLinux(h) + if !ok { + return nil, fmt.Errorf("%w: unable to resolve linux OS release", errAbort) } - return version, nil + return release, nil } diff --git a/pkg/configurer/oracle/oracle.go b/pkg/configurer/oracle/oracle.go index 1bdfb20b..3f78cd1a 100644 --- a/pkg/configurer/oracle/oracle.go +++ b/pkg/configurer/oracle/oracle.go @@ -1,9 +1,9 @@ package oracle import ( + "github.com/Mirantis/launchpad/pkg/configurer" "github.com/Mirantis/launchpad/pkg/configurer/enterpriselinux" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" ) // Configurer is the Oracle Linux specific implementation of a host configurer. @@ -12,9 +12,9 @@ type Configurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ol" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ol" }, func() any { return Configurer{} diff --git a/pkg/configurer/registry.go b/pkg/configurer/registry.go new file mode 100644 index 00000000..2dba2248 --- /dev/null +++ b/pkg/configurer/registry.go @@ -0,0 +1,44 @@ +package configurer + +import ( + "sync" + + rigos "github.com/k0sproject/rig/v2/os" +) + +type registryEntry struct { + matcher func(*rigos.Release) bool + builder func() any +} + +var ( + registryMu sync.RWMutex + registryEntries []registryEntry +) + +// RegisterOSModule registers a configurer factory for hosts whose detected OS +// release matches the given predicate. Modules are evaluated in registration +// order and the first matching one wins, so register more specific matchers +// before more general ones. +func RegisterOSModule(matcher func(*rigos.Release) bool, builder func() any) { + registryMu.Lock() + defer registryMu.Unlock() + registryEntries = append(registryEntries, registryEntry{matcher: matcher, builder: builder}) +} + +// ResolveOSModule returns the factory for the first registered module whose +// matcher accepts the given release. The boolean is false when no module +// matches. +func ResolveOSModule(release *rigos.Release) (func() any, bool) { + if release == nil { + return nil, false + } + registryMu.RLock() + defer registryMu.RUnlock() + for _, e := range registryEntries { + if e.matcher(release) { + return e.builder, true + } + } + return nil, false +} diff --git a/pkg/configurer/sles/sles.go b/pkg/configurer/sles/sles.go index ec4ba7d5..ca0b2a52 100644 --- a/pkg/configurer/sles/sles.go +++ b/pkg/configurer/sles/sles.go @@ -8,18 +8,15 @@ import ( "github.com/Mirantis/launchpad/pkg/configurer" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os" - "github.com/k0sproject/rig/os/linux" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" + "github.com/k0sproject/rig/v2/sh" log "github.com/sirupsen/logrus" ) func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "sles" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "sles" }, func() any { return Configurer{} @@ -32,15 +29,13 @@ const ( ZypperRepoAlias = "mirantis" ) -// Configurer is a generic Ubuntu level configurer implementation. Some of the configurer interface implementation -// might be on OS version specific implementation such as for Bionic. +// Configurer is a generic SLES level configurer implementation. type Configurer struct { - linux.SLES configurer.LinuxConfigurer } // PrepareHost prepares the machine host by installing the needed base packages, and fixing any container issues. -func (c Configurer) PrepareHost(h os.Host) error { +func (c Configurer) PrepareHost(h configurer.Host) error { if err := c.InstallPackage(h, "curl", "socat"); err != nil { return fmt.Errorf("failed to install base packages: %w", err) } @@ -54,7 +49,7 @@ func (c Configurer) PrepareHost(h os.Host) error { log.Debugf("%s: checking for Docker-CE conflict", h) if out, err := h.ExecOutput("zypper search --type=package --installed-only docker"); err == nil && !strings.Contains(out, ZypperPackageNotFound) && !strings.Contains(out, "docker-ee") { log.Warnf("%s: detected Docker-CE, removing from system", h) - if err := h.Exec("zypper remove -y --clean-deps docker", exec.Sudo(h)); err != nil { + if err := c.RemovePackage(h, "docker"); err != nil { return fmt.Errorf("could not remove existing docker-ce installation: %w", err) } } @@ -63,7 +58,7 @@ func (c Configurer) PrepareHost(h os.Host) error { } // InstallMCR install Docker EE engine on Linux. -func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) InstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { ver, verErr := configurer.ResolveLinux(h) if verErr != nil { return fmt.Errorf("could not discover Linux version information") @@ -76,15 +71,15 @@ func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) e if out, err := h.ExecOutput("zypper repos"); err != nil { return fmt.Errorf("%s: could not list zypper repos", h) } else if strings.Contains(out, ZypperRepoAlias) { - if err := h.Exec(fmt.Sprintf("zypper removerepo %s", ZypperRepoAlias), exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(sh.Command("zypper", "removerepo", ZypperRepoAlias)); err != nil { return errors.Join(fmt.Errorf("failed to remove existing zypper MCR repo: %s", ZypperRepoAlias), err) } } log.Debugf("%s: sles MCR GPG key import %s", h, zypperGpgURL) - if err := h.Exec(fmt.Sprintf("sudo rpm --import %s", zypperGpgURL), exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(sh.Command("rpm", "--import", zypperGpgURL)); err != nil { return errors.Join(fmt.Errorf("failed to add zypper GPG key for MCR"), err) } - if err := h.Exec(fmt.Sprintf("zypper addrepo --refresh '%s' mirantis", zypperRepoURL), exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(sh.Command("zypper", "addrepo", "--refresh", zypperRepoURL, "mirantis")); err != nil { return errors.Join(fmt.Errorf("failed to add zypper MCR repo: %s", zypperRepoURL), err) } @@ -104,7 +99,7 @@ func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) e } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -122,8 +117,8 @@ func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) return fmt.Errorf("stop containerd: %w", err) } - if err := h.Exec("zypper -n remove -y --clean-deps docker-ee docker-ee-cli", exec.Sudo(h)); err != nil { - return fmt.Errorf("remove docker-ee zypper package: %w", err) + if err := c.RemovePackage(h, "docker-ee", "docker-ee-cli"); err != nil { + return fmt.Errorf("remove docker-ee package: %w", err) } } diff --git a/pkg/configurer/ubuntu/bionic.go b/pkg/configurer/ubuntu/bionic.go index df338f5b..cb086ba2 100644 --- a/pkg/configurer/ubuntu/bionic.go +++ b/pkg/configurer/ubuntu/bionic.go @@ -1,8 +1,8 @@ package ubuntu import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // BionicConfigurer is the Ubuntu Bionix specific host configurer implementation. @@ -11,9 +11,9 @@ type BionicConfigurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ubuntu" && os.Version == "18.04" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ubuntu" && r.Version == "18.04" }, func() interface{} { return BionicConfigurer{} diff --git a/pkg/configurer/ubuntu/focal.go b/pkg/configurer/ubuntu/focal.go index 62081c16..6ba3a054 100644 --- a/pkg/configurer/ubuntu/focal.go +++ b/pkg/configurer/ubuntu/focal.go @@ -1,8 +1,8 @@ package ubuntu import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // FocalConfigurer is the Ubuntu Focal (20.04) specific host configurer implementation. @@ -11,9 +11,9 @@ type FocalConfigurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ubuntu" && os.Version == "20.04" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ubuntu" && r.Version == "20.04" }, func() interface{} { return FocalConfigurer{} diff --git a/pkg/configurer/ubuntu/jammy.go b/pkg/configurer/ubuntu/jammy.go index b6d9243f..57a8565c 100644 --- a/pkg/configurer/ubuntu/jammy.go +++ b/pkg/configurer/ubuntu/jammy.go @@ -1,8 +1,8 @@ package ubuntu import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // JammyConfigurer is the Ubuntu Jammy Jellyfish (22.04) specific host configurer implementation. @@ -11,9 +11,9 @@ type JammyConfigurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ubuntu" && os.Version == "22.04" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ubuntu" && r.Version == "22.04" }, func() interface{} { return JammyConfigurer{} diff --git a/pkg/configurer/ubuntu/noble.go b/pkg/configurer/ubuntu/noble.go index 4b4c80fd..82e406c8 100644 --- a/pkg/configurer/ubuntu/noble.go +++ b/pkg/configurer/ubuntu/noble.go @@ -1,8 +1,8 @@ package ubuntu import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // NobleConfigurer is the Ubuntu Noble Numbat (24.04) specific host configurer implementation. @@ -11,9 +11,9 @@ type NobleConfigurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ubuntu" && os.Version == "24.04" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ubuntu" && r.Version == "24.04" }, func() interface{} { return NobleConfigurer{} diff --git a/pkg/configurer/ubuntu/ubuntu.go b/pkg/configurer/ubuntu/ubuntu.go index a3470109..b23b6194 100644 --- a/pkg/configurer/ubuntu/ubuntu.go +++ b/pkg/configurer/ubuntu/ubuntu.go @@ -2,23 +2,20 @@ package ubuntu import ( "fmt" + "io/fs" "github.com/Mirantis/launchpad/pkg/configurer" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os" - "github.com/k0sproject/rig/os/linux" ) // Configurer is a generic Ubuntu level configurer implementation. Some of the configurer interface implementation // might be on OS version specific implementation such as for Bionic. type Configurer struct { - linux.Ubuntu configurer.LinuxConfigurer } // PrepareHost prepares the machine host by installing the needed base packages, and fixing any container issues. -func (c Configurer) PrepareHost(h os.Host) error { +func (c Configurer) PrepareHost(h configurer.Host) error { if err := c.InstallPackage(h, "curl", "apt-utils", "socat", "iputils-ping"); err != nil { return fmt.Errorf("failed to install base packages: %w", err) } @@ -32,7 +29,7 @@ func (c Configurer) PrepareHost(h os.Host) error { } // InstallMCR install Docker EE engine on Linux. -func (c Configurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) InstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { ver, verErr := configurer.ResolveLinux(h) if verErr != nil { return fmt.Errorf("could not discover Linux version information") @@ -56,19 +53,18 @@ Signed-by: /usr/share/keyrings/mirantis-archive-keyring.gpg // https://docs.mirantis.com/mcr/25.0/install/mcr-linux/ubuntu.html instructions // 2. import the mirantis gpg key - if err := h.Exec(fmt.Sprintf("sudo gpg --batch --yes --output /usr/share/keyrings/mirantis-archive-keyring.gpg --dearmor <<< $(curl -fsSL %s)", gpgURL), exec.Sudo(h)); err != nil { + if err := h.Sudo().Exec(fmt.Sprintf("gpg --batch --yes --output /usr/share/keyrings/mirantis-archive-keyring.gpg --dearmor <<< $(curl -fsSL %s)", gpgURL)); err != nil { return fmt.Errorf("could not install the Mirantis Ubuntu GPG signing key") } // 4. write the repo file // @TODO check if we can use apt-add-repository instead of writing a file (probably has better validation) - if err := c.WriteFile(h, debRepoFilePath, debRepo, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(debRepoFilePath, []byte(debRepo), fs.FileMode(0o600)); err != nil { return fmt.Errorf("could not write APT repo file for MCR") } - if err := h.Exec("apt-get update", exec.Sudo(h)); err != nil { - return fmt.Errorf("could not update apt package info") - } + // InstallPackage refreshes the package indexes (apt-get update) before + // installing, so the freshly written Mirantis repo is picked up here. if err := c.InstallPackage(h, "containerd.io"); err != nil { return fmt.Errorf("package manager could not install containerd.io") } @@ -83,7 +79,7 @@ Signed-by: /usr/share/keyrings/mirantis-archive-keyring.gpg } // UninstallMCR uninstalls docker-ee engine. -func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c Configurer) UninstallMCR(h configurer.Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -101,8 +97,8 @@ func (c Configurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) return fmt.Errorf("stop containerd: %w", err) } - if err := h.Exec("apt-get -y remove docker-ee docker-ee-cli", exec.Sudo(h)); err != nil { - return fmt.Errorf("failed to uninstall docker-ee apt package: %w", err) + if err := c.RemovePackage(h, "docker-ee", "docker-ee-cli"); err != nil { + return fmt.Errorf("failed to uninstall docker-ee package: %w", err) } } diff --git a/pkg/configurer/ubuntu/xenial.go b/pkg/configurer/ubuntu/xenial.go index 70a163b8..47bb06d2 100644 --- a/pkg/configurer/ubuntu/xenial.go +++ b/pkg/configurer/ubuntu/xenial.go @@ -1,8 +1,8 @@ package ubuntu import ( - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + "github.com/Mirantis/launchpad/pkg/configurer" + rigos "github.com/k0sproject/rig/v2/os" ) // XenialConfigurer is the Ubuntu Xenial specific host configurer implementation. @@ -11,9 +11,9 @@ type XenialConfigurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "ubuntu" && os.Version == "16.04" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "ubuntu" && r.Version == "16.04" }, func() interface{} { return XenialConfigurer{} diff --git a/pkg/configurer/windows.go b/pkg/configurer/windows.go index f961d2a7..ba435d59 100644 --- a/pkg/configurer/windows.go +++ b/pkg/configurer/windows.go @@ -6,7 +6,6 @@ import ( "io/fs" "path" "path/filepath" - "strconv" "strings" "time" @@ -15,9 +14,9 @@ import ( "github.com/Mirantis/launchpad/pkg/util/iputil" "github.com/avast/retry-go" "github.com/hashicorp/go-version" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os" - ps "github.com/k0sproject/rig/pkg/powershell" + "github.com/k0sproject/rig/v2/cmd" + ps "github.com/k0sproject/rig/v2/powershell" + "github.com/k0sproject/rig/v2/remotefs" log "github.com/sirupsen/logrus" ) @@ -28,12 +27,23 @@ const ( // WindowsConfigurer is a generic windows host configurer. type WindowsConfigurer struct { - os.Windows - PowerShellVersion *version.Version DockerConfigurer } +// Pwd returns the current working directory. +func (c WindowsConfigurer) Pwd(h Host) string { + if pwd, err := h.ExecOutput("echo %cd%"); err == nil { + return pwd + } + return "" +} + +// SELinuxEnabled is always false on windows. +func (c WindowsConfigurer) SELinuxEnabled(_ Host) bool { + return false +} + // MCRConfigPath returns the configuration file path. func (c WindowsConfigurer) MCRConfigPath() string { return `C:\ProgramData\Docker\config\daemon.json` @@ -42,7 +52,7 @@ func (c WindowsConfigurer) MCRConfigPath() string { var errRebootRequired = fmt.Errorf("reboot required") // InstallMCRLicense for license install.. -func (c WindowsConfigurer) InstallMCRLicense(h os.Host, lic string) error { +func (c WindowsConfigurer) InstallMCRLicense(h Host, lic string) error { // Use default docker root dir if not specified in docker info dockerRootDir := constant.WindowsDefaultDockerRoot @@ -52,14 +62,14 @@ func (c WindowsConfigurer) InstallMCRLicense(h os.Host, lic string) error { } licPath := filepath.Join(dockerRootDir, WindowsDockerLicenseFile) - if err := c.WriteFile(h, licPath, lic, "400"); err != nil { + if err := h.FS().WriteFile(licPath, []byte(lic), fs.FileMode(0o400)); err != nil { return fmt.Errorf("license write; %w", err) } return nil } // InstallMCR install MCR on Windows. -func (c WindowsConfigurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c WindowsConfigurer) InstallMCR(h Host, engineConfig commonconfig.MCRConfig) error { version := "latest" installerPath, getInstallerErr := GetInstaller(engineConfig.InstallURLWindows) @@ -70,11 +80,11 @@ func (c WindowsConfigurer) InstallMCR(h os.Host, engineConfig commonconfig.MCRCo pwd := c.Pwd(h) base := path.Base(installerPath) installer := pwd + "\\" + base + ".ps1" - if err := h.Upload(installerPath, installer, fs.FileMode(0o640)); err != nil { + if err := remotefs.Upload(h.FS(), installerPath, installer, remotefs.WithPermissions(fs.FileMode(0o640))); err != nil { return fmt.Errorf("failed to upload MCR installer: %w", err) } defer func() { - if err := c.DeleteFile(h, installer); err != nil { + if err := h.FS().Remove(installer); err != nil { log.Warnf("failed to delete MCR installer: %s", err.Error()) } }() @@ -141,7 +151,7 @@ func isExitCode3010(err error) bool { // a fallback. // // TODO: move this fix upstream into the k0sproject/rig Windows configurer. -func (c WindowsConfigurer) Reboot(h os.Host) error { +func (c WindowsConfigurer) Reboot(h Host) error { const taskName = "LaunchpadReboot" // The ONSTART trigger means the task will re-fire on the next startup, but // the post-reboot cleanup in InstallMCR deletes it once the host is back up. @@ -166,7 +176,7 @@ func (c WindowsConfigurer) Reboot(h os.Host) error { // UninstallMCR uninstalls docker-ee engine // This relies on using the http://get.mirantis.com/install.ps1 script with the '-Uninstall' option, and some cleanup as per // https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/configure-docker-daemon#how-to-uninstall-docker -func (c WindowsConfigurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCRConfig) error { +func (c WindowsConfigurer) UninstallMCR(h Host, engineConfig commonconfig.MCRConfig) error { info, getDockerError := c.GetDockerInfo(h) if engineConfig.Prune { defer c.CleanupLingeringMCR(h, info) @@ -184,11 +194,11 @@ func (c WindowsConfigurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCR pwd := c.Pwd(h) base := path.Base(installerPath) uninstaller := pwd + "\\" + base + ".ps1" - if err := h.Upload(installerPath, uninstaller, fs.FileMode(0o640)); err != nil { + if err := remotefs.Upload(h.FS(), installerPath, uninstaller, remotefs.WithPermissions(fs.FileMode(0o640))); err != nil { return fmt.Errorf("upload MCR uninstaller: %w", err) } defer func() { - if err := c.DeleteFile(h, uninstaller); err != nil { + if err := h.FS().Remove(uninstaller); err != nil { log.Warnf("failed to delete MCR uninstaller: %s", err.Error()) } }() @@ -203,7 +213,7 @@ func (c WindowsConfigurer) UninstallMCR(h os.Host, engineConfig commonconfig.MCR } // RestartMCR restarts Docker EE engine. -func (c WindowsConfigurer) RestartMCR(h os.Host) error { +func (c WindowsConfigurer) RestartMCR(h Host) error { _ = h.Exec("net stop com.docker.service") _ = h.Exec("net start com.docker.service") err := retry.Do( @@ -225,7 +235,7 @@ func (c WindowsConfigurer) RestartMCR(h os.Host) error { } // ResolveInternalIP resolves internal ip from private interface. -func (c WindowsConfigurer) ResolveInternalIP(h os.Host, privateInterface, publicIP string) (string, error) { +func (c WindowsConfigurer) ResolveInternalIP(h Host, privateInterface, publicIP string) (string, error) { output, err := c.interfaceIP(h, privateInterface) if err != nil { if !strings.HasPrefix(privateInterface, "vEthernet") { @@ -249,7 +259,7 @@ func (c WindowsConfigurer) ResolveInternalIP(h os.Host, privateInterface, public return publicIP, nil } -func (c WindowsConfigurer) interfaceIP(h os.Host, iface string) (string, error) { +func (c WindowsConfigurer) interfaceIP(h Host, iface string) (string, error) { output, err := h.ExecOutput(ps.Cmd(fmt.Sprintf(`(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias %s).IPAddress`, ps.SingleQuote(iface)))) if err != nil { return "", fmt.Errorf("failed to get IP address for interface %s: %w", iface, err) @@ -264,7 +274,7 @@ func (c WindowsConfigurer) DockerCommandf(template string, args ...interface{}) } // ValidateLocalhost returns an error if "localhost" is not local on the host. -func (c WindowsConfigurer) ValidateLocalhost(h os.Host) error { +func (c WindowsConfigurer) ValidateLocalhost(h Host) error { err := h.Exec(ps.Cmd(`"$ips=[System.Net.Dns]::GetHostAddresses('localhost'); Get-NetIPAddress -IPAddress $ips"`)) if err != nil { return fmt.Errorf("hostname 'localhost' does not resolve to an address local to the host: %w", err) @@ -273,7 +283,7 @@ func (c WindowsConfigurer) ValidateLocalhost(h os.Host) error { } // LocalAddresses returns a list of local addresses. -func (c WindowsConfigurer) LocalAddresses(h os.Host) ([]string, error) { +func (c WindowsConfigurer) LocalAddresses(h Host) ([]string, error) { output, err := h.ExecOutput(ps.Cmd(`(Get-NetIPAddress).IPV4Address`)) if err != nil { return nil, fmt.Errorf("failed to get local addresses: %w", err) @@ -288,7 +298,7 @@ func (c WindowsConfigurer) LocalAddresses(h os.Host) ([]string, error) { } // CheckPrivilege returns an error if the user does not have admin access to the host. -func (c WindowsConfigurer) CheckPrivilege(h os.Host) error { +func (c WindowsConfigurer) CheckPrivilege(h Host) error { privCheck := "\"$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (!$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { $host.SetShouldExit(1) }\"" if err := h.Exec(ps.Cmd(privCheck)); err != nil { @@ -299,32 +309,39 @@ func (c WindowsConfigurer) CheckPrivilege(h os.Host) error { } // AuthenticateDocker performs a docker login on the host. -func (c WindowsConfigurer) AuthenticateDocker(h os.Host, user, pass, imageRepo string) error { +func (c WindowsConfigurer) AuthenticateDocker(h Host, user, pass, imageRepo string) error { // the --pasword-stdin seems to hang in windows - if err := h.Exec(c.DockerCommandf("login -u %s -p %s %s", user, pass, imageRepo), exec.RedactString(user, pass), exec.AllowWinStderr()); err != nil { + if err := h.Exec(c.DockerCommandf("login -u %s -p %s %s", user, pass, imageRepo), cmd.Redact(user), cmd.Redact(pass), cmd.AllowWinStderr()); err != nil { return fmt.Errorf("failed to login to docker registry: %w", err) } return nil } // UpdateEnvironment updates the hosts's environment variables. -func (c WindowsConfigurer) UpdateEnvironment(h os.Host, env map[string]string) error { - if err := c.Windows.UpdateEnvironment(h, env); err != nil { - return fmt.Errorf("failed updating the env: %w", err) +func (c WindowsConfigurer) UpdateEnvironment(h Host, env map[string]string) error { + for k, v := range env { + if err := h.Exec(fmt.Sprintf(`setx %s %s`, ps.DoubleQuote(k), ps.DoubleQuote(v))); err != nil { + return fmt.Errorf("failed to set environment variable %s: %w", k, err) + } } return nil } // CleanupEnvironment removes environment variable configuration. -func (c WindowsConfigurer) CleanupEnvironment(h os.Host, env map[string]string) error { - if err := c.Windows.CleanupEnvironment(h, env); err != nil { - return fmt.Errorf("failed cleaning the env: %w", err) +func (c WindowsConfigurer) CleanupEnvironment(h Host, env map[string]string) error { + for k := range env { + if err := h.Exec(fmt.Sprintf(`powershell "[Environment]::SetEnvironmentVariable(%s, $null, 'User')"`, ps.SingleQuote(k))); err != nil { + return fmt.Errorf("failed to remove user environment variable %s: %w", k, err) + } + if err := h.Exec(fmt.Sprintf(`powershell "[Environment]::SetEnvironmentVariable(%s, $null, 'Machine')"`, ps.SingleQuote(k))); err != nil { + return fmt.Errorf("failed to remove machine environment variable %s: %w", k, err) + } } return nil } // ResolvePrivateInterface tries to find a private network interface. -func (c WindowsConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { +func (c WindowsConfigurer) ResolvePrivateInterface(h Host) (string, error) { output, err := h.ExecOutput(ps.Cmd(`(Get-NetConnectionProfile -NetworkCategory Private | Select-Object -First 1).InterfaceAlias`)) if err != nil || output == "" { output, err = h.ExecOutput(ps.Cmd(`(Get-NetConnectionProfile | Select-Object -First 1).InterfaceAlias`)) @@ -335,37 +352,23 @@ func (c WindowsConfigurer) ResolvePrivateInterface(h os.Host) (string, error) { return strings.TrimSpace(output), nil } -// HTTPStatus makes a HTTP GET request to the url and returns the status code or an error. -func (c WindowsConfigurer) HTTPStatus(h os.Host, url string) (int, error) { - log.Debugf("%s: requesting %s", h, url) - output, err := h.ExecOutput(ps.Cmd(fmt.Sprintf(`[int][System.Net.WebRequest]::Create(%s).GetResponse().StatusCode`, ps.SingleQuote(url)))) - if err != nil { - return -1, fmt.Errorf("failed to get HTTP status code: %w", err) - } - status, err := strconv.Atoi(output) - if err != nil { - return -1, fmt.Errorf("invalid response: %w", err) - } - return status, nil -} - // AuthorizeDocker does nothing on windows. -func (c WindowsConfigurer) AuthorizeDocker(_ os.Host) error { +func (c WindowsConfigurer) AuthorizeDocker(_ Host) error { return nil } // InstallMKEBasePackages is a no-op on Windows (no base packages to install). -func (c WindowsConfigurer) InstallMKEBasePackages(_ os.Host) error { +func (c WindowsConfigurer) InstallMKEBasePackages(_ Host) error { return nil } // PrepareHost prepares the host for MKE install (no-op on Windows). -func (c WindowsConfigurer) PrepareHost(_ os.Host) error { +func (c WindowsConfigurer) PrepareHost(_ Host) error { return nil } // CleanupLingeringMCR cleans up lingering MCR configuration files. -func (c WindowsConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfig.DockerInfo) { +func (c WindowsConfigurer) CleanupLingeringMCR(h Host, dockerInfo commonconfig.DockerInfo) { dockerRootDir := constant.WindowsDefaultDockerRoot if dockerInfo.DockerRootDir != "" { dockerRootDir = dockerInfo.DockerRootDir @@ -394,7 +397,7 @@ func (c WindowsConfigurer) CleanupLingeringMCR(h os.Host, dockerInfo commonconfi c.attemptPathDelete(h, dockerRootDir) } -func (c WindowsConfigurer) attemptPathDelete(h os.Host, path string) { +func (c WindowsConfigurer) attemptPathDelete(h Host, path string) { // Remove a folder using PowerShell command. removeCommand := fmt.Sprintf("powershell Remove-Item -LiteralPath %s -Force -Recurse ", ps.SingleQuote(path)) diff --git a/pkg/configurer/windows/windows_2019.go b/pkg/configurer/windows/windows_2019.go index 1db20aaa..8a8b8b44 100644 --- a/pkg/configurer/windows/windows_2019.go +++ b/pkg/configurer/windows/windows_2019.go @@ -2,8 +2,7 @@ package windows import ( "github.com/Mirantis/launchpad/pkg/configurer" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" ) // Windows2019Configurer is a Windows 2019 configurer implementation. @@ -12,9 +11,9 @@ type Windows2019Configurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "windows" && os.Version == "10.0.17763" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "windows" && r.Version == "10.0.17763" }, func() any { return Windows2019Configurer{} diff --git a/pkg/configurer/windows/windows_2022.go b/pkg/configurer/windows/windows_2022.go index 90fdcfec..276e0339 100644 --- a/pkg/configurer/windows/windows_2022.go +++ b/pkg/configurer/windows/windows_2022.go @@ -2,8 +2,7 @@ package windows import ( "github.com/Mirantis/launchpad/pkg/configurer" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" ) // Windows2022Configurer is a Windows 2022 configurer implementation. @@ -12,9 +11,9 @@ type Windows2022Configurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "windows" && os.Version == "10.0.20348" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "windows" && r.Version == "10.0.20348" }, func() any { return Windows2022Configurer{} diff --git a/pkg/configurer/windows/windows_2025.go b/pkg/configurer/windows/windows_2025.go index 3765e7fc..dd24a84f 100644 --- a/pkg/configurer/windows/windows_2025.go +++ b/pkg/configurer/windows/windows_2025.go @@ -2,8 +2,7 @@ package windows import ( "github.com/Mirantis/launchpad/pkg/configurer" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/os/registry" + rigos "github.com/k0sproject/rig/v2/os" ) // Windows2025Configurer is a Windows 2025 configurer implementation. @@ -12,9 +11,9 @@ type Windows2025Configurer struct { } func init() { - registry.RegisterOSModule( - func(os rig.OSVersion) bool { - return os.ID == "windows" && os.Version == "10.0.26100" + configurer.RegisterOSModule( + func(r *rigos.Release) bool { + return r.ID == "windows" && r.Version == "10.0.26100" }, func() any { return Windows2025Configurer{} diff --git a/pkg/docker/image.go b/pkg/docker/image.go index 312764ea..3afa3488 100644 --- a/pkg/docker/image.go +++ b/pkg/docker/image.go @@ -94,14 +94,14 @@ func (i *Image) Exist(h *mkeconfig.Host) bool { // PullImages pulls multiple images parallelly by using a worker pool. func PullImages(h *mkeconfig.Host, images []*Image) error { - wp := workerpool.New(5) + pool := workerpool.New(5) var mutex sync.Mutex var lastError error for _, image := range images { i := image // So we can safely pass i forward to pool without it getting mutated - wp.Submit(func() { + pool.Submit(func() { if err := i.Pull(h); err != nil { mutex.Lock() if lastError == nil { @@ -115,7 +115,7 @@ func PullImages(h *mkeconfig.Host, images []*Image) error { // Wait for all workers to complete before reading lastError. // A deferred StopWait() would let the return expression evaluate // before workers finish, potentially returning nil on a real error. - wp.StopWait() + pool.StopWait() return lastError } diff --git a/pkg/mke/bootstrap.go b/pkg/mke/bootstrap.go index 8f6d0ad1..034493a3 100644 --- a/pkg/mke/bootstrap.go +++ b/pkg/mke/bootstrap.go @@ -9,14 +9,14 @@ import ( commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/util/cmdbuffer" - "github.com/k0sproject/rig/exec" + rigcmd "github.com/k0sproject/rig/v2/cmd" ) // BootstrapOptions configure options for the Bootstrap. type BootstrapOptions struct { - OperationFlags commonconfig.Flags // OPTIONAL: flags to pass to the bootstrapper command - CleanupDisabled bool // OPTIONAL: if true, then the bootstrapper container will not be removed - ExecOptions []exec.Option // OPTIONAL: additional rig exec options to pass down to rig + OperationFlags commonconfig.Flags // OPTIONAL: flags to pass to the bootstrapper command + CleanupDisabled bool // OPTIONAL: if true, then the bootstrapper container will not be removed + ExecOptions []rigcmd.ExecOption // OPTIONAL: additional rig exec options to pass down to rig } // Bootstrap a leader host using the MKE bootsrapper as docker run, returning output. diff --git a/pkg/mke/mke.go b/pkg/mke/mke.go index beef6d47..bbdb8aa0 100644 --- a/pkg/mke/mke.go +++ b/pkg/mke/mke.go @@ -24,7 +24,7 @@ import ( commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/hashicorp/go-version" - "github.com/k0sproject/rig/exec" + rigcmd "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -159,7 +159,7 @@ func GetTLSConfigFrom(manager *mkeconfig.Host, imageRepo, mkeVersion string) (*t if manager.Configurer.SELinuxEnabled(manager) { runFlags.Add("--security-opt label=disable") } - output, err := manager.ExecOutput(manager.Configurer.DockerCommandf(`run %s %s/ucp:%s dump-certs --ca`, runFlags.Join(), imageRepo, mkeVersion), exec.Redact(`[A-Za-z0-9+/=_\-]{64}`)) + output, err := manager.ExecOutput(manager.Configurer.DockerCommandf(`run %s %s/ucp:%s dump-certs --ca`, runFlags.Join(), imageRepo, mkeVersion), rigcmd.Redact(`[A-Za-z0-9+/=_\-]{64}`)) if err != nil { return nil, fmt.Errorf("%w: error while exec-ing into the container: %w", errGetTLSConfig, err) } diff --git a/pkg/msr/bootstrap.go b/pkg/msr/bootstrap.go index ad053cbb..b66e90a8 100644 --- a/pkg/msr/bootstrap.go +++ b/pkg/msr/bootstrap.go @@ -9,15 +9,15 @@ import ( commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/util/cmdbuffer" - "github.com/k0sproject/rig/exec" + rigcmd "github.com/k0sproject/rig/v2/cmd" "github.com/sirupsen/logrus" ) // BootstrapOptions configure options for the Bootstrap. type BootstrapOptions struct { - OperationFlags commonconfig.Flags // OPTIONAL: flags to pass to the bootstrapper command - CleanupDisabled bool // OPTIONAL: if true, then the bootstrapper container will not be removed - ExecOptions []exec.Option // OPTIONAL: additional rig exec options to pass down to rig + OperationFlags commonconfig.Flags // OPTIONAL: flags to pass to the bootstrapper command + CleanupDisabled bool // OPTIONAL: if true, then the bootstrapper container will not be removed + ExecOptions []rigcmd.ExecOption // OPTIONAL: additional rig exec options to pass down to rig } // Bootstrap a leader host using the MKE bootsrapper as docker run, returning output. diff --git a/pkg/product/common/phase/connect.go b/pkg/product/common/phase/connect.go index 36bc3a18..04c3ff86 100644 --- a/pkg/product/common/phase/connect.go +++ b/pkg/product/common/phase/connect.go @@ -1,6 +1,7 @@ package phase import ( + "context" "errors" "fmt" "reflect" @@ -8,15 +9,15 @@ import ( "time" retry "github.com/avast/retry-go" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) type connectable interface { - Connect() error + Connect(context.Context) error String() string - Exec(cmd string, opts ...exec.Option) error + Exec(cmd string, opts ...cmd.ExecOption) error } // Connect connects to each of the hosts. @@ -82,7 +83,7 @@ const retries = 60 func (p *Connect) connectHost(host connectable) error { err := retry.Do( func() error { - if err := host.Connect(); err != nil { + if err := host.Connect(context.Background()); err != nil { return fmt.Errorf("connect: %w", err) } return nil @@ -94,7 +95,7 @@ func (p *Connect) connectHost(host connectable) error { ), retry.RetryIf( func(err error) bool { - return !errors.Is(err, rig.ErrCantConnect) + return !errors.Is(err, rig.ErrNonRetryable) }, ), retry.DelayType(retry.CombineDelay(retry.FixedDelay, retry.RandomDelay)), diff --git a/pkg/product/common/phase/run_hooks_test.go b/pkg/product/common/phase/run_hooks_test.go index 63b57818..b16e5248 100644 --- a/pkg/product/common/phase/run_hooks_test.go +++ b/pkg/product/common/phase/run_hooks_test.go @@ -5,7 +5,7 @@ import ( "testing" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" "github.com/stretchr/testify/require" ) @@ -27,11 +27,11 @@ func (t *testhost) String() string { return "foo" } -func (t *testhost) Exec(cmd string, opts ...exec.Option) error { +func (t *testhost) Exec(command string, opts ...cmd.ExecOption) error { return nil } -func (t *testhost) ExecOutput(cmd string, opts ...exec.Option) (string, error) { +func (t *testhost) ExecOutput(command string, opts ...cmd.ExecOption) (string, error) { return "", nil } diff --git a/pkg/product/mke/apply.go b/pkg/product/mke/apply.go index 87b9b2c7..4271f41e 100644 --- a/pkg/product/mke/apply.go +++ b/pkg/product/mke/apply.go @@ -18,7 +18,6 @@ func (p *MKE) Apply(disableCleanup, force bool, concurrency int, forceUpgrade bo phaseManager.AddPhases( &mke.UpgradeCheck{}, - &mke.OverrideHostSudo{}, &common.Connect{}, &mke.DetectOS{}, &mke.GatherFacts{}, diff --git a/pkg/product/mke/client_config.go b/pkg/product/mke/client_config.go index 58379523..2463451f 100644 --- a/pkg/product/mke/client_config.go +++ b/pkg/product/mke/client_config.go @@ -18,7 +18,6 @@ func (p *MKE) ClientConfig() error { phaseManager := phase.NewManager(&p.ClusterConfig) phaseManager.AddPhases( - &de.OverrideHostSudo{}, &common.Connect{}, &de.DetectOS{}, &de.GatherFacts{}, diff --git a/pkg/product/mke/config/cluster.go b/pkg/product/mke/config/cluster.go index ce6c8690..7d4c28d7 100644 --- a/pkg/product/mke/config/cluster.go +++ b/pkg/product/mke/config/cluster.go @@ -7,7 +7,8 @@ import ( "github.com/Mirantis/launchpad/pkg/docker/hub" common "github.com/Mirantis/launchpad/pkg/product/common/config" validator "github.com/go-playground/validator/v10" - "github.com/k0sproject/rig" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/protocol/ssh" ) // ClusterMeta defines cluster metadata. @@ -85,8 +86,8 @@ func Init(kind string) *ClusterConfig { Hosts: []*Host{ { Role: "manager", - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "10.0.0.1", User: "root", Port: 22, @@ -95,8 +96,8 @@ func Init(kind string) *ClusterConfig { }, { Role: "worker", - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "10.0.0.2", User: "root", Port: 22, @@ -119,8 +120,8 @@ func Init(kind string) *ClusterConfig { config.Spec.Hosts = append(config.Spec.Hosts, &Host{ Role: "msr", - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "10.0.0.2", User: "root", Port: 22, diff --git a/pkg/product/mke/config/cluster_spec.go b/pkg/product/mke/config/cluster_spec.go index 990575e1..346859f4 100644 --- a/pkg/product/mke/config/cluster_spec.go +++ b/pkg/product/mke/config/cluster_spec.go @@ -14,7 +14,7 @@ import ( common "github.com/Mirantis/launchpad/pkg/product/common/config" retry "github.com/avast/retry-go" "github.com/creasty/defaults" - "github.com/k0sproject/rig" + "github.com/k0sproject/rig/v2/protocol/ssh" log "github.com/sirupsen/logrus" ) @@ -194,7 +194,7 @@ func (c *ClusterSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { }) if len(bastionHosts) > 0 { log.Debugf("linking bastion hosts") - bastions := make(map[string]*rig.SSH) + bastions := make(map[string]*ssh.Config) for _, h := range bastionHosts { if h.WinRM != nil { id := fmt.Sprintf("%s@%s:%d", h.WinRM.User, h.WinRM.Address, h.WinRM.Port) diff --git a/pkg/product/mke/config/cluster_spec_test.go b/pkg/product/mke/config/cluster_spec_test.go index 5724adc5..558415eb 100644 --- a/pkg/product/mke/config/cluster_spec_test.go +++ b/pkg/product/mke/config/cluster_spec_test.go @@ -3,13 +3,14 @@ package config import ( "testing" - "github.com/k0sproject/rig" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/protocol/ssh" "github.com/stretchr/testify/require" ) var manager = &Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "192.168.1.2", }, }, @@ -17,8 +18,8 @@ var manager = &Host{ } var msr = &Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "192.168.1.3", }, }, @@ -107,8 +108,8 @@ func TestMKEClusterSpecMSRURLWithoutExternalURL(t *testing.T) { Hosts: []*Host{ manager, { - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "192.168.1.3", }, }, diff --git a/pkg/product/mke/config/configurer.go b/pkg/product/mke/config/configurer.go index 3375e493..8b077cbc 100644 --- a/pkg/product/mke/config/configurer.go +++ b/pkg/product/mke/config/configurer.go @@ -1,38 +1,30 @@ package config import ( + "github.com/Mirantis/launchpad/pkg/configurer" common "github.com/Mirantis/launchpad/pkg/product/common/config" - "github.com/k0sproject/rig/os" ) // HostConfigurer defines the interface each host OS specific configurers implement. // This is under api because it has direct deps to api structs. type HostConfigurer interface { - CheckPrivilege(os.Host) error - Hostname(os.Host) string - LongHostname(os.Host) string - ResolvePrivateInterface(os.Host) (string, error) - ResolveInternalIP(os.Host, string, string) (string, error) - SELinuxEnabled(os.Host) bool - UpdateEnvironment(os.Host, map[string]string) error - CleanupEnvironment(os.Host, map[string]string) error + CheckPrivilege(configurer.Host) error + ResolvePrivateInterface(configurer.Host) (string, error) + ResolveInternalIP(configurer.Host, string, string) (string, error) + SELinuxEnabled(configurer.Host) bool + UpdateEnvironment(configurer.Host, map[string]string) error + CleanupEnvironment(configurer.Host, map[string]string) error MCRConfigPath() string - InstallMCRLicense(os.Host, string) error - InstallMCR(os.Host, common.MCRConfig) error - UninstallMCR(os.Host, common.MCRConfig) error + InstallMCRLicense(configurer.Host, string) error + InstallMCR(configurer.Host, common.MCRConfig) error + UninstallMCR(configurer.Host, common.MCRConfig) error DockerCommandf(template string, args ...any) string - RestartMCR(os.Host) error - AuthenticateDocker(h os.Host, user, pass, repo string) error - LocalAddresses(os.Host) ([]string, error) - ValidateLocalhost(os.Host) error - WriteFile(os.Host, string, string, string) error - ReadFile(os.Host, string) (string, error) - DeleteFile(os.Host, string) error - FileExist(os.Host, string) bool - HTTPStatus(os.Host, string) (int, error) - Pwd(os.Host) string - JoinPath(...string) string - Reboot(os.Host) error - AuthorizeDocker(os.Host) error - PrepareHost(os.Host) error + RestartMCR(configurer.Host) error + AuthenticateDocker(h configurer.Host, user, pass, repo string) error + LocalAddresses(configurer.Host) ([]string, error) + ValidateLocalhost(configurer.Host) error + Pwd(configurer.Host) string + Reboot(configurer.Host) error + AuthorizeDocker(configurer.Host) error + PrepareHost(configurer.Host) error } diff --git a/pkg/product/mke/config/confirm.go b/pkg/product/mke/config/confirm.go new file mode 100644 index 00000000..b4fe779e --- /dev/null +++ b/pkg/product/mke/config/confirm.go @@ -0,0 +1,39 @@ +package config + +import ( + "bufio" + "fmt" + "os" + "strings" + "sync" +) + +// ConfirmCommands controls whether Connect installs a confirmation gate that +// prompts on stdin before every command runs on a host. It is set from the +// global --confirm CLI flag and mirrors the behaviour of rig v1's exec.Confirm. +var ConfirmCommands bool + +// confirmMu serializes prompts so that parallel host operations do not +// interleave their questions on the shared stdin/stderr. +var confirmMu sync.Mutex + +// confirmCommand prompts on stderr for approval of command on host and reads the +// answer from stdin. It returns true when the command may run (an empty answer +// or "y"/"yes" allows it). The command is the fully decorated, redacted form +// that rig is about to execute. +func confirmCommand(host, command string) bool { + confirmMu.Lock() + defer confirmMu.Unlock() + + fmt.Fprintf(os.Stderr, "\nHost: %s\nCommand: %s\nAllow? [Y/n]: ", host, command) + + answer, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + // Treat an unreadable stdin (e.g. EOF from a closed pipe) as a refusal + // so we never run an unconfirmed command. + return false + } + answer = strings.TrimSpace(answer) + + return answer == "" || strings.EqualFold(answer, "y") || strings.EqualFold(answer, "yes") +} diff --git a/pkg/product/mke/config/host.go b/pkg/product/mke/config/host.go index e8565eec..7f44e3a2 100644 --- a/pkg/product/mke/config/host.go +++ b/pkg/product/mke/config/host.go @@ -1,27 +1,43 @@ package config import ( + "context" "encoding/json" "errors" "fmt" "io" "io/fs" + "log/slog" "os" "reflect" "strings" "time" + "github.com/Mirantis/launchpad/pkg/configurer" common "github.com/Mirantis/launchpad/pkg/product/common/config" "github.com/Mirantis/launchpad/pkg/util/byteutil" retry "github.com/avast/retry-go" "github.com/creasty/defaults" "github.com/k0sproject/dig" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" - "github.com/k0sproject/rig/os/registry" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/cmd" + rigos "github.com/k0sproject/rig/v2/os" + "github.com/k0sproject/rig/v2/protocol" + "github.com/k0sproject/rig/v2/remotefs" + "github.com/k0sproject/rig/v2/sudo" + sloglogrus "github.com/samber/slog-logrus/v2" log "github.com/sirupsen/logrus" ) +// rigLogger bridges rig v2's slog-based logging into launchpad's logrus output. +// It wraps the logrus standard logger, so any level and hook changes applied +// there are reflected automatically. rig v2 has no global logger setter; the +// logger is injected per client via rig.WithLogger at Connect time. +var rigLogger = slog.New(sloglogrus.Option{ + Level: slog.LevelDebug, + Logger: log.StandardLogger(), +}.NewLogrusHandler()) + // HostMetadata resolved metadata for host. type HostMetadata struct { Hostname string @@ -69,7 +85,8 @@ func (errors *errs) String() string { // Host contains all the needed details to work with hosts. type Host struct { - rig.Connection `yaml:",inline"` + rig.CompositeConfig `yaml:",inline"` + *rig.Client `yaml:"-"` Role string `yaml:"role" validate:"oneof=manager worker msr"` PrivateInterface string `yaml:"privateInterface,omitempty" validate:"omitempty,gt=2"` @@ -86,14 +103,16 @@ type Host struct { // private NIC IP is not routable across DCs but the SSH/floating address is. SwarmAddressOverride string `yaml:"swarmAddress,omitempty"` - Metadata *HostMetadata `yaml:"-"` MSRMetadata *MSRMetadata `yaml:"-"` Configurer HostConfigurer `yaml:"-"` + OSRelease *rigos.Release `yaml:"-"` Errors errs `yaml:"-"` } // UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml. +// The alias type prevents recursion; the client is reset so Connect re-creates +// it with any updated connection config. func (h *Host) UnmarshalYAML(unmarshal func(interface{}) error) error { type host Host yh := (*host)(h) @@ -102,16 +121,98 @@ func (h *Host) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - if yh.SSH != nil && yh.SSH.HostKey != "" { - log.Warnf("%s: spec.hosts[*].ssh.hostKey is deprecated, please use ssh known hosts file instead (.ssh/config, SSH_KNOWN_HOSTS)", h) - } - if err := defaults.Set(yh); err != nil { return fmt.Errorf("failed to set host defaults: %w", err) } + + if h.Client != nil { + h.Disconnect() + h.Client = nil + } + return nil } +// Connect establishes the connection to the host, injecting launchpad's logger +// so that rig's internal logging is routed into logrus. When the host has +// sudooverride set, the sudo detection is bypassed and every privileged command +// is wrapped with sudo unconditionally. +func (h *Host) Connect(ctx context.Context) error { + if h.Client == nil { + opts := []rig.ClientOption{ + rig.WithConnectionFactory(&h.CompositeConfig), + rig.WithLogger(rigLogger), + } + if ConfirmCommands { + opts = append(opts, rig.WithConfirmFunc(confirmCommand)) + } + if h.SudoOverride { + opts = append(opts, rig.WithSudoProvider(func(runner cmd.Runner) (cmd.Runner, error) { + return cmd.NewExecutor(runner, sudo.Sudo), nil + })) + } + client, err := rig.NewClient(opts...) + if err != nil { + return fmt.Errorf("create rig client: %w", err) + } + h.Client = client + } + if err := h.Client.Connect(ctx); err != nil { + return fmt.Errorf("connect: %w", err) + } + return nil +} + +// String returns a human-readable description of the host, safe before Connect. +func (h *Host) String() string { + if h.Client != nil { + return h.Client.String() + } + return h.CompositeConfig.String() +} + +// Protocol returns the host communication protocol family. +func (h *Host) Protocol() string { + if h.Client != nil { + return h.Client.Protocol() + } + if h.WinRM != nil { + return "WinRM" + } + if bool(h.Localhost) { + return "Local" + } + return "SSH" +} + +// Address returns the host address, falling back to the configured connection +// address when the client is not yet connected. +func (h *Host) Address() string { + if h.Client != nil { + if addr := h.Client.Address(); addr != "" { + return addr + } + } + if h.SSH != nil && h.SSH.Address != "" { + return h.SSH.Address + } + if h.WinRM != nil && h.WinRM.Address != "" { + return h.WinRM.Address + } + if h.OpenSSH != nil && h.OpenSSH.Address != "" { + return h.OpenSSH.Address + } + return "127.0.0.1" +} + +// IsWindows returns true when the detected OS is Windows. +func (h *Host) IsWindows() bool { + if h.Client != nil { + return h.Client.IsWindows() + } + return h.WinRM != nil +} + // IsLocal returns true for localhost connections. func (h *Host) IsLocal() bool { return h.Protocol() == "Local" @@ -125,6 +226,15 @@ func (h *Host) IsSudoCommand(cmd string) bool { return false } +// runner returns the client used to execute cmd, using the sudo-decorated +// client clone when the command must run with elevated privileges. +func (h *Host) runner(cmd string) *rig.Client { + if h.IsSudoCommand(cmd) { + return h.Sudo() + } + return h.Client +} + // AuthorizeDocker if needed. func (h *Host) AuthorizeDocker() error { if h.SudoDocker { @@ -135,11 +245,42 @@ func (h *Host) AuthorizeDocker() error { return h.Configurer.AuthorizeDocker(h) //nolint:wrapcheck } -func (h *Host) sudoCommandOptions(cmd string, opts []exec.Option) []exec.Option { - if h.IsSudoCommand(cmd) { - opts = append(opts, exec.Sudo(h)) +// ExecStreams runs a command with the given stdin/stdout/stderr streams and +// returns a protocol.Waiter whose Wait blocks until the command exits. It is a +// thin wrapper over rig's cmd.Proc that adds the SudoDocker routing (see +// runner); callers that do not need that routing can use h.Proc directly. +func (h *Host) ExecStreams(cmd string, stdin io.Reader, stdout, stderr io.Writer, opts ...cmd.ExecOption) (protocol.Waiter, error) { //nolint:ireturn + proc := h.runner(cmd).Proc(cmd) + proc.Stdin = stdin + proc.Stdout = stdout + proc.Stderr = stderr + waiter, err := proc.Start(context.Background(), opts...) + if err != nil { + return nil, fmt.Errorf("failed to start command: %w", err) + } + return waiter, nil +} + +// Exec runs a command on the host. It delegates to rig's runner, selecting the +// sudo-decorated client clone for commands that must run privileged (see runner +// / SudoDocker). +func (h *Host) Exec(cmd string, opts ...cmd.ExecOption) error { + return h.runner(cmd).Exec(cmd, opts...) //nolint:wrapcheck +} + +// ExecOutput runs a command on the host and returns its output, applying the +// same SudoDocker routing as Exec. +func (h *Host) ExecOutput(cmd string, opts ...cmd.ExecOption) (string, error) { + return h.runner(cmd).ExecOutput(cmd, opts...) //nolint:wrapcheck +} + +// ExecInteractive runs a command (or an interactive shell when cmd is empty) +// on the host, wired to the local standard streams. +func (h *Host) ExecInteractive(cmd string) error { + if err := h.Client.ExecInteractive(context.Background(), cmd, os.Stdin, os.Stdout, os.Stderr); err != nil { + return fmt.Errorf("interactive exec failed: %w", err) } - return opts + return nil } // ExecAll execs a slice of commands on the host. @@ -158,22 +299,6 @@ func (h *Host) ExecAll(cmds []string) error { return nil } -// ExecStreams executes a command on the remote host and uses the passed in streams for stdin, stdout and stderr. It returns a Waiter with a .Wait() function that -// blocks until the command finishes and returns an error if the exit code is not zero. -func (h *Host) ExecStreams(cmd string, stdin io.ReadCloser, stdout, stderr io.Writer, opts ...exec.Option) (exec.Waiter, error) { //nolint:ireturn - return h.Connection.ExecStreams(cmd, stdin, stdout, stderr, h.sudoCommandOptions(cmd, opts)...) //nolint:wrapcheck -} - -// Exec runs a command on the host. -func (h *Host) Exec(cmd string, opts ...exec.Option) error { - return h.Connection.Exec(cmd, h.sudoCommandOptions(cmd, opts)...) //nolint:wrapcheck -} - -// ExecOutput runs a command on the host and returns the output as a String. -func (h *Host) ExecOutput(cmd string, opts ...exec.Option) (string, error) { - return h.Connection.ExecOutput(cmd, h.sudoCommandOptions(cmd, opts)...) //nolint:wrapcheck -} - var errAuthFailed = errors.New("authentication failed") // AuthenticateDocker performs a docker login on the host using local REGISTRY_USERNAME @@ -221,8 +346,10 @@ func (h *Host) MCRVersion() (string, error) { var errUnexpectedResponse = errors.New("unexpected response") // CheckHTTPStatus will perform a web request to the url and return an error if the http status is not the expected. +// TLS certificate verification is skipped, since these checks target services +// with self-signed certificates (e.g. the MKE controllers). func (h *Host) CheckHTTPStatus(url string, expected int) error { - status, err := h.Configurer.HTTPStatus(h, url) + status, err := remotefs.HTTPStatusInsecure(context.Background(), h.FS(), url) if err != nil { return fmt.Errorf("failed to get http status: %w", err) } @@ -251,7 +378,7 @@ func (h *Host) WriteFileLarge(src, dst string, fmo fs.FileMode) error { log.Infof("%s: uploading %s to %s", h, byteutil.FormatBytes(usize), dst) - if err := h.Upload(src, dst, fmo); err != nil { + if err := remotefs.Upload(h.FS(), src, dst, remotefs.WithPermissions(fmo)); err != nil { return fmt.Errorf("upload failed: %w", err) } @@ -269,7 +396,7 @@ func (h *Host) Reconnect() error { log.Infof("%s: waiting for reconnection", h) err := retry.Do( func() error { - if err := h.Connect(); err != nil { + if err := h.Connect(context.Background()); err != nil { return fmt.Errorf("failed to reconnect: %w", err) } return nil @@ -324,9 +451,9 @@ func (h *Host) ConfigureMCR() error { oldJSON := make(dig.Mapping) - if f, err := h.Configurer.ReadFile(h, cfgPath); err == nil { + if data, err := fs.ReadFile(h.Sudo().FS(), cfgPath); err == nil { log.Debugf("%s: parsing existing daemon.json", h) - if err := json.Unmarshal([]byte(f), &oldJSON); err != nil { + if err := json.Unmarshal(data, &oldJSON); err != nil { log.Debugf("%s: failed to parse existing MCR config: %s", h, err) } } else { @@ -350,13 +477,11 @@ func (h *Host) ConfigureMCR() error { log.Debugf("%s: writing new daemon.json", h) - daemonJSONContent := string(newJSONbytes) - - if err := h.Configurer.DeleteFile(h, cfgPath); err != nil { + if err := h.Sudo().FS().Remove(cfgPath); err != nil { log.Debugf("%s: failed to delete existing daemon.json: %s", h, err) } - if err := h.Configurer.WriteFile(h, cfgPath, daemonJSONContent, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(cfgPath, newJSONbytes, fs.FileMode(0o600)); err != nil { return fmt.Errorf("failed to write daemon.json: %w", err) } @@ -397,9 +522,17 @@ var errUnsupportedOS = errors.New("unsupported OS") // ResolveConfigurer assigns a rig-style configurer to the Host (see configurer/). func (h *Host) ResolveConfigurer() error { - bf, err := registry.GetOSModuleBuilder(*h.OSVersion) - if err != nil { - return fmt.Errorf("%w: failed to get OS module builder: %w", errUnsupportedOS, err) + if h.OSRelease == nil { + release, err := h.OS() + if err != nil { + return fmt.Errorf("%w: OS detection failed: %w", errUnsupportedOS, err) + } + h.OSRelease = release + } + + bf, ok := configurer.ResolveOSModule(h.OSRelease) + if !ok { + return fmt.Errorf("%w: %s", errUnsupportedOS, h.OSRelease.ID) } if c, ok := bf().(HostConfigurer); ok { diff --git a/pkg/product/mke/config/host_test.go b/pkg/product/mke/config/host_test.go index b6d82ca4..0150c9c6 100644 --- a/pkg/product/mke/config/host_test.go +++ b/pkg/product/mke/config/host_test.go @@ -3,14 +3,16 @@ package config import ( "testing" - "github.com/k0sproject/rig" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/protocol/ssh" + "github.com/k0sproject/rig/v2/protocol/winrm" "github.com/stretchr/testify/require" ) func TestHostSwarmAddress(t *testing.T) { h := Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "1.2.3.4", }, }, @@ -22,8 +24,8 @@ func TestHostSwarmAddress(t *testing.T) { require.Equal(t, "1.2.3.4:2377", h.SwarmAddress()) h = Host{ - Connection: rig.Connection{ - WinRM: &rig.WinRM{ + CompositeConfig: rig.CompositeConfig{ + WinRM: &winrm.Config{ Address: "10.0.0.1", }, }, @@ -38,8 +40,8 @@ func TestHostSwarmAddress(t *testing.T) { func TestHostSwarmAddressOverride(t *testing.T) { // When SwarmAddressOverride is set it takes precedence over InternalAddress. h := Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{Address: "172.19.121.30"}, + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{Address: "172.19.121.30"}, }, SwarmAddressOverride: "172.19.121.30", Metadata: &HostMetadata{ @@ -52,8 +54,8 @@ func TestHostSwarmAddressOverride(t *testing.T) { func TestHostSwarmAddressOverrideEmpty(t *testing.T) { // An empty SwarmAddressOverride falls back to InternalAddress. h := Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{Address: "172.19.121.30"}, + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{Address: "172.19.121.30"}, }, SwarmAddressOverride: "", Metadata: &HostMetadata{ @@ -65,8 +67,8 @@ func TestHostSwarmAddressOverrideEmpty(t *testing.T) { func TestHostAddress(t *testing.T) { h := Host{ - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "1.2.3.4", }, }, diff --git a/pkg/product/mke/config/hosts_test.go b/pkg/product/mke/config/hosts_test.go index cccfc11c..3e9fdb33 100644 --- a/pkg/product/mke/config/hosts_test.go +++ b/pkg/product/mke/config/hosts_test.go @@ -1,33 +1,35 @@ package config import ( + "context" "fmt" "testing" - "github.com/k0sproject/rig" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/protocol/ssh" "github.com/stretchr/testify/require" ) var hosts = Hosts{ { - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "man1", }, }, Role: "manager", }, { - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "man2", }, }, Role: "manager", }, { - Connection: rig.Connection{ - SSH: &rig.SSH{ + CompositeConfig: rig.CompositeConfig{ + SSH: &ssh.Config{ Address: "work1", }, }, @@ -170,5 +172,5 @@ func ExampleHosts_Filter() { return h.Role == "manager" }) - managers[0].Connect() + _ = managers[0].Connect(context.Background()) } diff --git a/pkg/product/mke/describe.go b/pkg/product/mke/describe.go index cd8d93e6..3793c61a 100644 --- a/pkg/product/mke/describe.go +++ b/pkg/product/mke/describe.go @@ -35,7 +35,6 @@ func (p *MKE) Describe(reportName string) error { phaseManager.IgnoreErrors = true phaseManager.AddPhases( - &de.OverrideHostSudo{}, &common.Connect{}, &de.DetectOS{}, &de.GatherFacts{}, diff --git a/pkg/product/mke/exec.go b/pkg/product/mke/exec.go index 68383119..ef405d6d 100644 --- a/pkg/product/mke/exec.go +++ b/pkg/product/mke/exec.go @@ -1,6 +1,7 @@ package mke import ( + "context" "errors" "fmt" "io" @@ -10,8 +11,8 @@ import ( "sync" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/k0sproject/rig" - "github.com/k0sproject/rig/exec" + rig "github.com/k0sproject/rig/v2" + rigcmd "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -24,7 +25,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol for _, target := range targets { switch { case target == "localhost": - hosts = append(hosts, &mkeconfig.Host{Connection: rig.Connection{Localhost: &rig.Localhost{Enabled: true}}}) + hosts = append(hosts, &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{Localhost: rig.LocalhostConfig(true)}}) case strings.Contains(target, ":"): parts := strings.SplitN(target, ":", 2) addr := parts[0] @@ -74,7 +75,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol var mutex sync.Mutex err := hosts.ParallelEach(func(host *mkeconfig.Host) error { - if err := host.Connect(); err != nil { + if err := host.Connect(context.Background()); err != nil { return fmt.Errorf("failed to connect to host %s: %w", host.Address(), err) } if err := host.ResolveConfigurer(); err != nil { @@ -87,7 +88,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol mutex.Unlock() } } else { - if hostos == "linux" || host.OSVersion.ID == hostos { + if hostos == "linux" || host.OSRelease.ID == hostos { mutex.Lock() foundhosts = append(foundhosts, host) mutex.Unlock() @@ -144,7 +145,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol } err := hosts.ParallelEach(func(h *mkeconfig.Host) error { - if err := h.Connect(); err != nil { + if err := h.Connect(context.Background()); err != nil { return fmt.Errorf("connect to host %s: %w", h.Address(), err) } return nil @@ -193,7 +194,7 @@ func (p *MKE) Exec(targets []string, interactive, first, all, parallel bool, rol log.Tracef("running non-interactive with cmd: %q", cmd) runFunc := func(h *mkeconfig.Host) error { - if err := h.Exec(cmd, exec.Stdin(stdin), exec.StreamOutput()); err != nil { + if err := h.Exec(cmd, rigcmd.StdinString(stdin), rigcmd.StreamOutput()); err != nil { return fmt.Errorf("failed on host %s: %w", h.Address(), err) } return nil diff --git a/pkg/product/mke/phase/describe.go b/pkg/product/mke/phase/describe.go index 67379e3c..0e314820 100644 --- a/pkg/product/mke/phase/describe.go +++ b/pkg/product/mke/phase/describe.go @@ -103,8 +103,8 @@ func (p *Describe) hostReport() { if h.Metadata.MCRVersion != "" { mcrV = h.Metadata.MCRVersion } - if h.OSVersion.ID != "" { - hostOS = fmt.Sprintf("%s/%s", h.OSVersion.ID, h.OSVersion.Version) + if h.OSRelease != nil && h.OSRelease.ID != "" { + hostOS = fmt.Sprintf("%s/%s", h.OSRelease.ID, h.OSRelease.Version) } if h.Metadata.InternalAddress != "" { internalAddr = h.Metadata.InternalAddress diff --git a/pkg/product/mke/phase/detect_os.go b/pkg/product/mke/phase/detect_os.go index d20f5a92..f7abdcea 100644 --- a/pkg/product/mke/phase/detect_os.go +++ b/pkg/product/mke/phase/detect_os.go @@ -32,7 +32,7 @@ func (p *DetectOS) Run() error { if err := h.ResolveConfigurer(); err != nil { return fmt.Errorf("failed to resolve configurer for %s: %w", h, err) } - os := h.OSVersion.String() + os := h.OSRelease.String() log.Infof("%s: is running %s", h, os) return nil diff --git a/pkg/product/mke/phase/gather_facts.go b/pkg/product/mke/phase/gather_facts.go index b3de162c..c0ab3196 100644 --- a/pkg/product/mke/phase/gather_facts.go +++ b/pkg/product/mke/phase/gather_facts.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "net" // needed to load the build func in package init. @@ -105,10 +106,10 @@ func (p *GatherFacts) investigateHost(h *mkeconfig.Host, _ *mkeconfig.ClusterCon log.Infof("%s: mirantis container runtime not installed", h) } else { log.Infof("%s: is running mirantis container runtime version %s", h, version) - configData, err := h.Configurer.ReadFile(h, "/etc/docker/daemon.json") + configData, err := fs.ReadFile(h.Sudo().FS(), "/etc/docker/daemon.json") if err == nil { var newCfg dig.Mapping - if err = json.Unmarshal([]byte(configData), &newCfg); err == nil { + if err = json.Unmarshal(configData, &newCfg); err == nil { for k, v := range newCfg { if _, ok := h.DaemonConfig[k]; !ok { log.Debugf("%s: set %s = %t for spec.hosts[].daemonConfig from existing daemon.json", h, k, v) @@ -121,8 +122,8 @@ func (p *GatherFacts) investigateHost(h *mkeconfig.Host, _ *mkeconfig.ClusterCon h.Metadata.MCRVersion = version - h.Metadata.Hostname = h.Configurer.Hostname(h) - h.Metadata.LongHostname = h.Configurer.LongHostname(h) + h.Metadata.Hostname, _ = h.FS().Hostname() + h.Metadata.LongHostname, _ = h.FS().LongHostname() if h.PrivateInterface == "" { i, err := h.Configurer.ResolvePrivateInterface(h) @@ -142,7 +143,9 @@ func (p *GatherFacts) investigateHost(h *mkeconfig.Host, _ *mkeconfig.ClusterCon } h.Metadata.InternalAddress = a - log.Infof("%s: is running \"%s\"", h, h.OSVersion.String()) + if h.OSRelease != nil { + log.Infof("%s: is running \"%s\"", h, h.OSRelease.String()) + } log.Infof("%s: internal address: %s", h, h.Metadata.InternalAddress) log.Infof("%s: gathered all facts", h) diff --git a/pkg/product/mke/phase/init_swarm.go b/pkg/product/mke/phase/init_swarm.go index 222872a4..0f30fa9c 100644 --- a/pkg/product/mke/phase/init_swarm.go +++ b/pkg/product/mke/phase/init_swarm.go @@ -5,7 +5,7 @@ import ( "github.com/Mirantis/launchpad/pkg/phase" "github.com/Mirantis/launchpad/pkg/swarm" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -26,7 +26,7 @@ func (p *InitSwarm) Run() error { if !swarm.IsSwarmNode(swarmLeader) { log.Infof("%s: initializing swarm", swarmLeader) - err := swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("swarm init --advertise-addr=%s %s", swarmLeader.SwarmAddress(), p.Config.Spec.MCR.SwarmInstallFlags.Join()), exec.Redact(`--token \S+`)) + err := swarmLeader.Exec(swarmLeader.Configurer.DockerCommandf("swarm init --advertise-addr=%s %s", swarmLeader.SwarmAddress(), p.Config.Spec.MCR.SwarmInstallFlags.Join()), cmd.Redact(`--token \S+`)) if err != nil { return fmt.Errorf("failed to initialize swarm: %w", err) } @@ -49,13 +49,13 @@ func (p *InitSwarm) Run() error { } } - mgrToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token manager -q"), exec.HideOutput()) + mgrToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token manager -q"), cmd.HideOutput()) if err != nil { return fmt.Errorf("%s: failed to get swarm manager join-token: %w", swarmLeader, err) } p.Config.Spec.MCR.Metadata.ManagerJoinToken = mgrToken - workerToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token worker -q"), exec.HideOutput()) + workerToken, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf("swarm join-token worker -q"), cmd.HideOutput()) if err != nil { return fmt.Errorf("%s: failed to get swarm worker join-token: %w", swarmLeader, err) } diff --git a/pkg/product/mke/phase/install_mke.go b/pkg/product/mke/phase/install_mke.go index d514fc86..44d56856 100644 --- a/pkg/product/mke/phase/install_mke.go +++ b/pkg/product/mke/phase/install_mke.go @@ -3,6 +3,7 @@ package phase import ( "errors" "fmt" + "io/fs" "regexp" "strings" @@ -11,7 +12,7 @@ import ( "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/util/installutil" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -61,7 +62,7 @@ func (p *InstallMKE) Run() error { installFlags.AddUnlessExist("--existing-config") log.Info("Creating MKE configuration") configCmd := p.leader.Configurer.DockerCommandf("config create %s -", configName) - err := p.leader.Exec(configCmd, exec.Stdin(p.Config.Spec.MKE.ConfigData)) + err := p.leader.Exec(configCmd, cmd.StdinString(p.Config.Spec.MKE.ConfigData)) if err != nil { return fmt.Errorf("%s: failed to create MKE configuration: %w", p.leader, err) } @@ -100,7 +101,7 @@ func (p *InstallMKE) Run() error { } log.Debugf("%s: install flags: %s", p.leader, installFlags.Join()) - output, err := mke.Bootstrap("install", *p.Config, mke.BootstrapOptions{OperationFlags: installFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: []exec.Option{exec.StreamOutput(), exec.RedactString(p.Config.Spec.MKE.AdminUsername, p.Config.Spec.MKE.AdminPassword)}}) + output, err := mke.Bootstrap("install", *p.Config, mke.BootstrapOptions{OperationFlags: installFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: []cmd.ExecOption{cmd.StreamOutput(), cmd.Redact(p.Config.Spec.MKE.AdminUsername), cmd.Redact(p.Config.Spec.MKE.AdminPassword)}}) if err != nil { return fmt.Errorf("%s: failed to run MKE installer: \n output: %s \n error: %w", p.leader, output, err) } @@ -149,7 +150,7 @@ func applyCloudConfig(config *mkeconfig.ClusterConfig) error { } log.Infof("%s: copying cloud provider (%s) config to %s", h, provider, destFile) - if err := h.Configurer.WriteFile(h, destFile, configData, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(destFile, []byte(configData), fs.FileMode(0o600)); err != nil { return fmt.Errorf("%s: failed to write cloud provider config: %w", h, err) } return nil diff --git a/pkg/product/mke/phase/install_mke_certs.go b/pkg/product/mke/phase/install_mke_certs.go index 315d2a9c..757cb9d0 100644 --- a/pkg/product/mke/phase/install_mke_certs.go +++ b/pkg/product/mke/phase/install_mke_certs.go @@ -2,6 +2,7 @@ package phase import ( "fmt" + "io/fs" "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" @@ -54,13 +55,13 @@ func (p *InstallMKECerts) installCertificates(config *mkeconfig.ClusterConfig) e } log.Infof("%s: installing certificate files to %s", h, dir) - if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "ca.pem"), config.Spec.MKE.CACertData, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(h.FS().Join(dir, "ca.pem"), []byte(config.Spec.MKE.CACertData), fs.FileMode(0o600)); err != nil { return fmt.Errorf("write ca.pem: %w", err) } - if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "cert.pem"), config.Spec.MKE.CertData, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(h.FS().Join(dir, "cert.pem"), []byte(config.Spec.MKE.CertData), fs.FileMode(0o600)); err != nil { return fmt.Errorf("write cert.pem: %w", err) } - if err := h.Configurer.WriteFile(h, h.Configurer.JoinPath(dir, "key.pem"), config.Spec.MKE.KeyData, "0600"); err != nil { + if err := h.Sudo().FS().WriteFile(h.FS().Join(dir, "key.pem"), []byte(config.Spec.MKE.KeyData), fs.FileMode(0o600)); err != nil { return fmt.Errorf("write key.pem: %w", err) } diff --git a/pkg/product/mke/phase/install_msr.go b/pkg/product/mke/phase/install_msr.go index b807f287..c708c10a 100644 --- a/pkg/product/mke/phase/install_msr.go +++ b/pkg/product/mke/phase/install_msr.go @@ -3,11 +3,11 @@ package phase import ( "fmt" - "al.essio.dev/pkg/shellescape" "github.com/Mirantis/launchpad/pkg/msr" "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" + "github.com/k0sproject/rig/v2/sh/shellescape" log "github.com/sirupsen/logrus" ) @@ -85,7 +85,11 @@ func (p *InstallMSR) Run() error { log.Infof("%s: installing MSR version %s", h, p.Config.Spec.MSR.Version) } - if _, err := msr.Bootstrap("install", *p.Config, msr.BootstrapOptions{OperationFlags: installFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: []exec.Option{exec.RedactString(redacts...)}}); err != nil { + execOpts := make([]cmd.ExecOption, 0, len(redacts)) + for _, r := range redacts { + execOpts = append(execOpts, cmd.Redact(r)) + } + if _, err := msr.Bootstrap("install", *p.Config, msr.BootstrapOptions{OperationFlags: installFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: execOpts}); err != nil { return fmt.Errorf("%s: failed to run MSR installer: %w", h, err) } diff --git a/pkg/product/mke/phase/join_controllers.go b/pkg/product/mke/phase/join_controllers.go index 4dbcbdb8..2318ca90 100644 --- a/pkg/product/mke/phase/join_controllers.go +++ b/pkg/product/mke/phase/join_controllers.go @@ -5,7 +5,7 @@ import ( "github.com/Mirantis/launchpad/pkg/phase" "github.com/Mirantis/launchpad/pkg/swarm" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -31,7 +31,7 @@ func (p *JoinManagers) Run() error { } joinCmd := h.Configurer.DockerCommandf("swarm join --advertise-addr=%s --token %s %s", h.SwarmAddress(), p.Config.Spec.MCR.Metadata.ManagerJoinToken, swarmLeader.SwarmAddress()) log.Debugf("%s: joining as manager", h) - err := h.Exec(joinCmd, exec.StreamOutput(), exec.RedactString(p.Config.Spec.MCR.Metadata.ManagerJoinToken)) + err := h.Exec(joinCmd, cmd.StreamOutput(), cmd.Redact(p.Config.Spec.MCR.Metadata.ManagerJoinToken)) if err != nil { return fmt.Errorf("%s: failed to join manager node to swarm: %w", h, err) } diff --git a/pkg/product/mke/phase/join_msr_replicas.go b/pkg/product/mke/phase/join_msr_replicas.go index 1670f358..c570f0a6 100644 --- a/pkg/product/mke/phase/join_msr_replicas.go +++ b/pkg/product/mke/phase/join_msr_replicas.go @@ -3,12 +3,12 @@ package phase import ( "fmt" - "al.essio.dev/pkg/shellescape" "github.com/Mirantis/launchpad/pkg/msr" "github.com/Mirantis/launchpad/pkg/phase" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" + "github.com/k0sproject/rig/v2/sh/shellescape" log "github.com/sirupsen/logrus" ) @@ -93,7 +93,12 @@ func (p *JoinMSRReplicas) Run() error { } joinCmd := msrLeader.Configurer.DockerCommandf("run %s %s join %s", runFlags.Join(), msrLeader.MSRMetadata.InstalledBootstrapImage, joinFlags.Join()) - err := msrLeader.Exec(joinCmd, exec.StreamOutput(), exec.RedactString(redacts...)) + execOpts := make([]cmd.ExecOption, 0, 1+len(redacts)) + execOpts = append(execOpts, cmd.StreamOutput()) + for _, r := range redacts { + execOpts = append(execOpts, cmd.Redact(r)) + } + err := msrLeader.Exec(joinCmd, execOpts...) if err != nil { return fmt.Errorf("%s: failed to run MSR join: %w", h, err) } diff --git a/pkg/product/mke/phase/join_workers.go b/pkg/product/mke/phase/join_workers.go index e161f345..bbb0c468 100644 --- a/pkg/product/mke/phase/join_workers.go +++ b/pkg/product/mke/phase/join_workers.go @@ -1,13 +1,14 @@ package phase import ( + "context" "fmt" "time" "github.com/Mirantis/launchpad/pkg/phase" "github.com/Mirantis/launchpad/pkg/swarm" retry "github.com/avast/retry-go" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -35,7 +36,7 @@ func (p *JoinWorkers) Run() error { } joinCmd := h.Configurer.DockerCommandf("swarm join --advertise-addr=%s --token %s %s", h.SwarmAddress(), p.Config.Spec.MCR.Metadata.WorkerJoinToken, swarmLeader.SwarmAddress()) log.Debugf("%s: joining as worker", h) - err := h.Exec(joinCmd, exec.RedactString(p.Config.Spec.MCR.Metadata.WorkerJoinToken)) + err := h.Exec(joinCmd, cmd.Redact(p.Config.Spec.MCR.Metadata.WorkerJoinToken)) if err != nil { return fmt.Errorf("failed to join worker %s node to swarm: %w", h, err) } @@ -49,7 +50,7 @@ func (p *JoinWorkers) Run() error { err = retry.Do( func() error { h.Disconnect() - err = h.Connect() + err = h.Connect(context.Background()) if err != nil { return fmt.Errorf("error reconnecting host %s: %w", h, err) } diff --git a/pkg/product/mke/phase/overridehostsudo.go b/pkg/product/mke/phase/overridehostsudo.go deleted file mode 100644 index 4241ed83..00000000 --- a/pkg/product/mke/phase/overridehostsudo.go +++ /dev/null @@ -1,77 +0,0 @@ -package phase - -import ( - "fmt" - "strings" - - "al.essio.dev/pkg/shellescape" - "github.com/Mirantis/launchpad/pkg/phase" - mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/mattn/go-shellwords" - log "github.com/sirupsen/logrus" -) - -// OverrideHostSudo of the host if it has an override in the config. -type OverrideHostSudo struct { - phase.Analytics - phase.HostSelectPhase - - overrideHosts mkeconfig.Hosts -} - -// Title for the phase. -func (p *OverrideHostSudo) Title() string { - return "Override the host sudo" -} - -// ShouldRun should return true only when there is a host with an overridet. -func (p *OverrideHostSudo) ShouldRun() bool { - for _, h := range p.Hosts { - if h.SudoOverride { - p.overrideHosts = append(p.overrideHosts, h) - } - } - return len(p.overrideHosts) > 0 -} - -// Run the phase. -func (p *OverrideHostSudo) Run() error { - err := p.Hosts.ParallelEach(func(h *mkeconfig.Host) error { - if h.SudoOverride { - log.Warnf("%s: overriding sudo for host", h) - h.SetSudofn(sudoSudo) - } - return nil - }) - if err != nil { - return fmt.Errorf("failed to override sudo on hosts: %w", err) - } - return nil -} - -// @see https://github.com/k0sproject/rig/blob/release-0.x/connection.go#L253 -func sudoSudo(cmd string) string { - parts, err := shellwords.Parse(cmd) - if err != nil { - return "sudo -- " + cmd - } - - var idx int - for i, p := range parts { - if strings.Contains(p, "=") { - idx = i + 1 - continue - } - break - } - - if idx == 0 { - return "sudo -- " + cmd - } - - for i, p := range parts { - parts[i] = shellescape.Quote(p) - } - - return fmt.Sprintf("sudo %s -- %s", strings.Join(parts[0:idx], " "), strings.Join(parts[idx:], " ")) -} diff --git a/pkg/product/mke/phase/remove_nodes.go b/pkg/product/mke/phase/remove_nodes.go index 572c30f0..54058f06 100644 --- a/pkg/product/mke/phase/remove_nodes.go +++ b/pkg/product/mke/phase/remove_nodes.go @@ -17,7 +17,7 @@ import ( mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/swarm" "github.com/Mirantis/launchpad/pkg/util/stringutil" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -238,7 +238,7 @@ func (p *RemoveNodes) removemsrNode(config *mkeconfig.ClusterConfig, replicaID s removeCmd := msrLeader.Configurer.DockerCommandf("run %s %s remove %s", runFlags.Join(), msrLeader.MSRMetadata.InstalledBootstrapImage, removeFlags.Join()) log.Debugf("%s: Removing MSR replica %s from cluster", msrLeader, replicaID) - err := msrLeader.Exec(removeCmd, exec.StreamOutput()) + err := msrLeader.Exec(removeCmd, cmd.StreamOutput()) if err != nil { return fmt.Errorf("%s: failed to run MSR remove: %w", msrLeader, err) } diff --git a/pkg/product/mke/phase/uninstall_mke.go b/pkg/product/mke/phase/uninstall_mke.go index 3ffba81c..21edc9c2 100644 --- a/pkg/product/mke/phase/uninstall_mke.go +++ b/pkg/product/mke/phase/uninstall_mke.go @@ -9,7 +9,7 @@ import ( commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/swarm" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -38,7 +38,7 @@ func (p *UninstallMKE) Run() error { // took too long") is emitted at error level by MKE and appears only in // the streamed output, not in the returned error (which only aggregates // fatal-level log lines from the bootstrapper). - output, err := mke.Bootstrap("uninstall-ucp", *p.Config, mke.BootstrapOptions{OperationFlags: uninstallFlags, ExecOptions: []exec.Option{exec.StreamOutput()}}) + output, err := mke.Bootstrap("uninstall-ucp", *p.Config, mke.BootstrapOptions{OperationFlags: uninstallFlags, ExecOptions: []cmd.ExecOption{cmd.StreamOutput()}}) if err != nil { // The uninstall-ucp bootstrapper deploys ucp-uninstall-agent as a global // Swarm service and waits (hardcoded ~2 minutes) for every node to report diff --git a/pkg/product/mke/phase/upgrade_mke.go b/pkg/product/mke/phase/upgrade_mke.go index 26e08e26..b6987143 100644 --- a/pkg/product/mke/phase/upgrade_mke.go +++ b/pkg/product/mke/phase/upgrade_mke.go @@ -7,7 +7,7 @@ import ( "github.com/Mirantis/launchpad/pkg/phase" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" "github.com/Mirantis/launchpad/pkg/swarm" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -40,7 +40,7 @@ func (p *UpgradeMKE) Run() error { upgradeFlags.Merge(commonconfig.Flags{"--id", swarmClusterID}) log.Debugf("%s: upgrade flags: %s", leader, upgradeFlags.Join()) - _, err := mke.Bootstrap("upgrade", *p.Config, mke.BootstrapOptions{OperationFlags: upgradeFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: []exec.Option{exec.StreamOutput()}}) + _, err := mke.Bootstrap("upgrade", *p.Config, mke.BootstrapOptions{OperationFlags: upgradeFlags, CleanupDisabled: p.CleanupDisabled(), ExecOptions: []cmd.ExecOption{cmd.StreamOutput()}}) if err != nil { return fmt.Errorf("%s: failed to run MKE upgrader: %w", leader, err) } diff --git a/pkg/product/mke/phase/upload_images.go b/pkg/product/mke/phase/upload_images.go index af1fb1f5..e380f3e4 100644 --- a/pkg/product/mke/phase/upload_images.go +++ b/pkg/product/mke/phase/upload_images.go @@ -7,10 +7,10 @@ import ( "path" "path/filepath" - "al.essio.dev/pkg/shellescape" "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/util/byteutil" + "github.com/k0sproject/rig/v2/sh/shellescape" log "github.com/sirupsen/logrus" ) @@ -92,7 +92,7 @@ func (p *LoadImages) Run() error { log.Debugf("%s: uploading image %d/%d", h, idx+1, len(h.Metadata.ImagesToUpload)) base := path.Base(f) - df := h.Configurer.JoinPath(h.Configurer.Pwd(h), base) + df := h.FS().Join(h.Configurer.Pwd(h), base) err := h.WriteFileLarge(f, df, fs.FileMode(0o640)) if err != nil { return fmt.Errorf("failed to write file %s: %w", f, err) diff --git a/pkg/product/mke/phase/validate_facts_test.go b/pkg/product/mke/phase/validate_facts_test.go index a8b4185b..fe8b64a8 100644 --- a/pkg/product/mke/phase/validate_facts_test.go +++ b/pkg/product/mke/phase/validate_facts_test.go @@ -6,7 +6,8 @@ import ( commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" - "github.com/k0sproject/rig" + rig "github.com/k0sproject/rig/v2" + "github.com/k0sproject/rig/v2/protocol/ssh" "github.com/stretchr/testify/require" ) @@ -157,15 +158,15 @@ func TestValidateFactsPopulateSan(t *testing.T) { phase.Config = &mkeconfig.ClusterConfig{ Spec: &mkeconfig.ClusterSpec{ Hosts: mkeconfig.Hosts{ - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.1"}}, Role: "manager"}, - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.2"}}, Role: "manager"}, - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.3"}}, Role: "worker"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.1"}}, Role: "manager"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.2"}}, Role: "manager"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.3"}}, Role: "worker"}, }, MCR: commonconfig.MCRConfig{ Channel: "stable-25.0", }, MKE: mkeconfig.MKEConfig{ - Version: "3.6.0", + Version: "3.6.0", Metadata: &mkeconfig.MKEMetadata{}, InstallFlags: commonconfig.Flags{ "--foo", @@ -194,9 +195,9 @@ func TestValidateFactsDontPopulateSan(t *testing.T) { phase.Config = &mkeconfig.ClusterConfig{ Spec: &mkeconfig.ClusterSpec{ Hosts: mkeconfig.Hosts{ - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.1"}}, Role: "manager"}, - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.2"}}, Role: "manager"}, - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.3"}}, Role: "worker"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.1"}}, Role: "manager"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.2"}}, Role: "manager"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.3"}}, Role: "worker"}, }, MCR: commonconfig.MCRConfig{ Channel: "stable-25.0", @@ -229,7 +230,7 @@ func TestValidateInvalidMCRConfig(t *testing.T) { phase.Config = &mkeconfig.ClusterConfig{ Spec: &mkeconfig.ClusterSpec{ Hosts: mkeconfig.Hosts{ - &mkeconfig.Host{Connection: rig.Connection{SSH: &rig.SSH{Address: "10.0.0.1"}}, Role: "manager"}, + &mkeconfig.Host{CompositeConfig: rig.CompositeConfig{SSH: &ssh.Config{Address: "10.0.0.1"}}, Role: "manager"}, }, }, } diff --git a/pkg/product/mke/phase/validate_hosts.go b/pkg/product/mke/phase/validate_hosts.go index 264e14e9..4bb6e708 100644 --- a/pkg/product/mke/phase/validate_hosts.go +++ b/pkg/product/mke/phase/validate_hosts.go @@ -12,7 +12,7 @@ import ( "github.com/Mirantis/launchpad/pkg/phase" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/Mirantis/launchpad/pkg/util/stringutil" - "github.com/k0sproject/rig/exec" + "github.com/k0sproject/rig/v2/cmd" log "github.com/sirupsen/logrus" ) @@ -66,7 +66,8 @@ func (p *ValidateHosts) validateHostConnection() error { if err != nil { return fmt.Errorf("connection test failed: create temp file: %w", err) } - defer os.Remove("uploadTest") + defer testFile.Close() + defer os.Remove(testFile.Name()) _, err = io.CopyN(testFile, rand.Reader, 1048576) // create an 1MB temp file full of random data if err != nil { @@ -76,16 +77,17 @@ func (p *ValidateHosts) validateHostConnection() error { err = p.Config.Spec.Hosts.Each(func(h *mkeconfig.Host) error { log.Infof("%s: testing file upload", h) + target := h.FS().Join(h.Configurer.Pwd(h), "launchpad.test") defer func() { - if err := h.Configurer.DeleteFile(h, "launchpad.test"); err != nil { + if err := h.Sudo().FS().Remove(target); err != nil { log.Debugf("%s: failed to delete test file: %s", h, err.Error()) } }() - err := h.WriteFileLarge(testFile.Name(), h.Configurer.JoinPath(h.Configurer.Pwd(h), "launchpad.test"), fs.FileMode(0o640)) - if err != nil { + if err := h.WriteFileLarge(testFile.Name(), target, fs.FileMode(0o640)); err != nil { h.Errors.Add(err.Error()) + return fmt.Errorf("failed to upload file: %w", err) } - return fmt.Errorf("failed to upload file: %w", err) + return nil }) if err != nil { return fmt.Errorf("connection test failed: upload: %w", err) @@ -95,26 +97,27 @@ func (p *ValidateHosts) validateHostConnection() error { filename := "launchpad.test" testStr := "hello world!\n" defer func() { - if err := h.Configurer.DeleteFile(h, filename); err != nil { + if err := h.Sudo().FS().Remove(filename); err != nil { log.Debugf("%s: failed to delete test file: %s", h, err.Error()) } }() log.Infof("%s: testing stdin redirection", h) if h.IsWindows() { - err := h.Exec(fmt.Sprintf(`findstr "^" > %s`, filename), exec.Stdin(testStr)) + err := h.Exec(fmt.Sprintf(`findstr "^" > %s`, filename), cmd.StdinString(testStr)) if err != nil { return fmt.Errorf("failed to test stdin redirection: %w", err) } } else { - err := h.Exec(fmt.Sprintf("cat > %s", filename), exec.Stdin(testStr)) + err := h.Exec(fmt.Sprintf("cat > %s", filename), cmd.StdinString(testStr)) if err != nil { return fmt.Errorf("failed to test stdin redirection: %w", err) } } - content, err := h.Configurer.ReadFile(h, filename) + data, err := fs.ReadFile(h.Sudo().FS(), filename) if err != nil { return fmt.Errorf("failed to read file: %w", err) } + content := string(data) if strings.TrimSpace(content) != strings.TrimSpace(testStr) { // Allow trailing linefeeds etc, mainly because windows is weird. return fmt.Errorf("%w: file write test content check mismatch: %q vs %q", errContentMismatch, strings.TrimSpace(content), strings.TrimSpace(testStr)) diff --git a/pkg/product/mke/reset.go b/pkg/product/mke/reset.go index c84eca22..42ed2791 100644 --- a/pkg/product/mke/reset.go +++ b/pkg/product/mke/reset.go @@ -13,7 +13,6 @@ func (p *MKE) Reset() error { phaseManager := phase.NewManager(&p.ClusterConfig) phaseManager.AddPhases( - &mke.OverrideHostSudo{}, &common.Connect{}, &mke.DetectOS{}, &mke.GatherFacts{},