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

Add support for Job to kube generate & play #23722

Merged
merged 1 commit into from
Sep 6, 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
2 changes: 1 addition & 1 deletion cmd/podman/kube/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ var (
playOptions = playKubeOptionsWrapper{}
playDescription = `Reads in a structured file of Kubernetes YAML.

Creates pods or volumes based on the Kubernetes kind described in the YAML. Supported kinds are Pods, Deployments, DaemonSets and PersistentVolumeClaims.`
Creates pods or volumes based on the Kubernetes kind described in the YAML. Supported kinds are Pods, Deployments, DaemonSets, Jobs, and PersistentVolumeClaims.`

playCmd = &cobra.Command{
Use: "play [options] KUBEFILE|-",
Expand Down
34 changes: 25 additions & 9 deletions docs/kubernetes_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,28 @@ Note: **N/A** means that the option cannot be supported in a single-node Podman

## DaemonSet Fields

| Field | Support |
|-----------------------------------------|-------------------------------------------------------|
| selector | ✅ |
| template | ✅ |
| minReadySeconds | no |
| strategy\.type | no |
| strategy\.rollingUpdate\.maxSurge | no |
| strategy\.rollingUpdate\.maxUnavailable | no |
| revisionHistoryLimit | no |
| Field | Support |
|-----------------------------------------|---------|
| selector | ✅ |
| template | ✅ |
| minReadySeconds | no |
| strategy\.type | no |
| strategy\.rollingUpdate\.maxSurge | no |
| strategy\.rollingUpdate\.maxUnavailable | no |
| revisionHistoryLimit | no |

## Job Fields

| Field | Support |
|-------------------------|----------------------------------|
| activeDeadlineSeconds | no |
| selector | no (automatically set by k8s) |
| template | ✅ |
| backoffLimit | no |
| completionMode | no |
| completions | no (set to 1 with kube generate) |
| manualSelector | no |
| parallelism | no (set to 1 with kube generate) |
| podFailurePolicy | no |
| suspend | no |
| ttlSecondsAfterFinished | no |
8 changes: 5 additions & 3 deletions docs/source/markdown/podman-kube-generate.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Note that the generated Kubernetes YAML file can be used to re-run the deploymen

Note that if the pod being generated was created with the **--infra-name** flag set, then the generated kube yaml will have the **io.podman.annotations.infra.name** set where the value is the name of the infra container set by the user.

Also note that both Deployment and DaemonSet can only have `restartPolicy` set to `Always`.
Note that both Deployment and DaemonSet can only have `restartPolicy` set to `Always`.

Note that Job can only have `restartPolicy` set to `OnFailure` or `Never`. By default, podman sets it to `Never` when generating a kube yaml using `kube generate`.

## OPTIONS

Expand All @@ -52,9 +54,9 @@ Note: this can only be set with the option `--type=deployment`.

Generate a Kubernetes service object in addition to the Pods. Used to generate a Service specification for the corresponding Pod output. In particular, if the object has portmap bindings, the service specification includes a NodePort declaration to expose the service. A random port is assigned by Podman in the specification.

#### **--type**, **-t**=*pod* | *deployment* | *daemonset*
#### **--type**, **-t**=*pod* | *deployment* | *daemonset* | *job*

The Kubernetes kind to generate in the YAML file. Currently, the only supported Kubernetes specifications are `Pod`, `Deployment` and `DaemonSet`. By default, the `Pod` specification is generated.
The Kubernetes kind to generate in the YAML file. Currently, the only supported Kubernetes specifications are `Pod`, `Deployment`, `Job`, and `DaemonSet`. By default, the `Pod` specification is generated.

## EXAMPLES

Expand Down
1 change: 1 addition & 0 deletions docs/source/markdown/podman-kube-play.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Currently, the supported Kubernetes kinds are:
- ConfigMap
- Secret
- DaemonSet
- Job

`Kubernetes Pods or Deployments`

Expand Down
2 changes: 2 additions & 0 deletions libpod/define/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ const (
K8sKindDeployment = "deployment"
// A DaemonSet kube yaml spec
K8sKindDaemonSet = "daemonset"
// a Job kube yaml spec
K8sKindJob = "job"
)
70 changes: 70 additions & 0 deletions libpod/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,61 @@ func GenerateForKubeDeployment(ctx context.Context, pod *YAMLPod, options entiti
return &dep, nil
}

// GenerateForKubeJob returns a YAMLDeployment from a YAMLPod that is then used to create a kubernetes Job
// kind YAML.
func GenerateForKubeJob(ctx context.Context, pod *YAMLPod, options entities.GenerateKubeOptions) (*YAMLJob, error) {
// Restart policy for Job cannot be set to Always
if options.Type == define.K8sKindJob && pod.Spec.RestartPolicy == v1.RestartPolicyAlways {
return nil, fmt.Errorf("k8s Jobs can not have restartPolicy set to Always; only Never and OnFailure policies allowed")
}

// Create label map that will be added to podSpec and Job metadata
// The matching label lets the job know which pods to manage
appKey := "app"
matchLabels := map[string]string{appKey: pod.Name}
// Add the key:value (app:pod-name) to the podSpec labels
if pod.Labels == nil {
pod.Labels = matchLabels
} else {
pod.Labels[appKey] = pod.Name
}

jobSpec := YAMLJobSpec{
Template: &YAMLPodTemplateSpec{
PodTemplateSpec: v1.PodTemplateSpec{
ObjectMeta: pod.ObjectMeta,
},
Spec: pod.Spec,
},
}

// Set the completions and parallelism to 1 by default for the Job
completions, parallelism := int32(1), int32(1)
jobSpec.Completions = &completions
jobSpec.Parallelism = &parallelism
umohnani8 marked this conversation as resolved.
Show resolved Hide resolved
// Set the restart policy to never as k8s requires a job to have a restart policy
// of onFailure or never set in the kube yaml
jobSpec.Template.Spec.RestartPolicy = v1.RestartPolicyNever

// Create the Deployment object
job := YAMLJob{
Job: v1.Job{
ObjectMeta: v12.ObjectMeta{
Name: pod.Name + "-job",
CreationTimestamp: pod.CreationTimestamp,
Labels: pod.Labels,
},
TypeMeta: v12.TypeMeta{
Kind: "Job",
APIVersion: "batch/v1",
},
},
Spec: &jobSpec,
}

return &job, nil
}

// GenerateForKube generates a v1.PersistentVolumeClaim from a libpod volume.
func (v *Volume) GenerateForKube() *v1.PersistentVolumeClaim {
annotations := make(map[string]string)
Expand Down Expand Up @@ -328,6 +383,15 @@ type YAMLDaemonSetSpec struct {
Strategy *v1.DaemonSetUpdateStrategy `json:"strategy,omitempty"`
}

// YAMLJobSpec represents the same k8s API core JobSpec with a small
// change and that is having Template as a pointer to YAMLPodTemplateSpec
// because Go doesn't omit empty struct and we want to omit Strategy and any fields in the Pod YAML
// if it's empty.
type YAMLJobSpec struct {
v1.JobSpec
Template *YAMLPodTemplateSpec `json:"template,omitempty"`
}

// YAMLDaemonSet represents the same k8s API core DaemonSet with a small change
// and that is having Spec as a pointer to YAMLDaemonSetSpec and Status as a pointer to
// k8s API core DaemonSetStatus.
Expand All @@ -350,6 +414,12 @@ type YAMLDeployment struct {
Status *v1.DeploymentStatus `json:"status,omitempty"`
}

type YAMLJob struct {
v1.Job
Spec *YAMLJobSpec `json:"spec,omitempty"`
Status *v1.JobStatus `json:"status,omitempty"`
}

// YAMLService represents the same k8s API core Service struct with a small
// change and that is having Status as a pointer to k8s API core ServiceStatus.
// Because Go doesn't omit empty struct and we want to omit Status in YAML
Expand Down
24 changes: 22 additions & 2 deletions pkg/domain/infra/abi/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,24 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string,
return nil, err
}
typeContent = append(typeContent, b)
case define.K8sKindJob:
job, err := libpod.GenerateForKubeJob(ctx, libpod.ConvertV1PodToYAMLPod(po), options)
if err != nil {
return nil, err
}
b, err := generateKubeYAML(job)
if err != nil {
return nil, err
}
typeContent = append(typeContent, b)
case define.K8sKindPod:
b, err := generateKubeYAML(libpod.ConvertV1PodToYAMLPod(po))
if err != nil {
return nil, err
}
typeContent = append(typeContent, b)
default:
return nil, fmt.Errorf("invalid generation type - only pods, deployments and daemonsets are currently supported: %+v", options.Type)
return nil, fmt.Errorf("invalid generation type - only pods, deployments, jobs, and daemonsets are currently supported: %+v", options.Type)
}

if options.Service {
Expand Down Expand Up @@ -311,14 +321,24 @@ func getKubePods(ctx context.Context, pods []*libpod.Pod, options entities.Gener
return nil, nil, err
}
out = append(out, b)
case define.K8sKindJob:
job, err := libpod.GenerateForKubeJob(ctx, libpod.ConvertV1PodToYAMLPod(po), options)
if err != nil {
return nil, nil, err
}
b, err := generateKubeYAML(job)
if err != nil {
return nil, nil, err
}
out = append(out, b)
case define.K8sKindPod:
b, err := generateKubeYAML(libpod.ConvertV1PodToYAMLPod(po))
if err != nil {
return nil, nil, err
}
out = append(out, b)
default:
return nil, nil, fmt.Errorf("invalid generation type - only pods, deployments and daemonsets are currently supported")
return nil, nil, fmt.Errorf("invalid generation type - only pods, deployments, jobs, and daemonsets are currently supported")
}

if options.Service {
Expand Down
50 changes: 49 additions & 1 deletion pkg/domain/infra/abi/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,22 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options
}
notifyProxies = append(notifyProxies, proxies...)

report.Pods = append(report.Pods, r.Pods...)
validKinds++
ranContainers = true
case "Job":
var jobYAML v1.Job

if err := yaml.Unmarshal(document, &jobYAML); err != nil {
return nil, fmt.Errorf("unable to read YAML as Kube Job: %w", err)
}

r, proxies, err := ic.playKubeJob(ctx, &jobYAML, options, &ipIndex, configMaps, serviceContainer)
if err != nil {
return nil, err
}
notifyProxies = append(notifyProxies, proxies...)

report.Pods = append(report.Pods, r.Pods...)
validKinds++
ranContainers = true
Expand Down Expand Up @@ -549,6 +565,29 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM
return &report, proxies, nil
}

func (ic *ContainerEngine) playKubeJob(ctx context.Context, jobYAML *v1.Job, options entities.PlayKubeOptions, ipIndex *int, configMaps []v1.ConfigMap, serviceContainer *libpod.Container) (*entities.PlayKubeReport, []*notifyproxy.NotifyProxy, error) {
var (
jobName string
podSpec v1.PodTemplateSpec
report entities.PlayKubeReport
)

jobName = jobYAML.ObjectMeta.Name
if jobName == "" {
return nil, nil, errors.New("job does not have a name")
}
podSpec = jobYAML.Spec.Template

podName := fmt.Sprintf("%s-pod", jobName)
podReport, proxies, err := ic.playKubePod(ctx, podName, &podSpec, options, ipIndex, jobYAML.Annotations, configMaps, serviceContainer)
if err != nil {
return nil, nil, fmt.Errorf("encountered while bringing up pod %s: %w", podName, err)
}
report.Pods = podReport.Pods

return &report, proxies, nil
}

func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions, ipIndex *int, annotations map[string]string, configMaps []v1.ConfigMap, serviceContainer *libpod.Container) (*entities.PlayKubeReport, []*notifyproxy.NotifyProxy, error) {
var (
writer io.Writer
Expand Down Expand Up @@ -1502,7 +1541,7 @@ func sortKubeKinds(documentList [][]byte) ([][]byte, error) {
}

switch kind {
case "Pod", "Deployment", "DaemonSet":
case "Pod", "Deployment", "DaemonSet", "Job":
sortedDocumentList = append(sortedDocumentList, document)
default:
sortedDocumentList = append([][]byte{document}, sortedDocumentList...)
Expand Down Expand Up @@ -1633,6 +1672,15 @@ func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, body io.Reader, opt
}
podName := fmt.Sprintf("%s-pod", deploymentName)
podNames = append(podNames, podName)
case "Job":
var jobYAML v1.Job

if err := yaml.Unmarshal(document, &jobYAML); err != nil {
return nil, fmt.Errorf("unable to read YAML as Kube Job: %w", err)
}
jobName := jobYAML.ObjectMeta.Name
podName := fmt.Sprintf("%s-pod", jobName)
podNames = append(podNames, podName)
case "PersistentVolumeClaim":
var pvcYAML v1.PersistentVolumeClaim
if err := yaml.Unmarshal(document, &pvcYAML); err != nil {
Expand Down
Loading