diff --git a/cmd/podman/kube/generate.go b/cmd/podman/kube/generate.go index 35a9c5710a..046f697bef 100644 --- a/cmd/podman/kube/generate.go +++ b/cmd/podman/kube/generate.go @@ -4,12 +4,14 @@ import ( "fmt" "io" "os" + "strings" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/generate" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/utils" + "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -68,10 +70,28 @@ func generateFlags(cmd *cobra.Command) { flags.StringVarP(&generateFile, filenameFlagName, "f", "", "Write output to the specified path") _ = cmd.RegisterFlagCompletionFunc(filenameFlagName, completion.AutocompleteDefault) + // TODO: default should be configurable in containers.conf + typeFlagName := "type" + flags.StringVarP(&generateOptions.Type, typeFlagName, "t", define.K8sKindPod, "Generate YAML for the given Kubernetes kind") + _ = cmd.RegisterFlagCompletionFunc(typeFlagName, completion.AutocompleteNone) + + replicasFlagName := "replicas" + flags.Int32VarP(&generateOptions.Replicas, replicasFlagName, "r", 1, "Set the replicas number for Deployment kind") + _ = cmd.RegisterFlagCompletionFunc(replicasFlagName, completion.AutocompleteNone) + flags.SetNormalizeFunc(utils.AliasFlags) } func generateKube(cmd *cobra.Command, args []string) error { + typeVal, err := cmd.Flags().GetString("type") + if err != nil { + return err + } + typeVal = strings.ToLower(typeVal) + if typeVal != define.K8sKindPod && typeVal != define.K8sKindDeployment { + return fmt.Errorf("invalid type given, only supported types are pod and deployment") + } + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args, generateOptions) if err != nil { return err diff --git a/docs/source/markdown/podman-kube-generate.1.md b/docs/source/markdown/podman-kube-generate.1.md index 583dd2fb17..114497767a 100644 --- a/docs/source/markdown/podman-kube-generate.1.md +++ b/docs/source/markdown/podman-kube-generate.1.md @@ -7,7 +7,7 @@ podman-kube-generate - Generate Kubernetes YAML based on containers, pods or vol ## DESCRIPTION **podman kube generate** will generate Kubernetes YAML (v1 specification) from Podman containers, pods or volumes. Regardless of whether -the input is for containers or pods, Podman will always generate the specification as a Pod. The input may be in the form +the input is for containers or pods, Podman will generate the specification as a Pod by default. The input may be in the form of one or more containers, pods or volumes names or IDs. `Podman Containers or Pods` @@ -34,10 +34,19 @@ Note that the generated Kubernetes YAML file can be used to re-run the deploymen Output to the given file, instead of STDOUT. If the file already exists, `kube generate` will refuse to replace it and return an error. +#### **--replicas**, **-r**=*replica count* + +The value to set `replicas` to when generating a **Deployment** kind. +Note: this can only be set with the option `--type=deployment`. + #### **--service**, **-s** 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 will include a NodePort declaration to expose the service. A random port is assigned by Podman in the specification. +#### **--type**, **-t**=*pod | deployment* + +The Kubernetes kind to generate in the YAML file. Currently, the only supported Kubernetes specifications are `Pod` and `Deployment`. By default, the `Pod` specification will be generated. + ## EXAMPLES Create Kubernetes Pod YAML for a container called `some-mariadb`. @@ -80,6 +89,43 @@ spec: tty: true ``` +Create Kubernetes Deployment YAML with 3 replicas for a container called `dep-ctr` +``` +$ podman kube generate --type deployment --replicas 3 dep-ct +r +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-4.5.0-dev +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: "2023-03-27T20:45:08Z" + labels: + app: dep-ctr-pod + name: dep-ctr-pod-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: dep-ctr-pod + template: + metadata: + annotations: + io.podman.annotations.ulimit: nofile=524288:524288,nproc=127332:127332 + creationTimestamp: "2023-03-27T20:45:08Z" + labels: + app: dep-ctr-pod + name: dep-ctr-pod + spec: + containers: + - command: + - top + image: docker.io/library/alpine:latest + name: dep-ctr +``` + + Create Kubernetes Pod YAML for a container with the directory `/home/user/my-data` on the host bind-mounted in the container to `/volume`. ``` $ podman kube generate my-container-with-bind-mounted-data diff --git a/libpod/define/container.go b/libpod/define/container.go index 97eb1c3bb1..c519326345 100644 --- a/libpod/define/container.go +++ b/libpod/define/container.go @@ -38,3 +38,11 @@ const ( // ContainerInitPath is the default path of the mounted container init. ContainerInitPath = "/run/podman-init" ) + +// Kubernetes Kinds +const ( + // A Pod kube yaml spec + K8sKindPod = "pod" + // A Deployment kube yaml spec + K8sKindDeployment = "deployment" +) diff --git a/libpod/kube.go b/libpod/kube.go index 6ea6d08659..4c8c6a9284 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -18,6 +18,7 @@ import ( cutil "github.com/containers/common/pkg/util" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/annotations" + "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/env" v1 "github.com/containers/podman/v4/pkg/k8s.io/api/core/v1" "github.com/containers/podman/v4/pkg/k8s.io/apimachinery/pkg/api/resource" @@ -107,8 +108,8 @@ func (p *Pod) GenerateForKube(ctx context.Context, getService bool) (*v1.Pod, [] pod.Spec.RestartPolicy = v1.RestartPolicyOnFailure case define.RestartPolicyNo: pod.Spec.RestartPolicy = v1.RestartPolicyNever - default: // some pod create from cmdline, such as "", so set it to Never - pod.Spec.RestartPolicy = v1.RestartPolicyNever + default: // some pod create from cmdline, such as "", so set it to "" as k8s automatically defaults to always + pod.Spec.RestartPolicy = "" } break } @@ -131,6 +132,65 @@ func (p *Pod) getInfraContainer() (*Container, error) { return p.runtime.GetContainer(infraID) } +// GenerateForKubeDeployment returns a YAMLDeployment from a YAMLPod that is then used to create a kubernetes Deployment +// kind YAML. +func GenerateForKubeDeployment(ctx context.Context, pod *YAMLPod, options entities.GenerateKubeOptions) (*YAMLDeployment, error) { + // Restart policy for Deployments can only be set to Always + if options.Type == define.K8sKindDeployment && !(pod.Spec.RestartPolicy == "" || pod.Spec.RestartPolicy == define.RestartPolicyAlways) { + return nil, fmt.Errorf("k8s Deployments can only have restartPolicy set to Always") + } + + // Create label map that will be added to podSpec and Deployment metadata + // The matching label lets the deployment 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 + } + + depSpec := YAMLDeploymentSpec{ + DeploymentSpec: v1.DeploymentSpec{ + Selector: &v12.LabelSelector{ + MatchLabels: matchLabels, + }, + }, + Template: &YAMLPodTemplateSpec{ + PodTemplateSpec: v1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + }, + Spec: pod.Spec, + }, + } + + // Add replicas count if user adds replica number with --replicas flag and is greater than 1 + // If replicas is set to 1, no need to add it to the generated yaml as k8s automatically defaults + // to that. Podman as sets replicas to 1 by default. + if options.Replicas > 1 { + depSpec.Replicas = &options.Replicas + } + + // Create the Deployment opbject + dep := YAMLDeployment{ + Deployment: v1.Deployment{ + ObjectMeta: v12.ObjectMeta{ + Name: pod.Name + "-deployment", + CreationTimestamp: pod.CreationTimestamp, + Labels: pod.Labels, + }, + TypeMeta: v12.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + }, + Spec: &depSpec, + } + + return &dep, nil +} + // GenerateForKube generates a v1.PersistentVolumeClaim from a libpod volume. func (v *Volume) GenerateForKube() *v1.PersistentVolumeClaim { annotations := make(map[string]string) @@ -195,6 +255,37 @@ type YAMLPod struct { Status *v1.PodStatus `json:"status,omitempty"` } +// YAMLPodTemplateSpec represents the same k8s API core PodTemplateStruct with a +// small change and that is having Spec as a pointer to YAMLPodSpec. +// Because Go doesn't omit empty struct and we want to omit any empty structs in the +// Pod yaml. This is used when generating a Deployment kind. +type YAMLPodTemplateSpec struct { + v1.PodTemplateSpec + Spec *YAMLPodSpec `json:"spec,omitempty"` +} + +// YAMLDeploymentSpec represents the same k8s API core DeploymentSpec with a small +// change and that is having Template as a pointer to YAMLPodTemplateSpec and Strategy +// as a pointer to k8s API core DeploymentStrategy. +// 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 YAMLDeploymentSpec struct { + v1.DeploymentSpec + Template *YAMLPodTemplateSpec `json:"template,omitempty"` + Strategy *v1.DeploymentStrategy `json:"strategy,omitempty"` +} + +// YAMLDeployment represents the same k8s API core Deployment with a small change +// and that is having Spec as a pointer to YAMLDeploymentSpec and Status as a pointer to +// k8s API core DeploymentStatus. +// Because Go doesn't omit empty struct and we want to omit Status and any fields in the DeploymentSpec +// if it's empty. +type YAMLDeployment struct { + v1.Deployment + Spec *YAMLDeploymentSpec `json:"spec,omitempty"` + Status *v1.DeploymentStatus `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 diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index 9b38829add..af4ec89222 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/containers/podman/v4/libpod" + "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/api/handlers/utils" api "github.com/containers/podman/v4/pkg/api/types" "github.com/containers/podman/v4/pkg/domain/entities" @@ -89,10 +90,14 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Names []string `schema:"names"` - Service bool `schema:"service"` + Names []string `schema:"names"` + Service bool `schema:"service"` + Type string `schema:"type"` + Replicas int32 `schema:"replicas"` }{ // Defaults would go here. + Type: define.K8sKindPod, + Replicas: 1, } if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -101,7 +106,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { } containerEngine := abi.ContainerEngine{Libpod: runtime} - options := entities.GenerateKubeOptions{Service: query.Service} + options := entities.GenerateKubeOptions{Service: query.Service, Type: query.Type, Replicas: query.Replicas} report, err := containerEngine.GenerateKube(r.Context(), query.Names, options) if err != nil { utils.Error(w, http.StatusInternalServerError, fmt.Errorf("generating YAML: %w", err)) diff --git a/pkg/api/server/register_kube.go b/pkg/api/server/register_kube.go index 73f4bc0ecf..aee2e0081a 100644 --- a/pkg/api/server/register_kube.go +++ b/pkg/api/server/register_kube.go @@ -114,6 +114,17 @@ func (s *APIServer) registerKubeHandlers(r *mux.Router) error { // type: boolean // default: false // description: Generate YAML for a Kubernetes service object. + // - in: query + // name: type + // type: string + // default: pod + // description: Generate YAML for the given Kubernetes kind. + // - in: query + // name: replicas + // type: integer + // format: int32 + // default: 0 + // description: Set the replica number for Deployment kind. // produces: // - text/vnd.yaml // - application/json diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go index daca8046fb..810aab2b34 100644 --- a/pkg/bindings/generate/generate.go +++ b/pkg/bindings/generate/generate.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "strconv" "github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/domain/entities" @@ -54,6 +55,9 @@ func Kube(ctx context.Context, nameOrIDs []string, options *KubeOptions) (*entit for _, name := range nameOrIDs { params.Add("names", name) } + if options.Replicas != nil { + params.Set("replicas", strconv.Itoa(int(*options.Replicas))) + } response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/generate/kube", params, nil) if err != nil { return nil, err diff --git a/pkg/bindings/generate/types.go b/pkg/bindings/generate/types.go index 235995ec0d..ec45c732dd 100644 --- a/pkg/bindings/generate/types.go +++ b/pkg/bindings/generate/types.go @@ -6,6 +6,10 @@ package generate type KubeOptions struct { // Service - generate YAML for a Kubernetes _service_ object. Service *bool + // Type - the k8s kind to be generated i.e Pod or Deployment + Type *string + // Replicas - the value to set in the replicas field for a Deployment + Replicas *int32 } // SystemdOptions are optional options for generating systemd files diff --git a/pkg/bindings/generate/types_kube_options.go b/pkg/bindings/generate/types_kube_options.go index 2a8b007b88..aba2d7ef98 100644 --- a/pkg/bindings/generate/types_kube_options.go +++ b/pkg/bindings/generate/types_kube_options.go @@ -31,3 +31,33 @@ func (o *KubeOptions) GetService() bool { } return *o.Service } + +// WithType set field Type to given value +func (o *KubeOptions) WithType(value string) *KubeOptions { + o.Type = &value + return o +} + +// GetType returns value of field Type +func (o *KubeOptions) GetType() string { + if o.Type == nil { + var z string + return z + } + return *o.Type +} + +// WithReplicas set field Replicas to given value +func (o *KubeOptions) WithReplicas(value int32) *KubeOptions { + o.Replicas = &value + return o +} + +// GetReplicas returns value of field Replicas +func (o *KubeOptions) GetReplicas() int32 { + if o.Replicas == nil { + var z int32 + return z + } + return *o.Replicas +} diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index 314996497a..a78f5d952e 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -31,6 +31,10 @@ type GenerateSystemdReport struct { type GenerateKubeOptions struct { // Service - generate YAML for a Kubernetes _service_ object. Service bool + // Type - the k8s kind to be generated i.e Pod or Deployment + Type string + // Replicas - the value to set in the replicas field for a Deployment + Replicas int32 } type KubeGenerateOptions = GenerateKubeOptions diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 1da3b21e9a..11b13433ad 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -103,13 +103,20 @@ func (ic *ContainerEngine) GenerateSpec(ctx context.Context, opts *entities.Gene func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { var ( - pods []*libpod.Pod - ctrs []*libpod.Container - vols []*libpod.Volume - podContent [][]byte - content [][]byte + pods []*libpod.Pod + ctrs []*libpod.Container + vols []*libpod.Volume + typeContent [][]byte + content [][]byte ) + if options.Replicas > 1 && options.Type != define.K8sKindDeployment { + return nil, fmt.Errorf("--replicas can only be set when --type is set to deployment") + } + if options.Replicas < 1 { + return nil, fmt.Errorf("--replicas has to be greater than or equal to 1. By default, --replicas is set to 1") + } + defaultKubeNS := true // Lookup for podman objects. for _, nameOrID := range nameOrIDs { @@ -187,12 +194,12 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, // Generate kube pods and services from pods. if len(pods) >= 1 { - pos, svcs, err := getKubePods(ctx, pods, options.Service) + out, svcs, err := getKubePods(ctx, pods, options) if err != nil { return nil, err } - podContent = append(podContent, pos...) + typeContent = append(typeContent, out...) if options.Service { content = append(content, svcs...) } @@ -212,12 +219,29 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, ` content = append(content, []byte(warning)) } - b, err := generateKubeYAML(libpod.ConvertV1PodToYAMLPod(po)) - if err != nil { - return nil, err + + // Create a pod or deployment kind depending on what Type was requested by the user + switch options.Type { + case define.K8sKindDeployment: + dep, err := libpod.GenerateForKubeDeployment(ctx, libpod.ConvertV1PodToYAMLPod(po), options) + if err != nil { + return nil, err + } + b, err := generateKubeYAML(dep) + 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 and deployments are currently supported") } - podContent = append(podContent, b) if options.Service { svc, err := libpod.GenerateKubeServiceFromV1Pod(po, []k8sAPI.ServicePort{}) if err != nil { @@ -231,8 +255,8 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, } } - // Content order is based on helm install order (secret, persistentVolumeClaim, service, pod). - content = append(content, podContent...) + // Content order is based on helm install order (secret, persistentVolumeClaim, service, pod/deployment). + content = append(content, typeContent...) // Generate kube YAML file from all kube kinds. k, err := generateKubeOutput(content) @@ -243,24 +267,39 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, return &entities.GenerateKubeReport{Reader: bytes.NewReader(k)}, nil } -// getKubePods returns kube pod and service YAML files from podman pods. -func getKubePods(ctx context.Context, pods []*libpod.Pod, getService bool) ([][]byte, [][]byte, error) { - pos := [][]byte{} +// getKubePods returns kube pod or deployment and service YAML files from podman pods. +func getKubePods(ctx context.Context, pods []*libpod.Pod, options entities.GenerateKubeOptions) ([][]byte, [][]byte, error) { + out := [][]byte{} svcs := [][]byte{} for _, p := range pods { - po, sp, err := p.GenerateForKube(ctx, getService) + po, sp, err := p.GenerateForKube(ctx, options.Service) if err != nil { return nil, nil, err } - b, err := generateKubeYAML(po) - if err != nil { - return nil, nil, err + switch options.Type { + case define.K8sKindDeployment: + dep, err := libpod.GenerateForKubeDeployment(ctx, libpod.ConvertV1PodToYAMLPod(po), options) + if err != nil { + return nil, nil, err + } + b, err := generateKubeYAML(dep) + 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 and deployments are currently supported") } - pos = append(pos, b) - if getService { + if options.Service { svc, err := libpod.GenerateKubeServiceFromV1Pod(po, sp) if err != nil { return nil, nil, err @@ -273,7 +312,7 @@ func getKubePods(ctx context.Context, pods []*libpod.Pod, getService bool) ([][] } } - return pos, svcs, nil + return out, svcs, nil } // getKubePVCs returns kube persistent volume claim YAML files from podman volumes. diff --git a/pkg/domain/infra/tunnel/kube.go b/pkg/domain/infra/tunnel/kube.go index d5c0fcb4d7..ca943374f1 100644 --- a/pkg/domain/infra/tunnel/kube.go +++ b/pkg/domain/infra/tunnel/kube.go @@ -46,7 +46,7 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, // // Note: Caller is responsible for closing returned Reader func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, opts entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { - options := new(generate.KubeOptions).WithService(opts.Service) + options := new(generate.KubeOptions).WithService(opts.Service).WithType(opts.Type).WithReplicas(opts.Replicas) return generate.Kube(ic.ClientCtx, nameOrIDs, options) } diff --git a/pkg/k8s.io/api/core/v1/types.go b/pkg/k8s.io/api/core/v1/types.go index 1f1b80a8b5..334b55445c 100644 --- a/pkg/k8s.io/api/core/v1/types.go +++ b/pkg/k8s.io/api/core/v1/types.go @@ -4738,3 +4738,220 @@ const ( // and an error will be returned by the exec plugin runner. AlwaysExecInteractiveMode ExecInteractiveMode = "Always" ) + +// +genclient +// +genclient:method=GetScale,verb=get,subresource=scale,result=k8s.io/api/autoscaling/v1.Scale +// +genclient:method=UpdateScale,verb=update,subresource=scale,input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale +// +genclient:method=ApplyScale,verb=apply,subresource=scale,input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Deployment enables declarative updates for Pods and ReplicaSets. +type Deployment struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Specification of the desired behavior of the Deployment. + // +optional + Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // Most recently observed status of the Deployment. + // +optional + Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// DeploymentSpec is the specification of the desired behavior of the Deployment. +type DeploymentSpec struct { + // Number of desired pods. This is a pointer to distinguish between explicit + // zero and not specified. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + + // Label selector for pods. Existing ReplicaSets whose pods are + // selected by this will be the ones affected by this deployment. + // It must match the pod template's labels. + Selector *metav1.LabelSelector `json:"selector" protobuf:"bytes,2,opt,name=selector"` + + // Template describes the pods that will be created. + // The only allowed template.spec.restartPolicy value is "Always". + Template PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` + + // The deployment strategy to use to replace existing pods with new ones. + // +optional + // +patchStrategy=retainKeys + Strategy DeploymentStrategy `json:"strategy,omitempty" patchStrategy:"retainKeys" protobuf:"bytes,4,opt,name=strategy"` + + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,5,opt,name=minReadySeconds"` + + // The number of old ReplicaSets to retain to allow rollback. + // This is a pointer to distinguish between explicit zero and not specified. + // Defaults to 10. + // +optional + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,6,opt,name=revisionHistoryLimit"` + + // Indicates that the deployment is paused. + // +optional + Paused bool `json:"paused,omitempty" protobuf:"varint,7,opt,name=paused"` + + // The maximum time in seconds for a deployment to make progress before it + // is considered to be failed. The deployment controller will continue to + // process failed deployments and a condition with a ProgressDeadlineExceeded + // reason will be surfaced in the deployment status. Note that progress will + // not be estimated during the time a deployment is paused. Defaults to 600s. + ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,9,opt,name=progressDeadlineSeconds"` +} + +const ( + // DefaultDeploymentUniqueLabelKey is the default key of the selector that is added + // to existing ReplicaSets (and label key that is added to its pods) to prevent the existing ReplicaSets + // to select new pods (and old pods being select by new ReplicaSet). + DefaultDeploymentUniqueLabelKey string = "pod-template-hash" +) + +// DeploymentStrategy describes how to replace existing pods with new ones. +type DeploymentStrategy struct { + // Type of deployment. Can be "Recreate" or "RollingUpdate". Default is RollingUpdate. + // +optional + Type DeploymentStrategyType `json:"type,omitempty" protobuf:"bytes,1,opt,name=type,casttype=DeploymentStrategyType"` + + // Rolling update config params. Present only if DeploymentStrategyType = + // RollingUpdate. + //--- + // TODO: Update this to follow our convention for oneOf, whatever we decide it + // to be. + // +optional + RollingUpdate *RollingUpdateDeployment `json:"rollingUpdate,omitempty" protobuf:"bytes,2,opt,name=rollingUpdate"` +} + +// +enum +type DeploymentStrategyType string + +const ( + // Kill all existing pods before creating new ones. + RecreateDeploymentStrategyType DeploymentStrategyType = "Recreate" + + // Replace the old ReplicaSets by new one using rolling update i.e gradually scale down the old ReplicaSets and scale up the new one. + RollingUpdateDeploymentStrategyType DeploymentStrategyType = "RollingUpdate" +) + +// Spec to control the desired behavior of rolling update. +type RollingUpdateDeployment struct { + // The maximum number of pods that can be unavailable during the update. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // Absolute number is calculated from percentage by rounding down. + // This can not be 0 if MaxSurge is 0. + // Defaults to 25%. + // Example: when this is set to 30%, the old ReplicaSet can be scaled down to 70% of desired pods + // immediately when the rolling update starts. Once new pods are ready, old ReplicaSet + // can be scaled down further, followed by scaling up the new ReplicaSet, ensuring + // that the total number of pods available at all times during the update is at + // least 70% of desired pods. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,1,opt,name=maxUnavailable"` + + // The maximum number of pods that can be scheduled above the desired number of + // pods. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // This can not be 0 if MaxUnavailable is 0. + // Absolute number is calculated from percentage by rounding up. + // Defaults to 25%. + // Example: when this is set to 30%, the new ReplicaSet can be scaled up immediately when + // the rolling update starts, such that the total number of old and new pods do not exceed + // 130% of desired pods. Once old pods have been killed, + // new ReplicaSet can be scaled up further, ensuring that total number of pods running + // at any time during the update is at most 130% of desired pods. + // +optional + MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty" protobuf:"bytes,2,opt,name=maxSurge"` +} + +// DeploymentStatus is the most recently observed status of the Deployment. +type DeploymentStatus struct { + // The generation observed by the deployment controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + + // Total number of non-terminated pods targeted by this deployment (their labels match the selector). + // +optional + Replicas int32 `json:"replicas,omitempty" protobuf:"varint,2,opt,name=replicas"` + + // Total number of non-terminated pods targeted by this deployment that have the desired template spec. + // +optional + UpdatedReplicas int32 `json:"updatedReplicas,omitempty" protobuf:"varint,3,opt,name=updatedReplicas"` + + // readyReplicas is the number of pods targeted by this Deployment with a Ready Condition. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,7,opt,name=readyReplicas"` + + // Total number of available pods (ready for at least minReadySeconds) targeted by this deployment. + // +optional + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"varint,4,opt,name=availableReplicas"` + + // Total number of unavailable pods targeted by this deployment. This is the total number of + // pods that are still required for the deployment to have 100% available capacity. They may + // either be pods that are running but not yet available or pods that still have not been created. + // +optional + UnavailableReplicas int32 `json:"unavailableReplicas,omitempty" protobuf:"varint,5,opt,name=unavailableReplicas"` + + // Represents the latest available observations of a deployment's current state. + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []DeploymentCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` + + // Count of hash collisions for the Deployment. The Deployment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int32 `json:"collisionCount,omitempty" protobuf:"varint,8,opt,name=collisionCount"` +} + +type DeploymentConditionType string + +// These are valid conditions of a deployment. +const ( + // Available means the deployment is available, ie. at least the minimum available + // replicas required are up and running for at least minReadySeconds. + DeploymentAvailable DeploymentConditionType = "Available" + // Progressing means the deployment is progressing. Progress for a deployment is + // considered when a new replica set is created or adopted, and when new pods scale + // up or old pods scale down. Progress is not estimated for paused deployments or + // when progressDeadlineSeconds is not specified. + DeploymentProgressing DeploymentConditionType = "Progressing" + // ReplicaFailure is added in a deployment when one of its pods fails to be created + // or deleted. + DeploymentReplicaFailure DeploymentConditionType = "ReplicaFailure" +) + +// DeploymentCondition describes the state of a deployment at a certain point. +type DeploymentCondition struct { + // Type of deployment condition. + Type DeploymentConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=DeploymentConditionType"` + // Status of the condition, one of True, False, Unknown. + Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,6,opt,name=lastUpdateTime"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,7,opt,name=lastTransitionTime"` + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"` + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DeploymentList is a list of Deployments. +type DeploymentList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is the list of Deployments. + Items []Deployment `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/test/apiv2/80-kube.at b/test/apiv2/80-kube.at index f40a6556ed..fb20642b3c 100644 --- a/test/apiv2/80-kube.at +++ b/test/apiv2/80-kube.at @@ -29,6 +29,12 @@ like "$output" ".*metadata:.*" "Check generated kube yaml(service=true) - metada like "$output" ".*spec:.*" "Check generated kube yaml(service=true) - spec" like "$output" ".*kind:\\sService.*" "Check generated kube yaml(service=true) - kind: Service" +t GET "libpod/generate/kube?type=deployment&names=$cid" 200 +like "$output" ".*apiVersion:.*" "Check generated kube yaml - apiVersion" +like "$output" ".*kind:\\sDeployment.*" "Check generated kube yaml - kind: Deployment" +like "$output" ".*metadata:.*" "Check generated kube yaml - metadata" +like "$output" ".*spec:.*" "Check generated kube yaml - spec" + TMPD=$(mktemp -d podman-apiv2-test-kube.XXXXXX) YAML="${TMPD}/kube.yaml" echo "$output" > $YAML diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index 5377bd5441..1b4f158bbb 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -531,7 +531,7 @@ var _ = Describe("Podman kube generate", func() { It("podman generate kube on pod with restartPolicy", func() { // podName, set, expect testSli := [][]string{ - {"testPod1", "", "Never"}, // some pod create from cmdline, so set it to Never + {"testPod1", "", ""}, // some pod create from cmdline, so set it to an empty string and let k8s default it to Always {"testPod2", "always", "Always"}, {"testPod3", "on-failure", "OnFailure"}, {"testPod4", "no", "Never"}, @@ -1402,4 +1402,87 @@ USER test1` Expect(inspect.OutputToString()).To(ContainSubstring("1231")) Expect(inspect.OutputToString()).To(ContainSubstring("3123")) }) + + It("podman generate kube on pod with --type=deployment", func() { + podName := "test-pod" + session := podmanTest.Podman([]string{"pod", "create", podName}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", podName, ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + session = podmanTest.Podman([]string{"create", "--pod", podName, ALPINE, "sleep", "100"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "--type", "deployment", podName}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + dep := new(v1.Deployment) + err := yaml.Unmarshal(kube.Out.Contents(), dep) + Expect(err).ToNot(HaveOccurred()) + Expect(dep.Name).To(Equal(podName + "-deployment")) + Expect(dep.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", podName)) + Expect(dep.Spec.Template.Name).To(Equal(podName)) + + numContainers := 0 + for range dep.Spec.Template.Spec.Containers { + numContainers++ + } + Expect(numContainers).To(Equal(2)) + }) + + It("podman generate kube on ctr with --type=deployment and --replicas=3", func() { + ctrName := "test-ctr" + session := podmanTest.Podman([]string{"create", "--name", ctrName, ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "--type", "deployment", "--replicas", "3", ctrName}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + dep := new(v1.Deployment) + err := yaml.Unmarshal(kube.Out.Contents(), dep) + Expect(err).ToNot(HaveOccurred()) + Expect(dep.Name).To(Equal(ctrName + "-pod-deployment")) + Expect(dep.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", ctrName+"-pod")) + Expect(dep.Spec.Template.Name).To(Equal(ctrName + "-pod")) + Expect(int(*dep.Spec.Replicas)).To(Equal(3)) + + numContainers := 0 + for range dep.Spec.Template.Spec.Containers { + numContainers++ + } + Expect(numContainers).To(Equal(1)) + }) + + It("podman generate kube on ctr with --type=pod and --replicas=3 should fail", func() { + ctrName := "test-ctr" + session := podmanTest.Podman([]string{"create", "--name", ctrName, ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "--type", "pod", "--replicas", "3", ctrName}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(125)) + }) + + It("podman generate kube on pod with --type=deployment and --restart=no should fail", func() { + // TODO: When we add --restart for pods, fix this test to reflect that + podName := "test-pod" + session := podmanTest.Podman([]string{"pod", "create", podName}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", podName, "--restart", "no", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "--type", "deployment", podName}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(125)) + }) }) diff --git a/test/minikube/001-kube.bats b/test/minikube/001-kube.bats index c8e037f644..e937a1b6b8 100755 --- a/test/minikube/001-kube.bats +++ b/test/minikube/001-kube.bats @@ -162,3 +162,44 @@ load helpers.bash run minikube kubectl delete namespace $project assert $status -eq 0 "delete namespace $project" } + +@test "minikube - deploy generated container yaml to minikube --type=deployment" { + cname="test-ctr" + fname="/tmp/minikube_deploy_$(random_string 6).yaml" + run_podman container create --name $cname $IMAGE top + run_podman kube generate --type deployment -f $fname $cname + + # deploy to the minikube cluster + project="dep-ctr-ns" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run minikube kubectl -- apply -f $fname + assert "$status" -eq 0 "deploy $fname to the cluster" + assert "$output" == "deployment.apps/$cname-pod-deployment created" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} + +@test "minikube - deploy generated pod yaml to minikube --type=deployment" { + pname="test-pod" + cname1="test-ctr1" + cname2="test-ctr2" + fname="/tmp/minikube_deploy_$(random_string 6).yaml" + + run_podman pod create --name $pname --publish 9999:8888 + run_podman container create --name $cname1 --pod $pname $IMAGE sleep 1000 + run_podman container create --name $cname2 --pod $pname $IMAGE sleep 2000 + run_podman kube generate --type deployment -f $fname $pname + + # deploy to the minikube cluster + project="dep-pod-ns" + run minikube kubectl create namespace $project + assert "$status" -eq 0 "create new namespace $project" + run minikube kubectl -- apply -f $fname + assert "$status" -eq 0 "deploy $fname to the cluster" + assert "$output" == "deployment.apps/$pname-deployment created" + wait_for_pods_to_start + run minikube kubectl delete namespace $project + assert $status -eq 0 "delete namespace $project" +} diff --git a/test/system/710-kube.bats b/test/system/710-kube.bats index 350d4cc77f..d5b3f9e89b 100644 --- a/test/system/710-kube.bats +++ b/test/system/710-kube.bats @@ -106,24 +106,23 @@ metadata.labels.app | = | ${pname} metadata.name | = | ${pname} spec.hostname | = | $pname -spec.restartPolicy | = | Never spec.containers[0].command | = | [\"top\"] spec.containers[0].image | = | $IMAGE spec.containers[0].name | = | $cname1 spec.containers[0].ports[0].containerPort | = | 8888 spec.containers[0].ports[0].hostPort | = | 9999 -spec.containers[0].resources | = | {} +spec.containers[0].resources | = | null spec.containers[1].command | = | [\"bottom\"] spec.containers[1].image | = | $IMAGE spec.containers[1].name | = | $cname2 spec.containers[1].ports | = | null -spec.containers[1].resources | = | {} +spec.containers[1].resources | = | null spec.containers[0].securityContext.capabilities | = | $capabilities -status | = | {} +status | = | null " while read key op expect; do