Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUILD-372] SHIP-0021: Local Source Upload #934

Merged
merged 3 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ defaultBaseImage: registry.access.redhat.com/ubi8/ubi-minimal

baseImageOverrides:
github.com/shipwright-io/build/cmd/git: ghcr.io/shipwright-io/base-git:latest
github.com/shipwright-io/build/cmd/waiter: ghcr.io/shipwright-io/base-waiter:latest
24 changes: 24 additions & 0 deletions cmd/waiter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!--
Copyright The Shipwright Contributors

SPDX-License-Identifier: Apache-2.0
-->

`waiter`: Wait Until Condition or Timeout
-----------------------------------------

In a nutshell, it waits until the lock-file is removed. When starting the application, a lock-file is created. 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
```
113 changes: 113 additions & 0 deletions cmd/waiter/main.go
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"
gabemontero marked this conversation as resolved.
Show resolved Hide resolved

// 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)
}
16 changes: 16 additions & 0 deletions cmd/waiter/main_suite_test.go
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")
}
134 changes: 134 additions & 0 deletions cmd/waiter/main_test.go
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())
})
})
})
91 changes: 91 additions & 0 deletions cmd/waiter/waiter.go
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}
gabemontero marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions deploy/500-controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ spec:
value: ko://github.com/shipwright-io/build/cmd/mutate-image
- name: BUNDLE_CONTAINER_IMAGE
value: ko://github.com/shipwright-io/build/cmd/bundle
- name: WAITER_CONTAINER_IMAGE
value: ko://github.com/shipwright-io/build/cmd/waiter
ports:
- containerPort: 8383
name: metrics-port
Expand Down
Loading