11package configurer
22
33import (
4- "bufio"
54 "fmt"
6- "path/filepath "
5+ "path"
76 "strconv"
87 "strings"
98 "sync"
109 "time"
1110
11+ "github.com/k0sproject/k0sctl/internal/shell"
1212 "github.com/k0sproject/rig/exec"
1313 "github.com/k0sproject/rig/os"
1414 ps "github.com/k0sproject/rig/pkg/powershell"
@@ -29,10 +29,10 @@ func (w *BaseWindows) OSKind() string {
2929func (w * BaseWindows ) initPaths () {
3030 w .pathOnce .Do (func () {
3131 w .paths = map [string ]string {
32- "K0sBinaryPath" : `C:\\ Program Files\\ k0s\\ k0s.exe` ,
33- "K0sConfigPath" : `C:\\ProgramData\\ k0s\\ k0s.yaml` ,
34- "K0sJoinTokenPath" : `C:\\ProgramData\\ k0s\\ k0stoken` ,
35- "DataDirDefaultPath" : `C:\\ProgramData\\ k0s` ,
32+ "K0sBinaryPath" : `C:/ Program Files/ k0s/ k0s.exe` ,
33+ "K0sConfigPath" : `C:/etc/ k0s/ k0s.yaml` ,
34+ "K0sJoinTokenPath" : `C:/etc/ k0s/ k0stoken` ,
35+ "DataDirDefaultPath" : `C:/var/lib/ k0s` ,
3636 }
3737 })
3838}
@@ -93,7 +93,7 @@ func (w *BaseWindows) Arch(h os.Host) (string, error) {
9393
9494// K0sCmdf can be used to construct k0s commands in sprintf style.
9595func (w * BaseWindows ) K0sCmdf (template string , args ... interface {}) string {
96- return fmt .Sprintf (`& %s %s` , ps .DoubleQuotePath (w .K0sBinaryPath ()), fmt .Sprintf (template , args ... ))
96+ return fmt .Sprintf (`%s %s` , ps .DoubleQuotePath (ps . ToWindowsPath ( w .K0sBinaryPath () )), fmt .Sprintf (template , args ... ))
9797}
9898
9999// K0sctlLockFilePath returns a path to a lock file
@@ -104,20 +104,41 @@ func (w *BaseWindows) K0sctlLockFilePath(h os.Host) string {
104104
105105// TempFile returns a temp file path
106106func (w * BaseWindows ) TempFile (h os.Host ) (string , error ) {
107- // Use .NET to generate a temp file path
108- return h .ExecOutput (ps .Cmd (`[System.IO.Path]::GetTempFileName()` ))
107+ output , err := h .ExecOutput (ps .Cmd (`[System.IO.Path]::GetTempFileName()` ))
108+ if err != nil {
109+ return "" , fmt .Errorf ("failed to create temp file: %w" , err )
110+ }
111+ output = strings .TrimSpace (output )
112+ // Normalize to use forward slashes
113+ output , err = shell .Unquote (output )
114+ if err != nil {
115+ return "" , fmt .Errorf ("failed to parse temp file path: %w" , err )
116+ }
117+ output = strings .ReplaceAll (output , "\\ " , "/" )
118+ return output , nil
109119}
110120
111121// TempDir returns a temp dir path
112122func (w * BaseWindows ) TempDir (h os.Host ) (string , error ) {
113123 // Create a unique temp directory and output its path
114124 script := `$p = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()); New-Item -ItemType Directory -Path $p | Out-Null; Write-Output $p`
115- return h .ExecOutput (ps .Cmd (script ))
125+ output , err := h .ExecOutput (ps .Cmd (script ))
126+ if err != nil {
127+ return "" , fmt .Errorf ("failed to create temp dir: %w" , err )
128+ }
129+ output = strings .TrimSpace (output )
130+ // Normalize to use forward slashes
131+ output , err = shell .Unquote (output )
132+ if err != nil {
133+ return "" , fmt .Errorf ("failed to parse temp dir path: %w" , err )
134+ }
135+ output = strings .ReplaceAll (output , "\\ " , "/" )
136+ return output , nil
116137}
117138
118139// DownloadURL performs a download from a URL on the host
119140func (w * BaseWindows ) DownloadURL (h os.Host , url , destination string , opts ... exec.Option ) error {
120- cmd := ps .Cmd (fmt .Sprintf (`Invoke-WebRequest -UseBasicParsing -Uri %s -OutFile %s` , ps .SingleQuote (url ), ps .DoubleQuotePath (destination )))
141+ cmd := ps .Cmd (fmt .Sprintf (`Invoke-WebRequest -UseBasicParsing -Uri %s -OutFile %s` , ps .SingleQuote (url ), ps .DoubleQuotePath (ps . ToWindowsPath ( destination ) )))
121142 if err := h .Exec (cmd , opts ... ); err != nil {
122143 return fmt .Errorf ("download failed: %w" , err )
123144 }
@@ -127,19 +148,19 @@ func (w *BaseWindows) DownloadURL(h os.Host, url, destination string, opts ...ex
127148// ReplaceK0sTokenPath replaces the config path in the service stub
128149func (w * BaseWindows ) ReplaceK0sTokenPath (h os.Host , spath string ) error {
129150 // Replace literal REPLACEME with actual token path
130- cmd := ps .Cmd (fmt .Sprintf (`(Get-Content -Path %s) -replace 'REPLACEME', %s | Set-Content -Path %s -Encoding ascii` , ps .DoubleQuotePath (spath ), ps .SingleQuote (w .K0sJoinTokenPath ()), ps .DoubleQuotePath (spath )))
151+ cmd := ps .Cmd (fmt .Sprintf (`(Get-Content -Path %s) -replace 'REPLACEME', %s | Set-Content -Path %s -Encoding ascii` , ps .DoubleQuotePath (ps . ToWindowsPath ( spath )) , ps .SingleQuote (ps . ToWindowsPath ( w .K0sJoinTokenPath ())) , ps .DoubleQuotePath (ps . ToWindowsPath ( spath ) )))
131152 return h .Exec (cmd )
132153}
133154
134155// FileContains returns true if a file contains the substring
135156func (w * BaseWindows ) FileContains (h os.Host , path , s string ) bool {
136- cmd := ps .Cmd (fmt .Sprintf (`if (Select-String -Path %s -Pattern %s -SimpleMatch -Quiet) { exit 0 } else { exit 1 }` , ps .DoubleQuotePath (path ), ps .SingleQuote (s )))
157+ cmd := ps .Cmd (fmt .Sprintf (`if (Select-String -Path %s -Pattern %s -SimpleMatch -Quiet) { exit 0 } else { exit 1 }` , ps .DoubleQuotePath (ps . ToWindowsPath ( path )) , ps .SingleQuote (ps . ToWindowsPath ( s ) )))
137158 return h .Exec (cmd ) == nil
138159}
139160
140161// MoveFile moves a file on the host
141162func (w * BaseWindows ) MoveFile (h os.Host , src , dst string ) error {
142- return h .Exec (ps .Cmd (fmt .Sprintf (`Move-Item -Force -Path %s -Destination %s` , ps .DoubleQuotePath (src ), ps .DoubleQuotePath (dst ))))
163+ return h .Exec (ps .Cmd (fmt .Sprintf (`Move-Item -Force -Path %s -Destination %s` , ps .DoubleQuotePath (ps . ToWindowsPath ( src )) , ps .DoubleQuotePath (ps . ToWindowsPath ( dst ) ))))
143164}
144165
145166// Chown is a no-op on Windows; ownership semantics differ and are not managed here
@@ -149,17 +170,18 @@ func (w *BaseWindows) Chown(h os.Host, path, owner string, _ ...exec.Option) err
149170
150171// KubeconfigPath returns the path to a kubeconfig on the host
151172func (w * BaseWindows ) KubeconfigPath (h os.Host , dataDir string ) string {
152- win := & os.Windows {}
153- adminConfPath := filepath .Join (dataDir , "pki" , "admin.conf" )
154- if win .FileExist (h , adminConfPath ) {
173+ adminConfPath := path .Join (dataDir , "pki" , "admin.conf" )
174+ adminConfPath = strings .ReplaceAll (adminConfPath , "\\ " , "/" )
175+ adminConfPath = ps .ToWindowsPath (adminConfPath )
176+ if err := h .Exec (ps .Cmd (fmt .Sprintf (`Test-Path -Path %s` , ps .DoubleQuotePath (adminConfPath ))), exec .Sudo (h )); err == nil {
155177 return adminConfPath
156178 }
157- return filepath .Join (dataDir , "kubelet.conf" )
179+ return path .Join (dataDir , "kubelet.conf" )
158180}
159181
160182// KubectlCmdf returns a command line in sprintf manner for running kubectl on the host using the kubeconfig from KubeconfigPath
161183func (w * BaseWindows ) KubectlCmdf (h os.Host , dataDir , s string , args ... interface {}) string {
162- return fmt .Sprintf (`$env:KUBECONFIG=%s; %s` , ps .DoubleQuotePath (w .KubeconfigPath (h , dataDir )), w .K0sCmdf (`kubectl %s` , fmt .Sprintf (s , args ... )))
184+ return ps . Cmd ( fmt .Sprintf (`$env:KUBECONFIG=%s; %s` , ps .DoubleQuotePath (ps . ToWindowsPath ( w .KubeconfigPath (h , dataDir ))) , w .K0sCmdf (`kubectl %s` , fmt .Sprintf (s , args ... ) )))
163185}
164186
165187// HTTPStatus makes a HTTP GET request to the url and returns the status code or an error
@@ -177,37 +199,60 @@ func (w *BaseWindows) HTTPStatus(h os.Host, url string) (int, error) {
177199
178200// PrivateInterface tries to find a private network interface (not implemented for Windows yet)
179201func (w * BaseWindows ) PrivateInterface (h os.Host ) (string , error ) {
180- out , err := h .ExecOutput (ps .Cmd (`(Get-NetConnectionProfile -NetworkCategory Private | Select-Object -First 1).InterfaceAlias` ))
181- if err != nil || strings .TrimSpace (out ) == "" {
182- out , err = h .ExecOutput (ps .Cmd (`(Get-NetConnectionProfile | Select-Object -First 1).InterfaceAlias` ))
183- }
184- if err != nil || strings .TrimSpace (out ) == "" {
185- return "" , fmt .Errorf ("failed to detect a private network interface, define the host privateInterface manually: %w" , err )
202+ cmd := ps .Cmd (`
203+ $if = Get-NetIPAddress -AddressFamily IPv4 |
204+ Where-Object { $_.IPAddress -match '^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.)' } |
205+ Sort-Object InterfaceMetric |
206+ Select-Object -First 1 -ExpandProperty InterfaceAlias;
207+ if (-not $if) {
208+ $if = Get-NetRoute -DestinationPrefix '0.0.0.0/0' |
209+ Sort-Object RouteMetric, InterfaceMetric |
210+ Select-Object -First 1 -ExpandProperty InterfaceAlias
211+ };
212+ $if` )
213+ cmd = strings .ReplaceAll (cmd , "\n " , " " )
214+ cmd = strings .ReplaceAll (cmd , "\t " , " " )
215+ output , err := h .ExecOutput (cmd , exec .Sudo (h ))
216+ if err != nil {
217+ return "" , fmt .Errorf ("failed to detect private network interface: %s" , err )
186218 }
187- sc := bufio .NewScanner (strings .NewReader (out ))
188- if sc .Scan () {
189- return strings .TrimSpace (sc .Text ()), nil
219+
220+ iface := strings .TrimSpace (output )
221+ if iface == "" {
222+ return "" , fmt .Errorf ("no private interface found, define host privateInterface manually" )
190223 }
191- return "" , fmt .Errorf ("failed to detect a private network interface" )
224+
225+ return iface , nil
192226}
193227
194228// PrivateAddress resolves internal ip from private interface (not implemented for Windows yet)
195229func (w * BaseWindows ) PrivateAddress (h os.Host , iface , publicip string ) (string , error ) {
196- ip , err := h .ExecOutput (ps .Cmd (fmt .Sprintf (`(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias %s).IPAddress` , ps .SingleQuote (iface ))))
197- if err != nil || strings .TrimSpace (ip ) == "" {
198- if ! strings .HasPrefix (iface , "vEthernet" ) {
199- ve := fmt .Sprintf ("vEthernet (%s)" , iface )
200- ip , err = h .ExecOutput (ps .Cmd (fmt .Sprintf (`(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias %s).IPAddress` , ps .SingleQuote (ve ))))
201- }
202- }
230+ cmd := ps .Cmd (fmt .Sprintf (`
231+ (Get-NetIPAddress -InterfaceAlias %s -AddressFamily IPv4 |
232+ Where-Object {
233+ $_.AddressState -eq 'Preferred' -and
234+ $_.IPAddress -match '^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.)' -and
235+ $_.IPAddress -ne '%s'
236+ } |
237+ Select-Object -First 1 -ExpandProperty IPAddress)
238+ ` , ps .SingleQuote (iface ), publicip ))
239+ cmd = strings .ReplaceAll (cmd , "\n " , " " )
240+ cmd = strings .ReplaceAll (cmd , "\t " , " " )
241+ output , err := h .ExecOutput (cmd )
203242 if err != nil {
204- return "" , fmt .Errorf ("failed to get IP address for interface %s: %w" , iface , err )
243+ return "" , fmt .Errorf ("failed to get IP for interface %s: %s" , iface , err )
244+ }
245+
246+ ip := strings .TrimSpace (output )
247+ if ip == "" {
248+ return "" , fmt .Errorf ("no IPv4 address found for interface %s" , iface )
205249 }
206- addr := strings . TrimSpace ( ip )
207- if addr != "" && addr ! = publicip {
208- return addr , nil
250+
251+ if ip = = publicip {
252+ return "" , fmt . Errorf ( "resolved IP equals public IP, no private address found" )
209253 }
210- return "" , fmt .Errorf ("not found" )
254+
255+ return ip , nil
211256}
212257
213258// UpsertFile creates a file in path with content only if the file does not exist already
@@ -219,25 +264,25 @@ func (w *BaseWindows) UpsertFile(h os.Host, path, content string) error {
219264 // Write content to temp file
220265 if err := h .Exec (ps .Cmd (fmt .Sprintf (`Set-Content -Path %s -Value @'
221266%s
222- '@ -Encoding ascii` , ps .DoubleQuotePath (tmpf ), content ))); err != nil {
267+ '@ -Encoding ascii` , ps .DoubleQuotePath (ps . ToWindowsPath ( tmpf ) ), content ))); err != nil {
223268 return err
224269 }
225270
226271 // Atomically move if destination does not exist
227- script := ps .Cmd (fmt .Sprintf (`if (!(Test-Path -Path %s)) { Move-Item -Path %s -Destination %s } else { Remove-Item -Path %s -Force }` , ps .DoubleQuotePath (path ), ps .DoubleQuotePath (tmpf ), ps .DoubleQuotePath (path ), ps .DoubleQuotePath (tmpf )))
272+ script := ps .Cmd (fmt .Sprintf (`if (!(Test-Path -Path %s)) { Move-Item -Path %s -Destination %s } else { Remove-Item -Path %s -Force }` , ps .DoubleQuotePath (ps . ToWindowsPath ( path )) , ps .DoubleQuotePath (ps . ToWindowsPath ( tmpf )) , ps .DoubleQuotePath (ps . ToWindowsPath ( path )) , ps .DoubleQuotePath (ps . ToWindowsPath ( tmpf ) )))
228273 if err := h .Exec (script ); err != nil {
229274 return fmt .Errorf ("upsert failed: %w" , err )
230275 }
231276
232277 // Ensure temp file is gone
233- if h .Exec (ps .Cmd (fmt .Sprintf (`Test-Path -Path %s` , ps .DoubleQuotePath (tmpf )))) == nil {
278+ if h .Exec (ps .Cmd (fmt .Sprintf (`Test-Path -Path %s` , ps .DoubleQuotePath (ps . ToWindowsPath ( tmpf ) )))) == nil {
234279 return fmt .Errorf ("upsert failed" )
235280 }
236281 return nil
237282}
238283
239284func (w * BaseWindows ) DeleteDir (h os.Host , path string , opts ... exec.Option ) error {
240- return h .Exec (ps .Cmd (fmt .Sprintf (`Remove-Item -Recurse -Force -Path %s` , ps .DoubleQuotePath (path ))), opts ... )
285+ return h .Exec (ps .Cmd (fmt .Sprintf (`Remove-Item -Recurse -Force -Path %s` , ps .DoubleQuotePath (ps . ToWindowsPath ( path ) ))), opts ... )
241286}
242287
243288func (w * BaseWindows ) MachineID (h os.Host ) (string , error ) {
@@ -257,3 +302,21 @@ func (w *BaseWindows) SystemTime(h os.Host) (time.Time, error) {
257302 }
258303 return time .Unix (unixTime , 0 ), nil
259304}
305+
306+ // Dir returns the directory part of a path
307+ func (w * BaseWindows ) Dir (path string ) string {
308+ index := strings .LastIndexAny (path , `\/` )
309+ if index == - 1 {
310+ return "."
311+ }
312+ return path [:index ]
313+ }
314+
315+ // Base returns the last element of a path
316+ func (w * BaseWindows ) Base (path string ) string {
317+ index := strings .LastIndexAny (path , `\/` )
318+ if index == - 1 {
319+ return path
320+ }
321+ return path [index + 1 :]
322+ }
0 commit comments