Skip to content
This repository has been archived by the owner on Mar 16, 2024. It is now read-only.

Commit

Permalink
Add jobs command and jobs restart subcommand
Browse files Browse the repository at this point in the history
This commit adds:
- "acorn job" command to list Jobs created by Apps
- "acorn job restart" subcommand to restart a Job created by an App
- Various integration tests for the above

Signed-off-by: tylerslaton <mtslaton1@gmail.com>
  • Loading branch information
tylerslaton committed Aug 16, 2023
1 parent 0f67bb5 commit 609aa56
Show file tree
Hide file tree
Showing 33 changed files with 1,291 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/docs/100-reference/01-command-line/acorn.md
Expand Up @@ -38,6 +38,7 @@ acorn [flags]
* [acorn image](acorn_image.md) - Manage images
* [acorn info](acorn_info.md) - Info about acorn installation
* [acorn install](acorn_install.md) - Install and configure acorn in the cluster
* [acorn job](acorn_job.md) - Manage jobs
* [acorn login](acorn_login.md) - Add registry credentials
* [acorn logout](acorn_logout.md) - Remove registry credentials
* [acorn logs](acorn_logs.md) - Log all workloads from an app
Expand Down
40 changes: 40 additions & 0 deletions docs/docs/100-reference/01-command-line/acorn_job.md
@@ -0,0 +1,40 @@
---
title: "acorn job"
---
## acorn job

Manage jobs

```
acorn job [flags] [ACORN_NAME|JOB_NAME...]
```

### Examples

```
acorn jobs
```

### Options

```
-h, --help help for job
-o, --output string Output format (json, yaml, {{gotemplate}})
-q, --quiet Output only names
```

### Options inherited from parent commands

```
--debug Enable debug logging
--debug-level int Debug log level (valid 0-9) (default 7)
--kubeconfig string Explicitly use kubeconfig file, overriding the default context
-j, --project string Project to work in
```

### SEE ALSO

* [acorn](acorn.md) -
* [acorn job restart](acorn_job_restart.md) - Restart a job

39 changes: 39 additions & 0 deletions docs/docs/100-reference/01-command-line/acorn_job_restart.md
@@ -0,0 +1,39 @@
---
title: "acorn job restart"
---
## acorn job restart

Restart a job

```
acorn job restart [JOB_NAME...] [flags]
```

### Examples

```
acorn job restart app-name.job-name
```

### Options

```
-h, --help help for restart
```

### Options inherited from parent commands

```
--debug Enable debug logging
--debug-level int Debug log level (valid 0-9) (default 7)
--kubeconfig string Explicitly use kubeconfig file, overriding the default context
-o, --output string Output format (json, yaml, {{gotemplate}})
-j, --project string Project to work in
-q, --quiet Output only names
```

### SEE ALSO

* [acorn job](acorn_job.md) - Manage jobs

1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -45,6 +45,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.49
github.com/rancher/wrangler v1.0.2
github.com/robfig/cron/v3 v3.0.1
github.com/sigstore/cosign/v2 v2.0.2
github.com/sigstore/sigstore v1.6.4
github.com/sirupsen/logrus v1.9.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -772,6 +772,8 @@ github.com/rancher/lasso v0.0.0-20221227210133-6ea88ca2fbcc h1:29VHrInLV4qSevvcv
github.com/rancher/lasso v0.0.0-20221227210133-6ea88ca2fbcc/go.mod h1:dEfC9eFQigj95lv/JQ8K5e7+qQCacWs1aIA6nLxKzT8=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down
13 changes: 13 additions & 0 deletions integration/client/client.go
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/acorn-io/runtime/integration/helper"
"github.com/acorn-io/runtime/pkg/client"
"github.com/stretchr/testify/require"
)

func NewImageWithSidecar(t *testing.T, namespace string) string {
Expand All @@ -20,6 +21,18 @@ func NewImageWithSidecar(t *testing.T, namespace string) string {
return image.ID
}

func NewImageWithJobs(t *testing.T, namespace string) string {
t.Helper()

c := helper.BuilderClient(t, namespace)
image, err := c.AcornImageBuild(helper.GetCTX(t), "../testdata/job/Acornfile", &client.AcornImageBuildOptions{
Cwd: "../testdata/job",
})
require.NoError(t, err)

return image.ID
}

func NewImage2(t *testing.T, namespace string) string {
t.Helper()

Expand Down
141 changes: 141 additions & 0 deletions integration/client/jobs/jobs_test.go
@@ -0,0 +1,141 @@
package containers

import (
"strings"
"testing"

client2 "github.com/acorn-io/runtime/integration/client"
"github.com/acorn-io/runtime/integration/helper"
apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
"github.com/acorn-io/runtime/pkg/client"
kclient "github.com/acorn-io/runtime/pkg/k8sclient"
"github.com/acorn-io/runtime/pkg/publicname"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestJobList(t *testing.T) {
helper.StartController(t)
restConfig := helper.StartAPI(t)

ctx := helper.GetCTX(t)
lclient, err := kclient.New(restConfig)
require.NoError(t, err)

kclient := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kclient)

c, err := client.New(restConfig, "", project.Name)
require.NoError(t, err)

imageID := client2.NewImageWithJobs(t, project.Name)
app, err := c.AppRun(ctx, imageID, nil)
require.NoError(t, err)

app = helper.WaitForObject(t, lclient.Watch, &apiv1.AppList{}, app, func(app *apiv1.App) bool {
return app.Status.AppStatus.Jobs["job"].CompletionTime != nil
})

jobs, err := c.JobList(ctx, nil)
require.NoError(t, err)

require.Len(t, jobs, 2)
for _, job := range jobs {
assert.Truef(t, strings.HasPrefix(job.Name, app.Name+"."), "not prefix %s %s", job.Name, app.Name)
assert.Equal(t, app.Namespace, job.Namespace)
}
}

func TestJobGet(t *testing.T) {
helper.StartController(t)
restConfig := helper.StartAPI(t)

ctx := helper.GetCTX(t)
lclient, err := kclient.New(restConfig)
require.NoError(t, err)

kclient := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kclient)

c, err := client.New(restConfig, "", project.Name)
require.NoError(t, err)

imageID := client2.NewImageWithJobs(t, project.Name)
app, err := c.AppRun(ctx, imageID, nil)
require.NoError(t, err)

helper.WaitForObject(t, lclient.Watch, &apiv1.AppList{}, app, func(app *apiv1.App) bool {
return app.Status.AppStatus.Jobs["job"].CompletionTime != nil
})

jobs, err := c.JobList(ctx, nil)
require.NoError(t, err)

// Determine which job is the cronjob and which is the job
require.Len(t, jobs, 2)
jobFromList, cronjobFromList := jobs[0], jobs[1]
if cronjobFromList.Spec.Schedule == "" {
jobFromList = jobs[1]
cronjobFromList = jobs[0]
}

// Check that the job without a schedule is correct
job, err := c.JobGet(ctx, jobFromList.Name)
require.NoError(t, err)

assert.Nil(t, jobFromList.Status.NextRun)
assert.Equal(t, jobFromList.Name, job.Name)
assert.Equal(t, jobFromList.Namespace, job.Namespace)
assert.Equal(t, jobFromList.UID, job.UID)

// Check that the cronjob is correct
cronjob, err := c.JobGet(ctx, cronjobFromList.Name)
require.NoError(t, err)

assert.Equal(t, cronjobFromList.Name, cronjob.Name)
assert.Equal(t, cronjobFromList.Namespace, cronjob.Namespace)
assert.Equal(t, cronjobFromList.UID, cronjob.UID)
}

func TestJobRestart(t *testing.T) {
helper.StartController(t)
restConfig := helper.StartAPI(t)

ctx := helper.GetCTX(t)
lclient, err := kclient.New(restConfig)
require.NoError(t, err)

kclient := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kclient)

c, err := client.New(restConfig, "", project.Name)
require.NoError(t, err)

imageID := client2.NewImageWithJobs(t, project.Name)
app, err := c.AppRun(ctx, imageID, nil)
require.NoError(t, err)

// Wait for the Job to initially complete
var firstCompletion *metav1.Time
helper.WaitForObject(t, lclient.Watch, &apiv1.AppList{}, app, func(app *apiv1.App) bool {
firstCompletion = app.Status.AppStatus.Jobs["job"].CompletionTime
return app.Status.Namespace != "" && app.Status.AppStatus.Jobs["job"].CompletionTime != nil
})

require.NoError(t, c.JobRestart(ctx, publicname.ForChild(app, "job")))

// Wait for the Job to complete again by checking for a difference in the completion time
helper.WaitForObject(t, lclient.Watch, &apiv1.AppList{}, app, func(app *apiv1.App) bool {
secondCompletion := app.Status.AppStatus.Jobs["job"].CompletionTime
return app.Status.Namespace != "" && !firstCompletion.Equal(secondCompletion)
})

require.NoError(t, c.JobRestart(ctx, publicname.ForChild(app, "cronjob")))

// Wait for the CronJob to complete once, which means it has been restarted since the job
// is scheduled to never run
helper.WaitForObject(t, lclient.Watch, &apiv1.AppList{}, app, func(app *apiv1.App) bool {
return app.Status.AppStatus.Jobs["cronjob"].LastRun != nil
})
}
9 changes: 9 additions & 0 deletions integration/client/testdata/job/Acornfile
@@ -0,0 +1,9 @@
jobs: {
job: {
image:"ghcr.io/acorn-io/images-mirror/alpine:latest"
}
cronjob: {
image:"ghcr.io/acorn-io/images-mirror/alpine:latest"
schedule: "0 0 31 2 *" // February 31st, never runs
}
}
3 changes: 3 additions & 0 deletions pkg/apis/api.acorn.io/v1/scheme.go
Expand Up @@ -53,6 +53,9 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
&ContainerReplicaList{},
&ContainerReplicaExecOptions{},
&ContainerReplicaPortForwardOptions{},
&Job{},
&JobRestart{},
&JobList{},
&Secret{},
&SecretList{},
&Service{},
Expand Down
37 changes: 37 additions & 0 deletions pkg/apis/api.acorn.io/v1/types.go
Expand Up @@ -96,6 +96,43 @@ type ContainerReplicaStatus struct {
Started *bool `json:"started,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type Job struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

Spec JobSpec `json:"spec,omitempty"`
Status v1.JobStatus `json:"status,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type JobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Job `json:"items"`
}

type JobSpec struct {
AppName string `json:"appName,omitempty"`
JobName string `json:"jobName,omitempty"`
Schedule string `json:"schedule,omitempty"`
}

type JobColumns struct {
State string `json:"state,omitempty"`
App string `json:"app,omitempty"`
NextRun *metav1.Time `json:"nextRun,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type JobRestart struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
}

// EnsureRegion checks or sets the region of a ContainerReplica.
// If a ContainerReplica's region is unset, EnsureRegion sets it to the given region and returns true.
// Otherwise, it returns true if and only if the ContainerReplica belongs to the given region.
Expand Down

0 comments on commit 609aa56

Please sign in to comment.