Skip to content

Commit

Permalink
feat: job with dockerfile support
Browse files Browse the repository at this point in the history
  • Loading branch information
crgisch committed Sep 9, 2024
1 parent d3de9bf commit 6981641
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 11 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
200 changes: 200 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,202 @@ 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
imageCommand 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 the application 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 app's platform build process (just sending source code and/or configurations):
Uploading all files within the current directory
$ tsuru app deploy -a <APP> .
Uploading all files within a specific directory
$ tsuru app deploy -a <APP> mysite/
Uploading specific files
$ tsuru app deploy -a <APP> ./myfile.jar ./Procfile
Uploading specific files (ignoring their base directories)
$ tsuru app deploy -a <APP> --files-only ./my-code/main.go ./tsuru_stuff/Procfile
To deploy using a container image:
$ tsuru app deploy -a <APP> --image registry.example.com/my-company/app: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 app deploy -a <APP> --dockerfile .
Sending a specific container file and specific directory as container build context:
$ tsuru app deploy -a <APP> --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
}
20 changes: 14 additions & 6 deletions tsuru/client/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ The [[--tag]] parameter sets a tag to your job. You can set multiple [[--tag]] p
The [[--max-running-time]] sets maximum amount of time (in seconds) that the job
can run. If the job exceeds this limit, it will be automatically stopped. If
this parameter is not informed, default value is 3600s`,
MinArgs: 2,
MinArgs: 1,
}
}

Expand Down Expand Up @@ -140,13 +140,21 @@ func (c *JobCreate) Run(ctx *cmd.Context) error {
if c.manual && c.schedule != "" {
return errors.New("cannot set both manual job and schedule options")
}

var image string
var parsedCommands []string
jobName := ctx.Args[0]
image := ctx.Args[1]
commands := ctx.Args[2:]
parsedCommands, err := parseJobCommands(commands)
if err != nil {
return err
if len(ctx.Args) > 1 {
fmt.Fprintf(ctx.Stdout, "Job creation with image is being deprecated. You should use 'tsuru job deploy' to set a job`s image\n")
image = ctx.Args[1]
commands := ctx.Args[2:]

parsedCommands, err = parseJobCommands(commands)
if err != nil {
return err
}
}

var activeDeadlineSecondsResult *int64
if c.fs != nil {
c.fs.Visit(func(f *gnuflag.Flag) {
Expand Down
1 change: 1 addition & 0 deletions tsuru/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Services aren’t managed by tsuru, but by their creators.`)
m.Register(&client.JobDelete{})
m.Register(&client.JobTrigger{})
m.Register(&client.JobLog{})
m.Register(&client.JobDeploy{})

m.Register(&client.PluginInstall{})
m.Register(&client.PluginRemove{})
Expand Down

0 comments on commit 6981641

Please sign in to comment.