Skip to content

Commit

Permalink
adds job CLI; WIP; mid-unit tests
Browse files Browse the repository at this point in the history
adds unit tests

fixes job templates; fixes unit tests

adds job cli tests
  • Loading branch information
stinkyfingers committed Jul 21, 2021
1 parent 4997b2a commit 6554f49
Show file tree
Hide file tree
Showing 16 changed files with 879 additions and 14 deletions.
43 changes: 43 additions & 0 deletions cli_tests/app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ setup() {
FRAMEWORK="myframework"
APP_IMAGE="gcr.io/shipa-ci/sample-go-app:latest"
APP_NAME="sample-app"
JOB_NAME="sample-job"
CNAME="my-cname.com"
TEST_ENVVAR_KEY="FOO"
TEST_ENVVAR_VALUE="BAR"
Expand All @@ -25,6 +26,7 @@ setup() {
teardown() {
rm -f app.yaml
rm -f framework.yaml
rm -f job.yaml
}

@test "help" {
Expand Down Expand Up @@ -197,6 +199,47 @@ EOF
[[ $status -eq 0 ]]
}

@test "job deploy with yaml file" {
cat << EOF > job.yaml
name: "$JOB_NAME"
version: v1
type: Job
framework: "$FRAMEWORK"
description: cli test job
EOF
run $KETCH job deploy job.yaml
[[ $status -eq 0 ]]

dataRegex="$JOB_NAME[ \t]+v1[ \t]+$FRAMEWORK[ \t]+cli test job"
result=$($KETCH job list $JOB_NAME)
echo "RECEIVED:" $result
[[ $result =~ $dataRegex ]]
}

@test "job list" {
result=$($KETCH job list)
headerRegex="NAME[ \t]+VERSION[ \t]+FRAMEWORK[ \t]+DESCRIPTION"
dataRegex="$JOB_NAME[ \t]+v1[ \t]+$FRAMEWORK[ \t]+cli test job"
echo "RECEIVED:" $result
[[ $result =~ $headerRegex ]]
[[ $result =~ $dataRegex ]]
}

@test "job export" {
run $KETCH job export "$JOB_NAME" -f job.yaml
result=$(cat job.yaml)
echo "RECEIVED:" $result
[[ $result =~ "name: $JOB_NAME" ]]
[[ $result =~ "type: Job" ]]
[[ $result =~ "framework: $FRAMEWORK" ]]
}

@test "job remove" {
result=$($KETCH job remove "$JOB_NAME")
echo "RECEIVED:" $result
[[ $result =~ "Successfully removed!" ]]
}

@test "builder list" {
result=$($KETCH builder list)
headerRegex="VENDOR[ \t]+IMAGE[ \t]+DESCRIPTION"
Expand Down
8 changes: 8 additions & 0 deletions cmd/ketch/auto_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,11 @@ func autoCompleteFrameworkNames(cfg config, toComplete ...string) ([]string, cob
func autoCompleteBuilderNames(cfg config, toComplete ...string) ([]string, cobra.ShellCompDirective) {
return builderList.Names(toComplete...), cobra.ShellCompDirectiveNoSpace
}

func autoCompleteJobNames(cfg config, toComplete ...string) ([]string, cobra.ShellCompDirective) {
names, err := jobListNames(cfg, toComplete...)
if err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveError
}
return names, cobra.ShellCompDirectiveNoSpace
}
3 changes: 3 additions & 0 deletions cmd/ketch/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const (
ErrInvalidAppName cliError = "invalid app name, app name should have at most 40 " +
"characters, containing only lower case letters, numbers or dashes, starting with a letter"

ErrInvalidJobName cliError = "invalid job name, job name should have at most 40 " +
"characters, containing only lower case letters, numbers or dashes, starting with a letter"

ErrNoEntrypointAndCmd cliError = "image doesn't have entrypoint and cmd set"
ErrLogUnknownTimeFormat cliError = "unknown time format"

Expand Down
55 changes: 55 additions & 0 deletions cmd/ketch/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"context"
"fmt"
"io"

"k8s.io/apimachinery/pkg/types"

ketchv1 "github.com/shipa-corp/ketch/internal/api/v1beta1"
"github.com/spf13/cobra"
)

const jobHelp = `
Manage jobs.
`

func newJobCmd(cfg config, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "job",
Short: "Manage Jobs",
Long: jobHelp,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Usage()
},
}
cmd.AddCommand(newJobListCmd(cfg, out))
cmd.AddCommand(newJobDeployCmd(cfg, out))
cmd.AddCommand(newJobRemoveCmd(cfg, out))
cmd.AddCommand(newJobExportCmd(cfg, out))
return cmd
}

func getJob(ctx context.Context, cfg config, jobName string) (*ketchv1.Job, error) {
var frameworks ketchv1.FrameworkList
if err := cfg.Client().List(ctx, &frameworks); err != nil {
return nil, fmt.Errorf("failed to get frameworks: %w", err)
}
var namespace string
for _, framework := range frameworks.Items {
if framework.HasJob(jobName) {
namespace = framework.Spec.NamespaceName
break
}
}
if namespace == "" {
return nil, fmt.Errorf("cannot find framework with job %s", jobName)
}

job := ketchv1.Job{}
if err := cfg.Client().Get(ctx, types.NamespacedName{Name: jobName, Namespace: namespace}, &job); err != nil {
return nil, fmt.Errorf("failed to get job: %w", err)
}
return &job, nil
}
96 changes: 96 additions & 0 deletions cmd/ketch/job_deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"os"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"

ketchv1 "github.com/shipa-corp/ketch/internal/api/v1beta1"
)

const jobDeployHelp = `
List all jobs.
`

const (
defaultJobVersion = "v1"
defaultJobParallelism = 1
defaultJobCompletions = 1
defaultJobBackoffLimit = 6
defaultJobRestartPolicy = "Never"
)

func newJobDeployCmd(cfg config, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "deploy [FILENAME]",
Short: "Deploy a job.",
Long: jobDeployHelp,
RunE: func(cmd *cobra.Command, args []string) error {
filename := args[0]
return jobDeploy(cmd.Context(), cfg, filename, out)
},
}
return cmd
}

func jobDeploy(ctx context.Context, cfg config, filename string, out io.Writer) error {
b, err := os.ReadFile(filename)
if err != nil {
return err
}
var job ketchv1.Job
err = yaml.Unmarshal(b, &job.Spec)
if err != nil {
return err
}
setJobSpecDefaults(&job.Spec)
if err = validateJobSpec(&job.Spec); err != nil {
return err
}
var framework ketchv1.Framework
if err := cfg.Client().Get(ctx, types.NamespacedName{Name: job.Spec.Framework}, &framework); err != nil {
return fmt.Errorf("failed to get framework: %s", job.Spec.Framework)
}
job.ObjectMeta.Namespace = framework.Spec.NamespaceName
job.ObjectMeta.Name = job.Spec.Name
if err := cfg.Client().Create(ctx, &job); err != nil {
return fmt.Errorf("failed to create job: %w", err)
}
fmt.Fprintln(out, "Successfully added!")
return nil
}

// setJobSpecDefaults sets defaults on job.Spec for some unset fields
func setJobSpecDefaults(jobSpec *ketchv1.JobSpec) {
jobSpec.Type = "Job"
if jobSpec.Version == "" {
jobSpec.Version = defaultJobVersion
}
if jobSpec.Parallelism == 0 {
jobSpec.Parallelism = defaultJobParallelism
}
if jobSpec.Completions == 0 && jobSpec.Parallelism > 1 {
jobSpec.Completions = defaultJobCompletions
}
if jobSpec.BackoffLimit == 0 {
jobSpec.BackoffLimit = defaultJobBackoffLimit
}
if jobSpec.Policy.RestartPolicy == "" {
jobSpec.Policy.RestartPolicy = defaultJobRestartPolicy
}
}

// validateJobSpec assures that required fields are populated. Missing fields will throw errors
// when the custom resource is created, but this is a way to surface errors to user clearly.
func validateJobSpec(jobSpec *ketchv1.JobSpec) error {
if jobSpec.Name == "" || jobSpec.Framework == "" {
return errors.New("job.name and job.framework are required")
}
return nil
}
133 changes: 133 additions & 0 deletions cmd/ketch/job_deploy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"bytes"
"context"
"os"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"

ketchv1 "github.com/shipa-corp/ketch/internal/api/v1beta1"
"github.com/shipa-corp/ketch/internal/mocks"
"github.com/shipa-corp/ketch/internal/utils/conversions"
)

func TestJobDeploy(t *testing.T) {
mockFramework := &ketchv1.Framework{ObjectMeta: metav1.ObjectMeta{Name: "myframework"}, Spec: ketchv1.FrameworkSpec{
Version: "v1",
NamespaceName: "ketch-myframework",
Name: "myframework",
AppQuotaLimit: conversions.IntPtr(1),
IngressController: ketchv1.IngressControllerSpec{
ClassName: "traefik",
ServiceEndpoint: "10.10.20.30",
IngressType: "traefik",
ClusterIssuer: "letsencrypt",
},
}}
tests := []struct {
name string
jobName string
cfg config
filename string
yamlData string
wantJobSpec ketchv1.JobSpec
wantOut string
wantErr string
}{
{
name: "job from yaml file",
jobName: "hello",
cfg: &mocks.Configuration{
CtrlClientObjects: []runtime.Object{mockFramework},
DynamicClientObjects: []runtime.Object{},
},
filename: "job.yaml",
yamlData: `name: hello
version: v1
framework: myframework
description: test
parallelism: 1
completions: 1
suspend: false
backoffLimit: 6
containers:
- name: lister
image: ubuntu
command:
- ls
- /
policy:
restartPolicy: Never
`,
wantJobSpec: ketchv1.JobSpec{
Name: "hello",
Version: "v1",
Type: "Job",
Framework: "myframework",
Description: "test",
Parallelism: 1,
Completions: 1,
Suspend: false,
BackoffLimit: 6,
Containers: []ketchv1.Container{
{
Name: "lister",
Image: "ubuntu",
Command: []string{"ls", "/"},
},
},
Policy: ketchv1.Policy{
RestartPolicy: "Never",
},
},
wantOut: "Successfully added!\n",
},
{
name: "error - no framework found",
jobName: "hello",
cfg: &mocks.Configuration{
CtrlClientObjects: []runtime.Object{mockFramework},
DynamicClientObjects: []runtime.Object{},
},
filename: "job.yaml",
yamlData: `name: hello
version: v1
framework: NOFRAMEWORK
description: test
`,
wantErr: "failed to get framework: NOFRAMEWORK",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.yamlData != "" {
file, err := os.CreateTemp(t.TempDir(), "*.yaml")
require.Nil(t, err)
_, err = file.Write([]byte(tt.yamlData))
require.Nil(t, err)
defer os.Remove(file.Name())
tt.filename = file.Name()
}
out := &bytes.Buffer{}
err := jobDeploy(context.Background(), tt.cfg, tt.filename, out)
if len(tt.wantErr) > 0 {
require.NotNil(t, err)
require.Equal(t, tt.wantErr, err.Error())
return
} else {
require.Nil(t, err)
}
require.Equal(t, tt.wantOut, out.String())

gotJob := ketchv1.Job{}
err = tt.cfg.Client().Get(context.Background(), types.NamespacedName{Name: tt.jobName, Namespace: mockFramework.Spec.NamespaceName}, &gotJob)
require.Nil(t, err)
require.Equal(t, tt.wantJobSpec, gotJob.Spec)
})
}
}
Loading

0 comments on commit 6554f49

Please sign in to comment.