Skip to content

Commit

Permalink
feat: job with dockerfile (#233)
Browse files Browse the repository at this point in the history
* feat: job with dockerfile support

* test: job with dockerfile support

* lint: remove unused variable

* fix: job deploy exemples

* chore: warning for job update with image
  • Loading branch information
crgisch authored Oct 9, 2024
1 parent 1744046 commit 823d8f5
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 18 deletions.
10 changes: 5 additions & 5 deletions tsuru/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func uploadFiles(context *cmd.Context, request *http.Request, buf *safe.Buffer,
return nil
}

func buildWithContainerFile(appName, path string, filesOnly bool, files []string, stderr io.Writer) (string, io.Reader, error) {
func buildWithContainerFile(resourceName, path string, filesOnly bool, files []string, stderr io.Writer) (string, io.Reader, error) {
fi, err := os.Stat(path)
if err != nil {
return "", nil, fmt.Errorf("failed to stat the file %s: %w", path, err)
Expand All @@ -200,7 +200,7 @@ func buildWithContainerFile(appName, path string, filesOnly bool, files []string

switch {
case fi.IsDir():
path, err = guessingContainerFile(appName, path)
path, err = guessingContainerFile(resourceName, path)
if err != nil {
return "", nil, fmt.Errorf("failed to guess the container file (can you specify the container file passing --dockerfile ./path/to/Dockerfile?): %w", err)
}
Expand Down Expand Up @@ -230,10 +230,10 @@ func buildWithContainerFile(appName, path string, filesOnly bool, files []string
return string(containerfile), &buildContext, nil
}

func guessingContainerFile(app, dir string) (string, error) {
func guessingContainerFile(resourceName, dir string) (string, error) {
validNames := []string{
fmt.Sprintf("Dockerfile.%s", app),
fmt.Sprintf("Containerfile.%s", app),
fmt.Sprintf("Dockerfile.%s", resourceName),
fmt.Sprintf("Containerfile.%s", resourceName),
"Dockerfile.tsuru",
"Containerfile.tsuru",
"Dockerfile",
Expand Down
186 changes: 186 additions & 0 deletions tsuru/client/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ func (c *AppDeploy) Cancel(ctx cmd.Context) error {
}
c.m.Lock()
defer c.m.Unlock()
ctx.RawOutput()
if c.eventID == "" {
return errors.New("event ID not available yet")
}
Expand Down Expand Up @@ -573,3 +574,188 @@ func (c *deployVersionArgs) values(values url.Values) {
values.Set("override-versions", strconv.FormatBool(c.overrideVersions))
}
}

var _ cmd.Cancelable = &JobDeploy{}

type JobDeploy struct {
jobName string
image string
message string
dockerfile string
eventID string
fs *gnuflag.FlagSet
m sync.Mutex
}

func (c *JobDeploy) Flags() *gnuflag.FlagSet {
if c.fs == nil {
c.fs = gnuflag.NewFlagSet("", gnuflag.ExitOnError)
c.fs.StringVar(&c.jobName, "job", "", "The name of the job.")
c.fs.StringVar(&c.jobName, "j", "", "The name of the job.")
image := "The image to deploy in job"
c.fs.StringVar(&c.image, "image", "", image)
c.fs.StringVar(&c.image, "i", "", image)
message := "A message describing this deploy"
c.fs.StringVar(&c.message, "message", "", message)
c.fs.StringVar(&c.message, "m", "", message)
c.fs.StringVar(&c.dockerfile, "dockerfile", "", "Container file")
}
return c.fs
}

func (c *JobDeploy) Info() *cmd.Info {
return &cmd.Info{
Name: "job-deploy",
Usage: "job deploy [--job <job name>] [--image <container image name>] [--dockerfile <container image file>] [--message <message>]",
Desc: `Deploy the source code and/or configurations to a Job on Tsuru.
Files specified in the ".tsuruignore" file are skipped - similar to ".gitignore". It also honors ".dockerignore" file if deploying with container file (--dockerfile).
Examples:
To deploy using a container image:
$ tsuru job deploy -j <JOB> --image registry.example.com/my-company/my-job:v42
To deploy using container file ("docker build" mode):
Sending the the current directory as container build context - uses Dockerfile file as container image instructions:
$ tsuru job deploy -j <JOB> --dockerfile .
Sending a specific container file and specific directory as container build context:
$ tsuru job deploy -j <JOB> --dockerfile ./Dockerfile.other ./other/
`,
MinArgs: 0,
}
}

func (c *JobDeploy) Run(context *cmd.Context) error {
context.RawOutput()

if c.jobName == "" {
return errors.New(`The name of the job is required.
Use the --job/-j flag to specify it.
`)
}

if c.image == "" && c.dockerfile == "" {
return errors.New("You should provide at least one between Docker image name or Dockerfile to deploy.\n")
}

if c.image != "" && len(context.Args) > 0 {
return errors.New("You can't deploy files and docker image at the same time.\n")
}

if c.image != "" && c.dockerfile != "" {
return errors.New("You can't deploy container image and container file at same time.\n")
}

values := url.Values{}

origin := "job-deploy"
if c.image != "" {
origin = "image"
}
values.Set("origin", origin)

if c.message != "" {
values.Set("message", c.message)
}

u, err := config.GetURLVersion("1.23", "/jobs/"+c.jobName+"/deploy")
if err != nil {
return err
}

body := safe.NewBuffer(nil)
request, err := http.NewRequest("POST", u, body)
if err != nil {
return err
}

buf := safe.NewBuffer(nil)

c.m.Lock()
respBody := prepareUploadStreams(context, buf)
c.m.Unlock()

var archive io.Reader

if c.image != "" {
fmt.Fprintln(context.Stdout, "Deploying container image...")
values.Set("image", c.image)
}

if c.dockerfile != "" {
fmt.Fprintln(context.Stdout, "Deploying with Dockerfile...")

var dockerfile string
dockerfile, archive, err = buildWithContainerFile(c.jobName, c.dockerfile, false, context.Args, nil)
if err != nil {
return err
}

values.Set("dockerfile", dockerfile)
}

if err = uploadFiles(context, request, buf, body, values, archive); err != nil {
return err
}

c.m.Lock()
resp, err := tsuruHTTP.AuthenticatedClient.Do(request)
if err != nil {
c.m.Unlock()
return err
}
defer resp.Body.Close()
c.eventID = resp.Header.Get("X-Tsuru-Eventid")
c.m.Unlock()

var readBuffer [deployOutputBufferSize]byte
var readErr error
for readErr == nil {
var read int
read, readErr = resp.Body.Read(readBuffer[:])
if read == 0 {
continue
}
c.m.Lock()
written, writeErr := respBody.Write(readBuffer[:read])
c.m.Unlock()
if written < read {
return fmt.Errorf("short write processing output, read: %d, written: %d", read, written)
}
if writeErr != nil {
return fmt.Errorf("error writing response: %v", writeErr)
}
}
if readErr != io.EOF {
return fmt.Errorf("error reading response: %v", readErr)
}
if strings.HasSuffix(buf.String(), "\nOK\n") {
return nil
}
return cmd.ErrAbortCommand
}

func (c *JobDeploy) Cancel(ctx cmd.Context) error {
apiClient, err := tsuruHTTP.TsuruClientFromEnvironment()
if err != nil {
return err
}
c.m.Lock()
defer c.m.Unlock()
ctx.RawOutput()
if c.eventID == "" {
return errors.New("event ID not available yet")
}
fmt.Fprintln(ctx.Stdout, cmd.Colorfy("Warning: the deploy is still RUNNING in the background!", "red", "", "bold"))
fmt.Fprint(ctx.Stdout, "Are you sure you want to cancel this deploy? (Y/n) ")
var answer string
fmt.Fscanf(ctx.Stdin, "%s", &answer)
if strings.ToLower(answer) != "y" && answer != "" {
return fmt.Errorf("aborted")
}
_, err = apiClient.EventApi.EventCancel(context.Background(), c.eventID, tsuru.EventCancelArgs{Reason: "Canceled on client."})
return err
}
Loading

0 comments on commit 823d8f5

Please sign in to comment.