Browse Source

support cli option for waiting for a job run

Alexander Belanger 4 năm trước cách đây
mục cha
commit
fb571b8f1c
3 tập tin đã thay đổi với 164 bổ sung2 xóa
  1. 23 0
      api/client/k8s.go
  2. 4 0
      api/types/jobs.go
  3. 137 2
      cli/cmd/job.go

+ 23 - 0
api/client/k8s.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/api/types"
+	v1 "k8s.io/api/batch/v1"
 )
 
 // GetK8sNamespaces gets a namespaces list in a k8s cluster
@@ -66,6 +67,28 @@ func (c *Client) GetRelease(
 	return resp, err
 }
 
+func (c *Client) GetJobs(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace, name string,
+) ([]v1.Job, error) {
+	respArr := make([]v1.Job, 0)
+
+	resp := &respArr
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/namespaces/%s/releases/%s/0/jobs",
+			projectID, clusterID,
+			namespace, name,
+		),
+		nil,
+		resp,
+	)
+
+	return *resp, err
+}
+
 // GetK8sAllPods gets all pods for a given release
 func (c *Client) GetK8sAllPods(
 	ctx context.Context,

+ 4 - 0
api/types/jobs.go

@@ -1,5 +1,9 @@
 package types
 
+import v1 "k8s.io/api/batch/v1"
+
 const (
 	URLParamJobName URLParam = "name"
 )
+
+type GetJobsResponse []v1.Job

+ 137 - 2
cli/cmd/job.go

@@ -4,15 +4,22 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
+	"time"
 
 	"github.com/fatih/color"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/spf13/cobra"
+	v1 "k8s.io/api/batch/v1"
 )
 
+var jobCmd = &cobra.Command{
+	Use: "job",
+}
+
 var batchImageUpdateCmd = &cobra.Command{
-	Use:   "job update-images",
+	Use:   "update-images",
 	Short: "Updates the image tag of all jobs in a namespace which use a specific image.",
 	Long: fmt.Sprintf(`
 %s 
@@ -43,10 +50,43 @@ use the --namespace flag:
 	},
 }
 
+var waitCmd = &cobra.Command{
+	Use:   "wait",
+	Short: "Waits for a job to complete.",
+	Long: fmt.Sprintf(`
+%s 
+
+Waits for a job with a given name and namespace to complete a run. If the job completes successfully,
+this command exits with exit code 0. Otherwise, this command exits with exit code 1. 
+
+Example commands:
+
+  %s
+
+This command is namespace-scoped and uses the default namespace. To specify a different namespace, 
+use the --namespace flag:
+
+  %s
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job wait\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example --namespace custom-namespace"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, waitForJob)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var imageRepoURI string
 
 func init() {
-	rootCmd.AddCommand(batchImageUpdateCmd)
+	rootCmd.AddCommand(jobCmd)
+	jobCmd.AddCommand(batchImageUpdateCmd)
+	jobCmd.AddCommand(waitCmd)
 
 	batchImageUpdateCmd.PersistentFlags().StringVar(
 		&tag,
@@ -72,6 +112,22 @@ func init() {
 
 	batchImageUpdateCmd.MarkPersistentFlagRequired("image-repo-uri")
 	batchImageUpdateCmd.MarkPersistentFlagRequired("tag")
+
+	waitCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"",
+		"The namespace of the jobs.",
+	)
+
+	waitCmd.PersistentFlags().StringVar(
+		&name,
+		"name",
+		"",
+		"The name of the jobs.",
+	)
+
+	waitCmd.MarkPersistentFlagRequired("name")
 }
 
 func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
@@ -88,3 +144,82 @@ func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client,
 		},
 	)
 }
+
+// waits for a job with a given name/namespace
+func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	// get the job release
+	jobRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, name)
+
+	if err != nil {
+		return err
+	}
+
+	// make sure the job chart has a manual job running
+	pausedVal, ok := jobRelease.Release.Config["paused"]
+	pausedErr := fmt.Errorf("this job template is not currently running a manual job")
+
+	if !ok {
+		return pausedErr
+	}
+
+	if pausedValBool, ok := pausedVal.(bool); ok && pausedValBool {
+		return pausedErr
+	}
+
+	// if no job exists with the given revision, wait up to 5 minutes
+	timeWait := time.Now().Add(5 * time.Minute)
+
+	for timeNow := time.Now(); timeNow.Before(timeWait); {
+		// get the jobs for that job chart
+		jobs, err := client.GetJobs(context.Background(), config.Project, config.Cluster, namespace, name)
+
+		if err != nil {
+			return err
+		}
+
+		job := getJobMatchingRevision(uint(jobRelease.Release.Version), jobs)
+
+		if job == nil {
+			time.Sleep(10 * time.Second)
+			continue
+		}
+
+		// once job is running, wait for status to be completed, or failed
+		// if failed, exit with non-zero exit code
+		if job.Status.Failed > 0 {
+			return fmt.Errorf("job failed")
+		}
+
+		if job.Status.Succeeded > 0 {
+			return nil
+		}
+
+		// otherwise, return no error
+		time.Sleep(10 * time.Second)
+		continue
+	}
+
+	return fmt.Errorf("timed out waiting for job")
+}
+
+func getJobMatchingRevision(revision uint, jobs []v1.Job) *v1.Job {
+	for _, job := range jobs {
+		revisionLabel, revisionLabelExists := job.Labels["helm.sh/revision"]
+
+		if !revisionLabelExists {
+			continue
+		}
+
+		jobRevision, err := strconv.ParseUint(revisionLabel, 10, 64)
+
+		if err != nil {
+			continue
+		}
+
+		if uint(jobRevision) == revision {
+			return &job
+		}
+	}
+
+	return nil
+}