-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sascha Schwarze <schwarzs@de.ibm.com>
- Loading branch information
1 parent
f29be78
commit 87b03f9
Showing
22 changed files
with
800 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<!-- | ||
Copyright The Shipwright Contributors | ||
SPDX-License-Identifier: Apache-2.0 | ||
--> | ||
`waiter` | ||
-------- | ||
|
||
In a nutshell, it waits until the lock-file is removed. When starting the application a lock-file | ||
(`--lock-file`) is created, and when the file is removed the `waiter` stops gracefully. When timeout | ||
is reached, the application exits on error. | ||
|
||
## Usage | ||
|
||
Please consider `--help` to see the possible flags, the possible sub-commands are: | ||
|
||
```sh | ||
waiter start | ||
``` | ||
|
||
And: | ||
|
||
```sh | ||
waiter done | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package main | ||
|
||
import ( | ||
"log" | ||
"os" | ||
"time" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
// settings composed by command-line flag values. | ||
type settings struct { | ||
lockFile string // path to lock file | ||
timeout time.Duration // how long wait for 'done' | ||
} | ||
|
||
const longDesc = ` | ||
# waiter | ||
Idle loop to hold a container (possibly a Kubernetes POD) running while some other | ||
action happens in the background. It is started by issuing "waiter start" and can | ||
be stopped with "waiter done", or after timeout. | ||
## Usage | ||
Start the waiting, use --timeout to change how long: | ||
$ waiter start | ||
You can signal "done" by running: | ||
$ waiter done | ||
Or, alternatively: | ||
$ rm -f <lock-file> | ||
## Return-Code | ||
In the case of timeout, the waiter will return error, it only exits gracefully via | ||
"waiter done", or the removal of the lock-file (before timeout). | ||
` | ||
|
||
var ( | ||
rootCmd = newRootCmd() | ||
startCmd = newStartCmd() | ||
doneCmd = newDoneCmd() | ||
) | ||
|
||
// defaultTimeout default timeout duration. | ||
var defaultTimeout = 60 * time.Second | ||
|
||
// defaultLockFile default location of the lock-file. | ||
var defaultLockFile = "/tmp/waiter.lock" | ||
|
||
// flagValues receives the command-line flag values. | ||
var flagValues = settings{} | ||
|
||
// init assembles the flags and the cobra sub-commands. | ||
func init() { | ||
flags := rootCmd.PersistentFlags() | ||
|
||
flags.StringVar(&flagValues.lockFile, "lock-file", defaultLockFile, "lock file full path") | ||
flags.DurationVar(&flagValues.timeout, "timeout", defaultTimeout, "how long to wait until 'done'") | ||
|
||
rootCmd.AddCommand(startCmd) | ||
rootCmd.AddCommand(doneCmd) | ||
} | ||
|
||
func newRootCmd() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "waiter [flags]", | ||
Short: "Will wait until `done` issued", | ||
Long: longDesc, | ||
} | ||
} | ||
|
||
func newStartCmd() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "start", | ||
Short: "Starts the wait, and holds until `done` is issued.", | ||
SilenceUsage: true, | ||
RunE: func(_ *cobra.Command, _ []string) error { | ||
w := NewWaiter(flagValues) | ||
return w.Wait() | ||
}, | ||
} | ||
} | ||
|
||
func newDoneCmd() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "done", | ||
Aliases: []string{"stop"}, | ||
Short: "Interrupts the waiting.", | ||
SilenceUsage: true, | ||
RunE: func(_ *cobra.Command, _ []string) error { | ||
w := NewWaiter(flagValues) | ||
return w.Done() | ||
}, | ||
} | ||
} | ||
|
||
// main waiter's entrypoint. | ||
func main() { | ||
if err := rootCmd.Execute(); err != nil { | ||
log.Fatalf("[ERROR] %v\n", err) | ||
os.Exit(1) | ||
} | ||
os.Exit(0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestWaiterCmd(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Waiter") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"os" | ||
"os/exec" | ||
"time" | ||
|
||
"github.com/onsi/gomega/gbytes" | ||
"github.com/onsi/gomega/gexec" | ||
"github.com/onsi/gomega/types" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("Waiter", func() { | ||
// executable path to the waiter executable file. | ||
var executable string | ||
|
||
// run creates a exec.Command instance using the arguments informed. | ||
var run = func(args ...string) *gexec.Session { | ||
cmd := exec.Command(executable) | ||
cmd.Args = append(cmd.Args, args...) | ||
stdin := &bytes.Buffer{} | ||
cmd.Stdin = stdin | ||
|
||
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
// when "start" sub-command is issued, a graceful wait takes place for the asynchronous | ||
// instantiation of the command-line application and creation of the lock-file | ||
for _, arg := range args { | ||
if arg == "start" { | ||
time.Sleep(3 * time.Second) | ||
} | ||
} | ||
return session | ||
} | ||
|
||
// inspectSession inspect the informed session to identify if the informed matcher is true, | ||
// after inspection it closes the informed channel. | ||
var inspectSession = func( | ||
session *gexec.Session, | ||
doneCh chan interface{}, | ||
matcher types.GomegaMatcher, | ||
) { | ||
defer GinkgoRecover() | ||
|
||
Eventually(session, defaultTimeout).Should(matcher) | ||
close(doneCh) | ||
} | ||
|
||
// building the command-line application before starting the test suite, it will populate the | ||
// global variable with the path to the waiter binary compiled. | ||
BeforeSuite(func() { | ||
var err error | ||
executable, err = gexec.Build("github.com/shipwright-io/build/cmd/waiter") | ||
Expect(err).ToNot(HaveOccurred()) | ||
}) | ||
|
||
AfterSuite(func() { | ||
gexec.CleanupBuildArtifacts() | ||
_ = os.RemoveAll(defaultLockFile) | ||
}) | ||
|
||
When("--help is passed", func() { | ||
var session *gexec.Session | ||
|
||
BeforeEach(func() { | ||
session = run("--help") | ||
}) | ||
|
||
It("shows the general help message", func() { | ||
Eventually(session).Should(gbytes.Say("Usage:")) | ||
}) | ||
}) | ||
|
||
Describe("expect to succeed when lock-file removed before timeout", func() { | ||
var startCh = make(chan interface{}) | ||
|
||
BeforeEach(func() { | ||
session := run("start") | ||
|
||
go inspectSession(session, startCh, gexec.Exit(0)) | ||
}) | ||
|
||
It("stops when lock-file is removed", func() { | ||
err := os.RemoveAll(defaultLockFile) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
Eventually(startCh, defaultTimeout).Should(BeClosed()) | ||
}) | ||
}) | ||
|
||
Describe("expect to succeed when `done` is issued before timeout", func() { | ||
var startCh = make(chan interface{}) | ||
var doneCh = make(chan interface{}) | ||
|
||
BeforeEach(func() { | ||
session := run("start") | ||
|
||
go inspectSession(session, startCh, gexec.Exit(0)) | ||
}) | ||
|
||
BeforeEach(func() { | ||
session := run("done") | ||
|
||
go inspectSession(session, doneCh, gexec.Exit(0)) | ||
}) | ||
|
||
It("stops when done is issued", func() { | ||
Eventually(startCh, defaultTimeout).Should(BeClosed()) | ||
Eventually(doneCh, defaultTimeout).Should(BeClosed()) | ||
}) | ||
}) | ||
|
||
Describe("expect to fail when timeout is reached", func() { | ||
var startCh = make(chan interface{}) | ||
|
||
BeforeEach(func() { | ||
session := run("start", "--timeout", "2s") | ||
|
||
go inspectSession(session, startCh, gexec.Exit(1)) | ||
}) | ||
|
||
It("stops when timeout is reached", func() { | ||
Eventually(startCh, defaultTimeout).Should(BeClosed()) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// Copyright The Shipwright Contributors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"log" | ||
"os" | ||
"strconv" | ||
"time" | ||
) | ||
|
||
// Waiter represents the actor that will wait for timeout, using a lock-file to keep it actively | ||
// waiting. When "done" is issued the lock-file is removed and the waiter ends. | ||
type Waiter struct { | ||
flagValues *settings // command-line flags | ||
} | ||
|
||
// ErrTimeout emitted when timeout is reached. | ||
var ErrTimeout = errors.New("timeout waiting for condition") | ||
|
||
// save writes the lock-file with informed PID. | ||
func (w *Waiter) save(pid int) error { | ||
return os.WriteFile(w.flagValues.lockFile, []byte(strconv.Itoa(pid)), 0600) | ||
} | ||
|
||
// read reads the lock-file, must contain an integer. | ||
func (w *Waiter) read() (int, error) { | ||
data, err := os.ReadFile(w.flagValues.lockFile) | ||
if err != nil { | ||
return -1, err | ||
} | ||
pid, err := strconv.Atoi(string(data)) | ||
if err != nil { | ||
return -1, err | ||
} | ||
return pid, nil | ||
} | ||
|
||
// retry re-execute the informed function waiting for 100ms per attempt. | ||
func retry(timeout time.Duration, fn func() bool) error { | ||
attempts := int(int(timeout.Milliseconds()) / 100) | ||
log.Printf("Will retry '%d' times (sleep 100ms)...\n", attempts) | ||
for i := attempts; i > 0; i-- { | ||
if fn() { | ||
log.Printf("Done! Condition has been reached on '%d' attempt\n", attempts-i) | ||
return nil | ||
} | ||
time.Sleep(100 * time.Millisecond) | ||
} | ||
return fmt.Errorf("%w: elapsed %v seconds", ErrTimeout, timeout.Seconds()) | ||
} | ||
|
||
// Wait wait for the lock-file to be removed, or timeout. | ||
func (w *Waiter) Wait() error { | ||
pid := os.Getpid() | ||
if err := w.save(pid); err != nil { | ||
return err | ||
} | ||
|
||
// waiting for the lock-file removal... | ||
err := retry(w.flagValues.timeout, func() bool { | ||
_, err := os.Stat(w.flagValues.lockFile) | ||
return err != nil && os.IsNotExist(err) | ||
}) | ||
if err != nil { | ||
_ = os.RemoveAll(w.flagValues.lockFile) | ||
} | ||
return err | ||
} | ||
|
||
// Done removes the lock-file. | ||
func (w *Waiter) Done() error { | ||
pid, err := w.read() | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("Removing lock-file at '%s' (%d PID)", w.flagValues.lockFile, pid) | ||
return os.Remove(w.flagValues.lockFile) | ||
} | ||
|
||
// NewWaiter instantiate a new waiter, making sure the timeout informed is acceptable. | ||
func NewWaiter(flagValues settings) *Waiter { | ||
if flagValues.timeout <= time.Second { | ||
log.Printf("Warning! The timeout informed '%s' is lower than 1s!\n", flagValues.timeout) | ||
flagValues.timeout = defaultTimeout | ||
} | ||
return &Waiter{flagValues: &flagValues} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.