diff options
author | OpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com> | 2020-05-06 20:34:13 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-06 20:34:13 +0200 |
commit | 8b5df5b5d97facad5bb84fe4597bf7f0059c09fa (patch) | |
tree | 66296d6b47bc9627053bd0b3dadb07059cf711c6 | |
parent | 22bf906e8cc440cb3c33b9eed620ae91f447b269 (diff) | |
parent | f269be3a314a0903bb74a20de0e93b4f274531e6 (diff) | |
download | podman-8b5df5b5d97facad5bb84fe4597bf7f0059c09fa.tar.gz podman-8b5df5b5d97facad5bb84fe4597bf7f0059c09fa.tar.bz2 podman-8b5df5b5d97facad5bb84fe4597bf7f0059c09fa.zip |
Merge pull request #6092 from vrothberg/v2-kube
add {generate,play} kube
35 files changed, 1269 insertions, 110 deletions
diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 3681804ea..3e9772576 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -534,10 +534,13 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string case "label": // TODO selinux opts and label opts are the same thing s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + s.Annotations[define.InspectAnnotationLabel] = con[1] case "apparmor": s.ContainerSecurityConfig.ApparmorProfile = con[1] + s.Annotations[define.InspectAnnotationApparmor] = con[1] case "seccomp": s.SeccompProfilePath = con[1] + s.Annotations[define.InspectAnnotationSeccomp] = con[1] default: return fmt.Errorf("invalid --security-opt 2: %q", opt) } diff --git a/cmd/podman/common/types.go b/cmd/podman/common/types.go deleted file mode 100644 index 2427ae975..000000000 --- a/cmd/podman/common/types.go +++ /dev/null @@ -1,3 +0,0 @@ -package common - -var DefaultKernelNamespaces = "cgroup,ipc,net,uts" diff --git a/cmd/podman/generate/generate.go b/cmd/podman/generate/generate.go index b112e666a..7803c0c78 100644 --- a/cmd/podman/generate/generate.go +++ b/cmd/podman/generate/generate.go @@ -22,7 +22,7 @@ var ( func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode}, + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: generateCmd, }) } diff --git a/cmd/podman/generate/kube.go b/cmd/podman/generate/kube.go new file mode 100644 index 000000000..86a9cc686 --- /dev/null +++ b/cmd/podman/generate/kube.go @@ -0,0 +1,68 @@ +package pods + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + kubeOptions = entities.GenerateKubeOptions{} + kubeFile = "" + kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from a Podman container or pod. + +Whether the input is for a container or pod, Podman will always generate the specification as a pod.` + + kubeCmd = &cobra.Command{ + Use: "kube [flags] CONTAINER | POD", + Short: "Generate Kubernetes YAML from a container or pod.", + Long: kubeDescription, + RunE: kube, + Args: cobra.ExactArgs(1), + Example: `podman generate kube ctrID + podman generate kube podID + podman generate kube --service podID`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: kubeCmd, + Parent: generateCmd, + }) + flags := kubeCmd.Flags() + flags.BoolVarP(&kubeOptions.Service, "service", "s", false, "Generate YAML for a Kubernetes service object") + flags.StringVarP(&kubeFile, "filename", "f", "", "Write output to the specified path") + flags.SetNormalizeFunc(utils.AliasFlags) +} + +func kube(cmd *cobra.Command, args []string) error { + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args[0], kubeOptions) + if err != nil { + return err + } + + content, err := ioutil.ReadAll(report.Reader) + if err != nil { + return err + } + if cmd.Flags().Changed("filename") { + if _, err := os.Stat(kubeFile); err == nil { + return errors.Errorf("cannot write to %q", kubeFile) + } + if err := ioutil.WriteFile(kubeFile, content, 0644); err != nil { + return errors.Wrapf(err, "cannot write to %q", kubeFile) + } + return nil + } + + fmt.Println(string(content)) + return nil +} diff --git a/cmd/podman/generate/systemd.go b/cmd/podman/generate/systemd.go index 55d770249..20d9748d4 100644 --- a/cmd/podman/generate/systemd.go +++ b/cmd/podman/generate/systemd.go @@ -29,7 +29,7 @@ var ( func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Mode: []entities.EngineMode{entities.ABIMode}, Command: systemdCmd, Parent: generateCmd, }) diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 422dee90b..76ec7bc8e 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -10,6 +10,7 @@ import ( _ "github.com/containers/libpod/cmd/podman/images" _ "github.com/containers/libpod/cmd/podman/manifest" _ "github.com/containers/libpod/cmd/podman/networks" + _ "github.com/containers/libpod/cmd/podman/play" _ "github.com/containers/libpod/cmd/podman/pods" "github.com/containers/libpod/cmd/podman/registry" _ "github.com/containers/libpod/cmd/podman/system" diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go new file mode 100644 index 000000000..2499b54b9 --- /dev/null +++ b/cmd/podman/play/kube.go @@ -0,0 +1,101 @@ +package pods + +import ( + "fmt" + "os" + + "github.com/containers/common/pkg/auth" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// playKubeOptionsWrapper allows for separating CLI-only fields from API-only +// fields. +type playKubeOptionsWrapper struct { + entities.PlayKubeOptions + + TLSVerifyCLI bool +} + +var ( + // https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ + defaultSeccompRoot = "/var/lib/kubelet/seccomp" + kubeOptions = playKubeOptionsWrapper{} + kubeDescription = `Command reads in a structured file of Kubernetes YAML. + + It creates the pod and containers described in the YAML. The containers within the pod are then started and the ID of the new Pod is output.` + + kubeCmd = &cobra.Command{ + Use: "kube [flags] KUBEFILE", + Short: "Play a pod based on Kubernetes YAML.", + Long: kubeDescription, + RunE: kube, + Args: cobra.ExactArgs(1), + Example: `podman play kube nginx.yml + podman play kube --creds user:password --seccomp-profile-root /custom/path apache.yml`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: kubeCmd, + Parent: playCmd, + }) + + flags := kubeCmd.Flags() + flags.SetNormalizeFunc(utils.AliasFlags) + flags.StringVar(&kubeOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + flags.StringVar(&kubeOptions.Network, "network", "", "Connect pod to CNI network(s)") + flags.BoolVarP(&kubeOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") + if !registry.IsRemote() { + flags.StringVar(&kubeOptions.Authfile, "authfile", auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override") + flags.StringVar(&kubeOptions.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys") + flags.BoolVar(&kubeOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + flags.StringVar(&kubeOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") + flags.StringVar(&kubeOptions.SeccompProfileRoot, "seccomp-profile-root", defaultSeccompRoot, "Directory path for seccomp profiles") + } +} + +func kube(cmd *cobra.Command, args []string) error { + // TLS verification in c/image is controlled via a `types.OptionalBool` + // which allows for distinguishing among set-true, set-false, unspecified + // which is important to implement a sane way of dealing with defaults of + // boolean CLI flags. + if cmd.Flags().Changed("tls-verify") { + kubeOptions.SkipTLSVerify = types.NewOptionalBool(!kubeOptions.TLSVerifyCLI) + } + if kubeOptions.Authfile != "" { + if _, err := os.Stat(kubeOptions.Authfile); err != nil { + return errors.Wrapf(err, "error getting authfile %s", kubeOptions.Authfile) + } + } + + report, err := registry.ContainerEngine().PlayKube(registry.GetContext(), args[0], kubeOptions.PlayKubeOptions) + if err != nil { + return err + } + + for _, l := range report.Logs { + fmt.Fprintf(os.Stderr, l) + } + + fmt.Printf("Pod:\n%s\n", report.Pod) + switch len(report.Containers) { + case 0: + return nil + case 1: + fmt.Printf("Container:\n") + default: + fmt.Printf("Containers:\n") + } + for _, ctr := range report.Containers { + fmt.Println(ctr) + } + + return nil +} diff --git a/cmd/podman/play/play.go b/cmd/podman/play/play.go new file mode 100644 index 000000000..b151e5f5d --- /dev/null +++ b/cmd/podman/play/play.go @@ -0,0 +1,26 @@ +package pods + +import ( + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/cmd/podman/validate" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _play_ + playCmd = &cobra.Command{ + Use: "play", + Short: "Play a pod and its containers from a structured file.", + Long: "Play structured data (e.g., Kubernetes pod or service yaml) based on containers and pods.", + TraverseChildren: true, + RunE: validate.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: playCmd, + }) +} diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 85b96d37b..0a2016496 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -12,6 +12,7 @@ import ( "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/errorhandling" + createconfig "github.com/containers/libpod/pkg/spec" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" @@ -57,7 +58,7 @@ func init() { flags.StringVarP(&createOptions.Name, "name", "n", "", "Assign a name to the pod") flags.StringVarP(&createOptions.Hostname, "hostname", "", "", "Set a hostname to the pod") flags.StringVar(&podIDFile, "pod-id-file", "", "Write the pod ID to the file") - flags.StringVar(&share, "share", common.DefaultKernelNamespaces, "A comma delimited list of kernel namespaces the pod will share") + flags.StringVar(&share, "share", createconfig.DefaultKernelNamespaces, "A comma delimited list of kernel namespaces the pod will share") } func create(cmd *cobra.Command, args []string) error { diff --git a/libpod/container.go b/libpod/container.go index 5cd719ab6..d4a779b13 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -1221,5 +1221,5 @@ func (c *Container) AutoRemove() bool { if spec.Annotations == nil { return false } - return c.Spec().Annotations[InspectAnnotationAutoremove] == InspectResponseTrue + return c.Spec().Annotations[define.InspectAnnotationAutoremove] == define.InspectResponseTrue } diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 276429b11..ae28dde94 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -16,73 +16,6 @@ import ( "github.com/syndtr/gocapability/capability" ) -const ( - // InspectAnnotationCIDFile is used by Inspect to determine if a - // container ID file was created for the container. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationCIDFile = "io.podman.annotations.cid-file" - // InspectAnnotationAutoremove is used by Inspect to determine if a - // container will be automatically removed on exit. - // If an annotation with this key is found in the OCI spec and is one of - // the two supported boolean values (InspectResponseTrue and - // InspectResponseFalse) it will be used in the output of Inspect(). - InspectAnnotationAutoremove = "io.podman.annotations.autoremove" - // InspectAnnotationVolumesFrom is used by Inspect to identify - // containers whose volumes are are being used by this container. - // It is expected to be a comma-separated list of container names and/or - // IDs. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationVolumesFrom = "io.podman.annotations.volumes-from" - // InspectAnnotationPrivileged is used by Inspect to identify containers - // which are privileged (IE, running with elevated privileges). - // It is expected to be a boolean, populated by one of - // InspectResponseTrue or InspectResponseFalse. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationPrivileged = "io.podman.annotations.privileged" - // InspectAnnotationPublishAll is used by Inspect to identify containers - // which have all the ports from their image published. - // It is expected to be a boolean, populated by one of - // InspectResponseTrue or InspectResponseFalse. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationPublishAll = "io.podman.annotations.publish-all" - // InspectAnnotationInit is used by Inspect to identify containers that - // mount an init binary in. - // It is expected to be a boolean, populated by one of - // InspectResponseTrue or InspectResponseFalse. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationInit = "io.podman.annotations.init" - // InspectAnnotationLabel is used by Inspect to identify containers with - // special SELinux-related settings. It is used to populate the output - // of the SecurityOpt setting. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationLabel = "io.podman.annotations.label" - // InspectAnnotationSeccomp is used by Inspect to identify containers - // with special Seccomp-related settings. It is used to populate the - // output of the SecurityOpt setting in Inspect. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationSeccomp = "io.podman.annotations.seccomp" - // InspectAnnotationApparmor is used by Inspect to identify containers - // with special Apparmor-related settings. It is used to populate the - // output of the SecurityOpt setting. - // If an annotation with this key is found in the OCI spec, it will be - // used in the output of Inspect(). - InspectAnnotationApparmor = "io.podman.annotations.apparmor" - - // InspectResponseTrue is a boolean True response for an inspect - // annotation. - InspectResponseTrue = "TRUE" - // InspectResponseFalse is a boolean False response for an inspect - // annotation. - InspectResponseFalse = "FALSE" -) - // inspectLocked inspects a container for low-level information. // The caller must held c.lock. func (c *Container) inspectLocked(size bool) (*define.InspectContainerData, error) { @@ -452,26 +385,26 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named // Annotations if ctrSpec.Annotations != nil { - hostConfig.ContainerIDFile = ctrSpec.Annotations[InspectAnnotationCIDFile] - if ctrSpec.Annotations[InspectAnnotationAutoremove] == InspectResponseTrue { + hostConfig.ContainerIDFile = ctrSpec.Annotations[define.InspectAnnotationCIDFile] + if ctrSpec.Annotations[define.InspectAnnotationAutoremove] == define.InspectResponseTrue { hostConfig.AutoRemove = true } - if ctrs, ok := ctrSpec.Annotations[InspectAnnotationVolumesFrom]; ok { + if ctrs, ok := ctrSpec.Annotations[define.InspectAnnotationVolumesFrom]; ok { hostConfig.VolumesFrom = strings.Split(ctrs, ",") } - if ctrSpec.Annotations[InspectAnnotationPrivileged] == InspectResponseTrue { + if ctrSpec.Annotations[define.InspectAnnotationPrivileged] == define.InspectResponseTrue { hostConfig.Privileged = true } - if ctrSpec.Annotations[InspectAnnotationInit] == InspectResponseTrue { + if ctrSpec.Annotations[define.InspectAnnotationInit] == define.InspectResponseTrue { hostConfig.Init = true } - if label, ok := ctrSpec.Annotations[InspectAnnotationLabel]; ok { + if label, ok := ctrSpec.Annotations[define.InspectAnnotationLabel]; ok { hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("label=%s", label)) } - if seccomp, ok := ctrSpec.Annotations[InspectAnnotationSeccomp]; ok { + if seccomp, ok := ctrSpec.Annotations[define.InspectAnnotationSeccomp]; ok { hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp)) } - if apparmor, ok := ctrSpec.Annotations[InspectAnnotationApparmor]; ok { + if apparmor, ok := ctrSpec.Annotations[define.InspectAnnotationApparmor]; ok { hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor)) } } diff --git a/libpod/define/annotations.go b/libpod/define/annotations.go new file mode 100644 index 000000000..f6b1c06ea --- /dev/null +++ b/libpod/define/annotations.go @@ -0,0 +1,68 @@ +package define + +const ( + // InspectAnnotationCIDFile is used by Inspect to determine if a + // container ID file was created for the container. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationCIDFile = "io.podman.annotations.cid-file" + // InspectAnnotationAutoremove is used by Inspect to determine if a + // container will be automatically removed on exit. + // If an annotation with this key is found in the OCI spec and is one of + // the two supported boolean values (InspectResponseTrue and + // InspectResponseFalse) it will be used in the output of Inspect(). + InspectAnnotationAutoremove = "io.podman.annotations.autoremove" + // InspectAnnotationVolumesFrom is used by Inspect to identify + // containers whose volumes are are being used by this container. + // It is expected to be a comma-separated list of container names and/or + // IDs. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationVolumesFrom = "io.podman.annotations.volumes-from" + // InspectAnnotationPrivileged is used by Inspect to identify containers + // which are privileged (IE, running with elevated privileges). + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationPrivileged = "io.podman.annotations.privileged" + // InspectAnnotationPublishAll is used by Inspect to identify containers + // which have all the ports from their image published. + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationPublishAll = "io.podman.annotations.publish-all" + // InspectAnnotationInit is used by Inspect to identify containers that + // mount an init binary in. + // It is expected to be a boolean, populated by one of + // InspectResponseTrue or InspectResponseFalse. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationInit = "io.podman.annotations.init" + // InspectAnnotationLabel is used by Inspect to identify containers with + // special SELinux-related settings. It is used to populate the output + // of the SecurityOpt setting. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationLabel = "io.podman.annotations.label" + // InspectAnnotationSeccomp is used by Inspect to identify containers + // with special Seccomp-related settings. It is used to populate the + // output of the SecurityOpt setting in Inspect. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationSeccomp = "io.podman.annotations.seccomp" + // InspectAnnotationApparmor is used by Inspect to identify containers + // with special Apparmor-related settings. It is used to populate the + // output of the SecurityOpt setting. + // If an annotation with this key is found in the OCI spec, it will be + // used in the output of Inspect(). + InspectAnnotationApparmor = "io.podman.annotations.apparmor" + + // InspectResponseTrue is a boolean True response for an inspect + // annotation. + InspectResponseTrue = "TRUE" + // InspectResponseFalse is a boolean False response for an inspect + // annotation. + InspectResponseFalse = "FALSE" +) diff --git a/libpod/kube.go b/libpod/kube.go index 5511d303d..a3c5e912f 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -469,7 +469,7 @@ func generateKubeSecurityContext(c *Container) (*v1.SecurityContext, error) { } var selinuxOpts v1.SELinuxOptions - opts := strings.SplitN(c.config.Spec.Annotations[InspectAnnotationLabel], ":", 2) + opts := strings.SplitN(c.config.Spec.Annotations[define.InspectAnnotationLabel], ":", 2) if len(opts) == 2 { switch opts[0] { case "type": diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go new file mode 100644 index 000000000..23320d346 --- /dev/null +++ b/pkg/api/handlers/libpod/generate.go @@ -0,0 +1,38 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func GenerateKube(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Service bool `schema:"service"` + }{ + // Defaults would go here. + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + containerEngine := abi.ContainerEngine{Libpod: runtime} + options := entities.GenerateKubeOptions{Service: query.Service} + report, err := containerEngine.GenerateKube(r.Context(), utils.GetName(r), options) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating YAML")) + return + } + + utils.WriteResponse(w, http.StatusOK, report.Reader) +} diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go new file mode 100644 index 000000000..26e02bf4f --- /dev/null +++ b/pkg/api/handlers/libpod/play.go @@ -0,0 +1,64 @@ +package libpod + +import ( + "io" + "io/ioutil" + "net/http" + "os" + + "github.com/containers/image/v5/types" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func PlayKube(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Network string `schema:"reference"` + TLSVerify bool `schema:"tlsVerify"` + }{ + TLSVerify: true, + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + // Fetch the K8s YAML file from the body, and copy it to a temp file. + tmpfile, err := ioutil.TempFile("", "libpod-play-kube.yml") + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + defer os.Remove(tmpfile.Name()) + if _, err := io.Copy(tmpfile, r.Body); err != nil && err != io.EOF { + tmpfile.Close() + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to write archive to temporary file")) + return + } + if err := tmpfile.Close(); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error closing temporary file")) + return + } + + containerEngine := abi.ContainerEngine{Libpod: runtime} + options := entities.PlayKubeOptions{Network: query.Network, Quiet: true} + if _, found := r.URL.Query()["tlsVerify"]; found { + options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + } + + report, err := containerEngine.PlayKube(r.Context(), tmpfile.Name(), options) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error playing YAML file")) + return + } + + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index 0aceaf5f6..5d125417b 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -56,6 +56,13 @@ type swagLibpodImagesRemoveResponse struct { Body handlers.LibpodImagesRemoveReport } +// PlayKube response +// swagger:response DocsLibpodPlayKubeResponse +type swagLibpodPlayKubeResponse struct { + // in:body + Body entities.PlayKubeReport +} + // Delete response // swagger:response DocsImageDeleteResponse type swagImageDeleteResponse struct { diff --git a/pkg/api/server/register_generate.go b/pkg/api/server/register_generate.go new file mode 100644 index 000000000..391e60111 --- /dev/null +++ b/pkg/api/server/register_generate.go @@ -0,0 +1,41 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { + // swagger:operation GET /libpod/generate/{name:.*}/kube libpod libpodGenerateKube + // --- + // tags: + // - containers + // - pods + // summary: Play a Kubernetes YAML file. + // description: Create and run pods based on a Kubernetes YAML file (pod or service kind). + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: Name or ID of the container or pod. + // - in: query + // name: service + // type: boolean + // default: false + // description: Generate YAML for a Kubernetes service object. + // produces: + // - application/json + // responses: + // 200: + // description: no error + // schema: + // type: string + // format: binary + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) + return nil +} diff --git a/pkg/api/server/register_play.go b/pkg/api/server/register_play.go new file mode 100644 index 000000000..d04879c19 --- /dev/null +++ b/pkg/api/server/register_play.go @@ -0,0 +1,42 @@ +package server + +import ( + "net/http" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerPlayHandlers(r *mux.Router) error { + // swagger:operation POST /libpod/play/kube libpod libpodPlayKube + // --- + // tags: + // - containers + // - pods + // summary: Play a Kubernetes YAML file. + // description: Create and run pods based on a Kubernetes YAML file (pod or service kind). + // parameters: + // - in: query + // name: network + // type: string + // description: Connect the pod to this network. + // - in: query + // name: tlsVerify + // type: boolean + // default: true + // description: Require HTTPS and verify signatures when contating registries. + // - in: body + // name: request + // description: Kubernetes YAML file. + // schema: + // type: string + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsLibpodPlayKubeResponse" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/play/kube"), s.APIHandler(libpod.PlayKube)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index ce2d152e0..a6c5d8e1e 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -98,12 +98,14 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li server.registerDistributionHandlers, server.registerEventsHandlers, server.registerExecHandlers, + server.registerGenerateHandlers, server.registerHealthCheckHandlers, server.registerImagesHandlers, server.registerInfoHandlers, server.registerManifestHandlers, server.registerMonitorHandlers, server.registerPingHandlers, + server.registerPlayHandlers, server.registerPluginsHandlers, server.registerPodsHandlers, server.RegisterSwaggerHandlers, diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go index 2916754b8..d3177133f 100644 --- a/pkg/bindings/generate/generate.go +++ b/pkg/bindings/generate/generate.go @@ -1,4 +1,32 @@ package generate -func GenerateKube() {} -func GenerateSystemd() {} +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" +) + +func GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + params.Set("service", strconv.FormatBool(options.Service)) + + response, err := conn.DoRequest(nil, http.MethodGet, "/generate/%s/kube", params, nameOrID) + if err != nil { + return nil, err + } + + if response.StatusCode == http.StatusOK { + return &entities.GenerateKubeReport{Reader: response.Body}, nil + } + + // Unpack the error. + return nil, response.Process(nil) +} diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go index a6f03cad2..653558a3c 100644 --- a/pkg/bindings/play/play.go +++ b/pkg/bindings/play/play.go @@ -1,7 +1,43 @@ package play -import "github.com/containers/libpod/pkg/bindings" +import ( + "context" + "net/http" + "net/url" + "os" + "strconv" -func PlayKube() error { - return bindings.ErrNotImplemented + "github.com/containers/image/v5/types" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" +) + +func PlayKube(ctx context.Context, path string, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + var report entities.PlayKubeReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + params := url.Values{} + params.Set("network", options.Network) + if options.SkipTLSVerify != types.OptionalBoolUndefined { + params.Set("tlsVerify", strconv.FormatBool(options.SkipTLSVerify == types.OptionalBoolTrue)) + } + + response, err := conn.DoRequest(f, http.MethodPost, "/play/kube", params) + if err != nil { + return nil, err + } + if err := response.Process(&report); err != nil { + return nil, err + } + + return &report, nil } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 2e4e486b5..1bfac4514 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -43,6 +43,7 @@ type ContainerEngine interface { ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) Events(ctx context.Context, opts EventsOptions) error GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error) + GenerateKube(ctx context.Context, nameOrID string, opts GenerateKubeOptions) (*GenerateKubeReport, error) SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error) HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) @@ -50,6 +51,7 @@ type ContainerEngine interface { NetworkInspect(ctx context.Context, namesOrIds []string, options NetworkInspectOptions) ([]NetworkInspectReport, error) NetworkList(ctx context.Context, options NetworkListOptions) ([]*NetworkListReport, error) NetworkRm(ctx context.Context, namesOrIds []string, options NetworkRmOptions) ([]*NetworkRmReport, error) + PlayKube(ctx context.Context, path string, opts PlayKubeOptions) (*PlayKubeReport, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) PodInspect(ctx context.Context, options PodInspectOptions) (*PodInspectReport, error) diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index 6d65b52f8..edd217615 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -1,5 +1,7 @@ package entities +import "io" + // GenerateSystemdOptions control the generation of systemd unit files. type GenerateSystemdOptions struct { // Files - generate files instead of printing to stdout. @@ -20,3 +22,15 @@ type GenerateSystemdReport struct { // entire content. Output string } + +// GenerateKubeOptions control the generation of Kubernetes YAML files. +type GenerateKubeOptions struct { + // Service - generate YAML for a Kubernetes _service_ object. + Service bool +} + +// GenerateKubeReport +type GenerateKubeReport struct { + // Reader - the io.Reader to reader the generated YAML file. + Reader io.Reader +} diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go new file mode 100644 index 000000000..93864c23b --- /dev/null +++ b/pkg/domain/entities/play.go @@ -0,0 +1,36 @@ +package entities + +import "github.com/containers/image/v5/types" + +// PlayKubeOptions controls playing kube YAML files. +type PlayKubeOptions struct { + // Authfile - path to an authentication file. + Authfile string + // CertDir - to a directory containing TLS certifications and keys. + CertDir string + // Credentials - `username:password` for authentication against a + // container registry. + Credentials string + // Network - name of the CNI network to connect to. + Network string + // Quiet - suppress output when pulling images. + Quiet bool + // SignaturePolicy - path to a signature-policy file. + SignaturePolicy string + // SkipTLSVerify - skip https and certificate validation when + // contacting container registries. + SkipTLSVerify types.OptionalBool + // SeccompProfileRoot - path to a directory containing seccomp + // profiles. + SeccompProfileRoot string +} + +// PlayKubeReport contains the results of running play kube. +type PlayKubeReport struct { + // Pod - the ID of the created pod. + Pod string + // Containers - the IDs of the containers running in the created pod. + Containers []string + // Logs - non-fatal erros and log messages while processing. + Logs []string +} diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index f69ba560e..be5d452bd 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -1,14 +1,18 @@ package abi import ( + "bytes" "context" "fmt" "strings" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/systemd/generate" + "github.com/ghodss/yaml" "github.com/pkg/errors" + k8sAPI "k8s.io/api/core/v1" ) func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) { @@ -172,3 +176,84 @@ func generateServiceName(ctr *libpod.Container, pod *libpod.Pod, options entitie } return ctrName, fmt.Sprintf("%s-%s", kind, name) } + +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { + var ( + pod *libpod.Pod + podYAML *k8sAPI.Pod + err error + ctr *libpod.Container + servicePorts []k8sAPI.ServicePort + serviceYAML k8sAPI.Service + ) + // Get the container in question. + ctr, err = ic.Libpod.LookupContainer(nameOrID) + if err != nil { + pod, err = ic.Libpod.LookupPod(nameOrID) + if err != nil { + return nil, err + } + podYAML, servicePorts, err = pod.GenerateForKube() + } else { + if len(ctr.Dependencies()) > 0 { + return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") + } + podYAML, err = ctr.GenerateForKube() + } + if err != nil { + return nil, err + } + + if options.Service { + serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts) + } + + content, err := generateKubeOutput(podYAML, &serviceYAML) + if err != nil { + return nil, err + } + + return &entities.GenerateKubeReport{Reader: bytes.NewReader(content)}, nil +} + +func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byte, error) { + var ( + output []byte + marshalledPod []byte + marshalledService []byte + err error + ) + + marshalledPod, err = yaml.Marshal(podYAML) + if err != nil { + return nil, err + } + + if serviceYAML != nil { + marshalledService, err = yaml.Marshal(serviceYAML) + if err != nil { + return nil, err + } + } + + header := `# Generation of Kubernetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-%s +` + podmanVersion, err := define.GetVersion() + if err != nil { + return nil, err + } + + output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...) + output = append(output, marshalledPod...) + if serviceYAML != nil { + output = append(output, []byte("---\n")...) + output = append(output, marshalledService...) + } + + return output, nil +} diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go new file mode 100644 index 000000000..cd7eec7e6 --- /dev/null +++ b/pkg/domain/infra/abi/play.go @@ -0,0 +1,544 @@ +package abi + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/image/v5/types" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + ann "github.com/containers/libpod/pkg/annotations" + "github.com/containers/libpod/pkg/domain/entities" + envLib "github.com/containers/libpod/pkg/env" + ns "github.com/containers/libpod/pkg/namespaces" + createconfig "github.com/containers/libpod/pkg/spec" + "github.com/containers/libpod/pkg/specgen/generate" + "github.com/containers/libpod/pkg/util" + "github.com/containers/storage" + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/distribution/reference" + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" +) + +const ( + // https://kubernetes.io/docs/concepts/storage/volumes/#hostpath + kubeDirectoryPermission = 0755 + // https://kubernetes.io/docs/concepts/storage/volumes/#hostpath + kubeFilePermission = 0644 +) + +func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + var ( + containers []*libpod.Container + pod *libpod.Pod + podOptions []libpod.PodCreateOption + podYAML v1.Pod + registryCreds *types.DockerAuthConfig + writer io.Writer + report entities.PlayKubeReport + ) + + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(content, &podYAML); err != nil { + return nil, errors.Wrapf(err, "unable to read %q as YAML", path) + } + + if podYAML.Kind != "Pod" { + return nil, errors.Errorf("invalid YAML kind: %q. Pod is the only supported Kubernetes YAML kind", podYAML.Kind) + } + + // check for name collision between pod and container + podName := podYAML.ObjectMeta.Name + if podName == "" { + return nil, errors.Errorf("pod does not have a name") + } + for _, n := range podYAML.Spec.Containers { + if n.Name == podName { + report.Logs = append(report.Logs, + fmt.Sprintf("a container exists with the same name (%q) as the pod in your YAML file; changing pod name to %s_pod\n", podName, podName)) + podName = fmt.Sprintf("%s_pod", podName) + } + } + + podOptions = append(podOptions, libpod.WithInfraContainer()) + podOptions = append(podOptions, libpod.WithPodName(podName)) + // TODO for now we just used the default kernel namespaces; we need to add/subtract this from yaml + + hostname := podYAML.Spec.Hostname + if hostname == "" { + hostname = podName + } + podOptions = append(podOptions, libpod.WithPodHostname(hostname)) + + if podYAML.Spec.HostNetwork { + podOptions = append(podOptions, libpod.WithPodHostNetwork()) + } + + nsOptions, err := generate.GetNamespaceOptions(strings.Split(createconfig.DefaultKernelNamespaces, ",")) + if err != nil { + return nil, err + } + podOptions = append(podOptions, nsOptions...) + podPorts := getPodPorts(podYAML.Spec.Containers) + podOptions = append(podOptions, libpod.WithInfraContainerPorts(podPorts)) + + if options.Network != "" { + switch strings.ToLower(options.Network) { + case "bridge", "host": + return nil, errors.Errorf("invalid value passed to --network: bridge or host networking must be configured in YAML") + case "": + return nil, errors.Errorf("invalid value passed to --network: must provide a comma-separated list of CNI networks") + default: + // We'll assume this is a comma-separated list of CNI + // networks. + networks := strings.Split(options.Network, ",") + logrus.Debugf("Pod joining CNI networks: %v", networks) + podOptions = append(podOptions, libpod.WithPodNetworks(networks)) + } + } + + // Create the Pod + pod, err = ic.Libpod.NewPod(ctx, podOptions...) + if err != nil { + return nil, err + } + + podInfraID, err := pod.InfraContainerID() + if err != nil { + return nil, err + } + hasUserns := false + if podInfraID != "" { + podCtr, err := ic.Libpod.GetContainer(podInfraID) + if err != nil { + return nil, err + } + mappings, err := podCtr.IDMappings() + if err != nil { + return nil, err + } + hasUserns = len(mappings.UIDMap) > 0 + } + + namespaces := map[string]string{ + // Disabled during code review per mheon + //"pid": fmt.Sprintf("container:%s", podInfraID), + "net": fmt.Sprintf("container:%s", podInfraID), + "ipc": fmt.Sprintf("container:%s", podInfraID), + "uts": fmt.Sprintf("container:%s", podInfraID), + } + if hasUserns { + namespaces["user"] = fmt.Sprintf("container:%s", podInfraID) + } + if !options.Quiet { + writer = os.Stderr + } + + dockerRegistryOptions := image.DockerRegistryOptions{ + DockerRegistryCreds: registryCreds, + DockerCertPath: options.CertDir, + DockerInsecureSkipTLSVerify: options.SkipTLSVerify, + } + + // map from name to mount point + volumes := make(map[string]string) + for _, volume := range podYAML.Spec.Volumes { + hostPath := volume.VolumeSource.HostPath + if hostPath == nil { + return nil, errors.Errorf("HostPath is currently the only supported VolumeSource") + } + if hostPath.Type != nil { + switch *hostPath.Type { + case v1.HostPathDirectoryOrCreate: + if _, err := os.Stat(hostPath.Path); os.IsNotExist(err) { + if err := os.Mkdir(hostPath.Path, kubeDirectoryPermission); err != nil { + return nil, errors.Errorf("Error creating HostPath %s at %s", volume.Name, hostPath.Path) + } + } + // Label a newly created volume + if err := libpod.LabelVolumePath(hostPath.Path); err != nil { + return nil, errors.Wrapf(err, "Error giving %s a label", hostPath.Path) + } + case v1.HostPathFileOrCreate: + if _, err := os.Stat(hostPath.Path); os.IsNotExist(err) { + f, err := os.OpenFile(hostPath.Path, os.O_RDONLY|os.O_CREATE, kubeFilePermission) + if err != nil { + return nil, errors.Errorf("Error creating HostPath %s at %s", volume.Name, hostPath.Path) + } + if err := f.Close(); err != nil { + logrus.Warnf("Error in closing newly created HostPath file: %v", err) + } + } + // unconditionally label a newly created volume + if err := libpod.LabelVolumePath(hostPath.Path); err != nil { + return nil, errors.Wrapf(err, "Error giving %s a label", hostPath.Path) + } + case v1.HostPathDirectory: + case v1.HostPathFile: + case v1.HostPathUnset: + // do nothing here because we will verify the path exists in validateVolumeHostDir + break + default: + return nil, errors.Errorf("Directories are the only supported HostPath type") + } + } + + if err := parse.ValidateVolumeHostDir(hostPath.Path); err != nil { + return nil, errors.Wrapf(err, "Error in parsing HostPath in YAML") + } + volumes[volume.Name] = hostPath.Path + } + + seccompPaths, err := initializeSeccompPaths(podYAML.ObjectMeta.Annotations, options.SeccompProfileRoot) + if err != nil { + return nil, err + } + + for _, container := range podYAML.Spec.Containers { + pullPolicy := util.PullImageMissing + if len(container.ImagePullPolicy) > 0 { + pullPolicy, err = util.ValidatePullType(string(container.ImagePullPolicy)) + if err != nil { + return nil, err + } + } + named, err := reference.ParseNormalizedNamed(container.Image) + if err != nil { + return nil, err + } + // In kube, if the image is tagged with latest, it should always pull + if tagged, isTagged := named.(reference.NamedTagged); isTagged { + if tagged.Tag() == image.LatestTag { + pullPolicy = util.PullImageAlways + } + } + newImage, err := ic.Libpod.ImageRuntime().New(ctx, container.Image, options.SignaturePolicy, options.Authfile, writer, &dockerRegistryOptions, image.SigningOptions{}, nil, pullPolicy) + if err != nil { + return nil, err + } + conf, err := kubeContainerToCreateConfig(ctx, container, ic.Libpod, newImage, namespaces, volumes, pod.ID(), podInfraID, seccompPaths) + if err != nil { + return nil, err + } + ctr, err := createconfig.CreateContainerFromCreateConfig(ic.Libpod, conf, ctx, pod) + if err != nil { + return nil, err + } + containers = append(containers, ctr) + } + + // start the containers + for _, ctr := range containers { + if err := ctr.Start(ctx, true); err != nil { + // Making this a hard failure here to avoid a mess + // the other containers are in created status + return nil, err + } + } + + report.Pod = pod.ID() + for _, ctr := range containers { + report.Containers = append(report.Containers, ctr.ID()) + } + + return &report, nil +} + +// getPodPorts converts a slice of kube container descriptions to an +// array of ocicni portmapping descriptions usable in libpod +func getPodPorts(containers []v1.Container) []ocicni.PortMapping { + var infraPorts []ocicni.PortMapping + for _, container := range containers { + for _, p := range container.Ports { + if p.HostPort != 0 && p.ContainerPort == 0 { + p.ContainerPort = p.HostPort + } + if p.Protocol == "" { + p.Protocol = "tcp" + } + portBinding := ocicni.PortMapping{ + HostPort: p.HostPort, + ContainerPort: p.ContainerPort, + Protocol: strings.ToLower(string(p.Protocol)), + } + if p.HostIP != "" { + logrus.Debug("HostIP on port bindings is not supported") + } + // only hostPort is utilized in podman context, all container ports + // are accessible inside the shared network namespace + if p.HostPort != 0 { + infraPorts = append(infraPorts, portBinding) + } + + } + } + return infraPorts +} + +func setupSecurityContext(securityConfig *createconfig.SecurityConfig, userConfig *createconfig.UserConfig, containerYAML v1.Container) { + if containerYAML.SecurityContext == nil { + return + } + if containerYAML.SecurityContext.ReadOnlyRootFilesystem != nil { + securityConfig.ReadOnlyRootfs = *containerYAML.SecurityContext.ReadOnlyRootFilesystem + } + if containerYAML.SecurityContext.Privileged != nil { + securityConfig.Privileged = *containerYAML.SecurityContext.Privileged + } + + if containerYAML.SecurityContext.AllowPrivilegeEscalation != nil { + securityConfig.NoNewPrivs = !*containerYAML.SecurityContext.AllowPrivilegeEscalation + } + + if seopt := containerYAML.SecurityContext.SELinuxOptions; seopt != nil { + if seopt.User != "" { + securityConfig.SecurityOpts = append(securityConfig.SecurityOpts, fmt.Sprintf("label=user:%s", seopt.User)) + securityConfig.LabelOpts = append(securityConfig.LabelOpts, fmt.Sprintf("user:%s", seopt.User)) + } + if seopt.Role != "" { + securityConfig.SecurityOpts = append(securityConfig.SecurityOpts, fmt.Sprintf("label=role:%s", seopt.Role)) + securityConfig.LabelOpts = append(securityConfig.LabelOpts, fmt.Sprintf("role:%s", seopt.Role)) + } + if seopt.Type != "" { + securityConfig.SecurityOpts = append(securityConfig.SecurityOpts, fmt.Sprintf("label=type:%s", seopt.Type)) + securityConfig.LabelOpts = append(securityConfig.LabelOpts, fmt.Sprintf("type:%s", seopt.Type)) + } + if seopt.Level != "" { + securityConfig.SecurityOpts = append(securityConfig.SecurityOpts, fmt.Sprintf("label=level:%s", seopt.Level)) + securityConfig.LabelOpts = append(securityConfig.LabelOpts, fmt.Sprintf("level:%s", seopt.Level)) + } + } + if caps := containerYAML.SecurityContext.Capabilities; caps != nil { + for _, capability := range caps.Add { + securityConfig.CapAdd = append(securityConfig.CapAdd, string(capability)) + } + for _, capability := range caps.Drop { + securityConfig.CapDrop = append(securityConfig.CapDrop, string(capability)) + } + } + if containerYAML.SecurityContext.RunAsUser != nil { + userConfig.User = fmt.Sprintf("%d", *containerYAML.SecurityContext.RunAsUser) + } + if containerYAML.SecurityContext.RunAsGroup != nil { + if userConfig.User == "" { + userConfig.User = "0" + } + userConfig.User = fmt.Sprintf("%s:%d", userConfig.User, *containerYAML.SecurityContext.RunAsGroup) + } +} + +// kubeContainerToCreateConfig takes a v1.Container and returns a createconfig describing a container +func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container, runtime *libpod.Runtime, newImage *image.Image, namespaces map[string]string, volumes map[string]string, podID, infraID string, seccompPaths *kubeSeccompPaths) (*createconfig.CreateConfig, error) { + var ( + containerConfig createconfig.CreateConfig + pidConfig createconfig.PidConfig + networkConfig createconfig.NetworkConfig + cgroupConfig createconfig.CgroupConfig + utsConfig createconfig.UtsConfig + ipcConfig createconfig.IpcConfig + userConfig createconfig.UserConfig + securityConfig createconfig.SecurityConfig + ) + + // The default for MemorySwappiness is -1, not 0 + containerConfig.Resources.MemorySwappiness = -1 + + containerConfig.Image = containerYAML.Image + containerConfig.ImageID = newImage.ID() + containerConfig.Name = containerYAML.Name + containerConfig.Tty = containerYAML.TTY + + containerConfig.Pod = podID + + imageData, _ := newImage.Inspect(ctx) + + userConfig.User = "0" + if imageData != nil { + userConfig.User = imageData.Config.User + } + + setupSecurityContext(&securityConfig, &userConfig, containerYAML) + + securityConfig.SeccompProfilePath = seccompPaths.findForContainer(containerConfig.Name) + + containerConfig.Command = []string{} + if imageData != nil && imageData.Config != nil { + containerConfig.Command = append(containerConfig.Command, imageData.Config.Entrypoint...) + } + if len(containerYAML.Command) != 0 { + containerConfig.Command = append(containerConfig.Command, containerYAML.Command...) + } else if imageData != nil && imageData.Config != nil { + containerConfig.Command = append(containerConfig.Command, imageData.Config.Cmd...) + } + if imageData != nil && len(containerConfig.Command) == 0 { + return nil, errors.Errorf("No command specified in container YAML or as CMD or ENTRYPOINT in this image for %s", containerConfig.Name) + } + + containerConfig.UserCommand = containerConfig.Command + + containerConfig.StopSignal = 15 + + containerConfig.WorkDir = "/" + if imageData != nil { + // FIXME, + // we are currently ignoring imageData.Config.ExposedPorts + containerConfig.BuiltinImgVolumes = imageData.Config.Volumes + if imageData.Config.WorkingDir != "" { + containerConfig.WorkDir = imageData.Config.WorkingDir + } + containerConfig.Labels = imageData.Config.Labels + if imageData.Config.StopSignal != "" { + stopSignal, err := util.ParseSignal(imageData.Config.StopSignal) + if err != nil { + return nil, err + } + containerConfig.StopSignal = stopSignal + } + } + + if containerYAML.WorkingDir != "" { + containerConfig.WorkDir = containerYAML.WorkingDir + } + // If the user does not pass in ID mappings, just set to basics + if userConfig.IDMappings == nil { + userConfig.IDMappings = &storage.IDMappingOptions{} + } + + networkConfig.NetMode = ns.NetworkMode(namespaces["net"]) + ipcConfig.IpcMode = ns.IpcMode(namespaces["ipc"]) + utsConfig.UtsMode = ns.UTSMode(namespaces["uts"]) + // disabled in code review per mheon + //containerConfig.PidMode = ns.PidMode(namespaces["pid"]) + userConfig.UsernsMode = ns.UsernsMode(namespaces["user"]) + if len(containerConfig.WorkDir) == 0 { + containerConfig.WorkDir = "/" + } + + containerConfig.Pid = pidConfig + containerConfig.Network = networkConfig + containerConfig.Uts = utsConfig + containerConfig.Ipc = ipcConfig + containerConfig.Cgroup = cgroupConfig + containerConfig.User = userConfig + containerConfig.Security = securityConfig + + annotations := make(map[string]string) + if infraID != "" { + annotations[ann.SandboxID] = infraID + annotations[ann.ContainerType] = ann.ContainerTypeContainer + } + containerConfig.Annotations = annotations + + // Environment Variables + envs := map[string]string{} + if imageData != nil { + imageEnv, err := envLib.ParseSlice(imageData.Config.Env) + if err != nil { + return nil, errors.Wrap(err, "error parsing image environment variables") + } + envs = imageEnv + } + for _, e := range containerYAML.Env { + envs[e.Name] = e.Value + } + containerConfig.Env = envs + + for _, volume := range containerYAML.VolumeMounts { + hostPath, exists := volumes[volume.Name] + if !exists { + return nil, errors.Errorf("Volume mount %s specified for container but not configured in volumes", volume.Name) + } + if err := parse.ValidateVolumeCtrDir(volume.MountPath); err != nil { + return nil, errors.Wrapf(err, "error in parsing MountPath") + } + containerConfig.Volumes = append(containerConfig.Volumes, fmt.Sprintf("%s:%s", hostPath, volume.MountPath)) + } + return &containerConfig, nil +} + +// kubeSeccompPaths holds information about a pod YAML's seccomp configuration +// it holds both container and pod seccomp paths +type kubeSeccompPaths struct { + containerPaths map[string]string + podPath string +} + +// findForContainer checks whether a container has a seccomp path configured for it +// if not, it returns the podPath, which should always have a value +func (k *kubeSeccompPaths) findForContainer(ctrName string) string { + if path, ok := k.containerPaths[ctrName]; ok { + return path + } + return k.podPath +} + +// initializeSeccompPaths takes annotations from the pod object metadata and finds annotations pertaining to seccomp +// it parses both pod and container level +// if the annotation is of the form "localhost/%s", the seccomp profile will be set to profileRoot/%s +func initializeSeccompPaths(annotations map[string]string, profileRoot string) (*kubeSeccompPaths, error) { + seccompPaths := &kubeSeccompPaths{containerPaths: make(map[string]string)} + var err error + if annotations != nil { + for annKeyValue, seccomp := range annotations { + // check if it is prefaced with container.seccomp.security.alpha.kubernetes.io/ + prefixAndCtr := strings.Split(annKeyValue, "/") + if prefixAndCtr[0]+"/" != v1.SeccompContainerAnnotationKeyPrefix { + continue + } else if len(prefixAndCtr) != 2 { + // this could be caused by a user inputting either of + // container.seccomp.security.alpha.kubernetes.io{,/} + // both of which are invalid + return nil, errors.Errorf("Invalid seccomp path: %s", prefixAndCtr[0]) + } + + path, err := verifySeccompPath(seccomp, profileRoot) + if err != nil { + return nil, err + } + seccompPaths.containerPaths[prefixAndCtr[1]] = path + } + + podSeccomp, ok := annotations[v1.SeccompPodAnnotationKey] + if ok { + seccompPaths.podPath, err = verifySeccompPath(podSeccomp, profileRoot) + } else { + seccompPaths.podPath, err = libpod.DefaultSeccompPath() + } + if err != nil { + return nil, err + } + } + return seccompPaths, nil +} + +// verifySeccompPath takes a path and checks whether it is a default, unconfined, or a path +// the available options are parsed as defined in https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp +func verifySeccompPath(path string, profileRoot string) (string, error) { + switch path { + case v1.DeprecatedSeccompProfileDockerDefault: + fallthrough + case v1.SeccompProfileRuntimeDefault: + return libpod.DefaultSeccompPath() + case "unconfined": + return path, nil + default: + parts := strings.Split(path, "/") + if parts[0] == "localhost" { + return filepath.Join(profileRoot, parts[1]), nil + } + return "", errors.Errorf("invalid seccomp path: %s", path) + } +} diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index 3cd483053..eb5587f89 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -3,6 +3,7 @@ package tunnel import ( "context" + "github.com/containers/libpod/pkg/bindings/generate" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" ) @@ -10,3 +11,7 @@ import ( func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) { return nil, errors.New("not implemented for tunnel") } + +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { + return generate.GenerateKube(ic.ClientCxt, nameOrID, options) +} diff --git a/pkg/domain/infra/tunnel/play.go b/pkg/domain/infra/tunnel/play.go new file mode 100644 index 000000000..15383a703 --- /dev/null +++ b/pkg/domain/infra/tunnel/play.go @@ -0,0 +1,12 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/play" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + return play.PlayKube(ic.ClientCxt, path, options) +} diff --git a/pkg/spec/namespaces.go b/pkg/spec/namespaces.go index aebc90f68..40364b054 100644 --- a/pkg/spec/namespaces.go +++ b/pkg/spec/namespaces.go @@ -17,6 +17,10 @@ import ( "github.com/sirupsen/logrus" ) +// DefaultKernelNamespaces is a comma-separated list of default kernel +// namespaces. +const DefaultKernelNamespaces = "cgroup,ipc,net,uts" + // ToCreateOptions converts the input to a slice of container create options. func (c *NetworkConfig) ToCreateOptions(runtime *libpod.Runtime, userns *UserConfig) ([]libpod.CtrCreateOption, error) { var portBindings []ocicni.PortMapping @@ -154,9 +158,9 @@ func (c *NetworkConfig) ConfigureGenerator(g *generate.Generator) error { } if c.PublishAll { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue + g.Config.Annotations[define.InspectAnnotationPublishAll] = define.InspectResponseTrue } else { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse + g.Config.Annotations[define.InspectAnnotationPublishAll] = define.InspectResponseFalse } return nil diff --git a/pkg/spec/security.go b/pkg/spec/security.go index 0f8d36f00..6d74e97e6 100644 --- a/pkg/spec/security.go +++ b/pkg/spec/security.go @@ -6,6 +6,7 @@ import ( "github.com/containers/common/pkg/capabilities" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/util" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -184,11 +185,11 @@ func (c *SecurityConfig) ConfigureGenerator(g *generate.Generator, user *UserCon } switch splitOpt[0] { case "label": - configSpec.Annotations[libpod.InspectAnnotationLabel] = splitOpt[1] + configSpec.Annotations[define.InspectAnnotationLabel] = splitOpt[1] case "seccomp": - configSpec.Annotations[libpod.InspectAnnotationSeccomp] = splitOpt[1] + configSpec.Annotations[define.InspectAnnotationSeccomp] = splitOpt[1] case "apparmor": - configSpec.Annotations[libpod.InspectAnnotationApparmor] = splitOpt[1] + configSpec.Annotations[define.InspectAnnotationApparmor] = splitOpt[1] } } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 41ed5f1f0..77e92ae29 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -7,6 +7,7 @@ import ( cconfig "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/sysinfo" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/rootless" @@ -436,29 +437,29 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } if config.CidFile != "" { - configSpec.Annotations[libpod.InspectAnnotationCIDFile] = config.CidFile + configSpec.Annotations[define.InspectAnnotationCIDFile] = config.CidFile } if config.Rm { - configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseTrue + configSpec.Annotations[define.InspectAnnotationAutoremove] = define.InspectResponseTrue } else { - configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseFalse + configSpec.Annotations[define.InspectAnnotationAutoremove] = define.InspectResponseFalse } if len(config.VolumesFrom) > 0 { - configSpec.Annotations[libpod.InspectAnnotationVolumesFrom] = strings.Join(config.VolumesFrom, ",") + configSpec.Annotations[define.InspectAnnotationVolumesFrom] = strings.Join(config.VolumesFrom, ",") } if config.Security.Privileged { - configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseTrue + configSpec.Annotations[define.InspectAnnotationPrivileged] = define.InspectResponseTrue } else { - configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseFalse + configSpec.Annotations[define.InspectAnnotationPrivileged] = define.InspectResponseFalse } if config.Init { - configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseTrue + configSpec.Annotations[define.InspectAnnotationInit] = define.InspectResponseTrue } else { - configSpec.Annotations[libpod.InspectAnnotationInit] = libpod.InspectResponseFalse + configSpec.Annotations[define.InspectAnnotationInit] = define.InspectResponseFalse } return configSpec, nil diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 96c65b551..138d9e0cd 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -438,9 +438,9 @@ func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt g.Config.Annotations = make(map[string]string) } if s.PublishExposedPorts { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue + g.Config.Annotations[define.InspectAnnotationPublishAll] = define.InspectResponseTrue } else { - g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseFalse + g.Config.Annotations[define.InspectAnnotationPublishAll] = define.InspectResponseFalse } return nil diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 8136c0993..a2bb66a44 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -6,6 +6,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/specgen" @@ -327,19 +328,19 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt //} if s.Remove { - configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseTrue + configSpec.Annotations[define.InspectAnnotationAutoremove] = define.InspectResponseTrue } else { - configSpec.Annotations[libpod.InspectAnnotationAutoremove] = libpod.InspectResponseFalse + configSpec.Annotations[define.InspectAnnotationAutoremove] = define.InspectResponseFalse } if len(s.VolumesFrom) > 0 { - configSpec.Annotations[libpod.InspectAnnotationVolumesFrom] = strings.Join(s.VolumesFrom, ",") + configSpec.Annotations[define.InspectAnnotationVolumesFrom] = strings.Join(s.VolumesFrom, ",") } if s.Privileged { - configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseTrue + configSpec.Annotations[define.InspectAnnotationPrivileged] = define.InspectResponseTrue } else { - configSpec.Annotations[libpod.InspectAnnotationPrivileged] = libpod.InspectResponseFalse + configSpec.Annotations[define.InspectAnnotationPrivileged] = define.InspectResponseFalse } // TODO Init might not make it into the specgen and therefore is not available here. We should deal diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index e4f487634..389f2c822 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -21,7 +21,6 @@ var _ = Describe("Podman generate kube", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 16f7af55e..9daf266b8 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -217,7 +217,6 @@ var _ = Describe("Podman generate kube", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) |