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

feat: job with dockerfile #233

Merged
merged 5 commits into from
Oct 9, 2024
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
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
Loading