diff options
-rw-r--r-- | cmd/podman/play/kube.go | 3 | ||||
-rw-r--r-- | docs/source/markdown/podman-play-kube.1.md | 34 | ||||
-rw-r--r-- | pkg/domain/entities/play.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/play.go | 131 | ||||
-rw-r--r-- | test/e2e/common_test.go | 15 | ||||
-rw-r--r-- | test/e2e/play_build_test.go | 243 | ||||
-rw-r--r-- | test/e2e/play_kube_test.go | 15 |
7 files changed, 401 insertions, 42 deletions
diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go index ece7d1f98..2eebd9f86 100644 --- a/cmd/podman/play/kube.go +++ b/cmd/podman/play/kube.go @@ -100,6 +100,9 @@ func init() { configmapFlagName := "configmap" flags.StringSliceVar(&kubeOptions.ConfigMaps, configmapFlagName, []string{}, "`Pathname` of a YAML file containing a kubernetes configmap") _ = kubeCmd.RegisterFlagCompletionFunc(configmapFlagName, completion.AutocompleteDefault) + + buildFlagName := "build" + flags.BoolVar(&kubeOptions.Build, buildFlagName, false, "Build all images in a YAML (given Containerfiles exist)") } _ = flags.MarkHidden("signature-policy") } diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index ad5ae7e4c..268e4bbcb 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -35,6 +35,36 @@ A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the Pe - volume.podman.io/gid - volume.podman.io/mount-options +Play kube is capable of building images on the fly given the correct directory layout and Containerfiles. This +option is not available for remote clients yet. Consider the following excerpt from a YAML file: +``` +apiVersion: v1 +kind: Pod +metadata: +... +spec: + containers: + - command: + - top + - name: container + value: podman + image: foobar +... +``` + +If there is a directory named `foobar` in the current working directory with a file named `Containerfile` or `Dockerfile`, +Podman play kube will build that image and name it `foobar`. An example directory structure for this example would look +like: +``` +|- mykubefiles + |- myplayfile.yaml + |- foobar + |- Containerfile +``` + +The build will consider `foobar` to be the context directory for the build. If there is an image in local storage +called `foobar`, the image will not be built unless the `--build` flag is used. + ## OPTIONS #### **--authfile**=*path* @@ -45,6 +75,10 @@ If the authorization state is not found there, $HOME/.docker/config.json is chec Note: You can also override the default path of the authentication file by setting the REGISTRY\_AUTH\_FILE environment variable. `export REGISTRY_AUTH_FILE=path` +#### **--build** + +Build images even if they are found in the local storage. + #### **--cert-dir**=*path* Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index 89dfc08e9..01de73ebe 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -10,6 +10,8 @@ import ( type PlayKubeOptions struct { // Authfile - path to an authentication file. Authfile string + // Indicator to build all images with Containerfile or Dockerfile + Build bool // CertDir - to a directory containing TLS certifications and keys. CertDir string // Username for authenticating against the registry. diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index d257bad18..6224feff5 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -7,9 +7,11 @@ import ( "io" "io/ioutil" "os" + "path/filepath" "strconv" "strings" + buildahDefine "github.com/containers/buildah/define" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" "github.com/containers/image/v5/types" @@ -266,39 +268,69 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } containers := make([]*libpod.Container, 0, len(podYAML.Spec.Containers)) + cwd, err := os.Getwd() + if err != nil { + return nil, err + } for _, container := range podYAML.Spec.Containers { // Contains all labels obtained from kube labels := make(map[string]string) - - // NOTE: set the pull policy to "newer". This will cover cases - // where the "latest" tag requires a pull and will also - // transparently handle "localhost/" prefixed files which *may* - // refer to a locally built image OR an image running a - // registry on localhost. - pullPolicy := config.PullPolicyNewer - if len(container.ImagePullPolicy) > 0 { - // Make sure to lower the strings since K8s pull policy - // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). - rawPolicy := string(container.ImagePullPolicy) - pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) - if err != nil { - return nil, err - } + var pulledImage *libimage.Image + buildFile, err := getBuildFile(container.Image, cwd) + if err != nil { + return nil, err } - // This ensures the image is the image store - pullOptions := &libimage.PullOptions{} - pullOptions.AuthFilePath = options.Authfile - pullOptions.CertDirPath = options.CertDir - pullOptions.SignaturePolicyPath = options.SignaturePolicy - pullOptions.Writer = writer - pullOptions.Username = options.Username - pullOptions.Password = options.Password - pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify - - pulledImages, err := ic.Libpod.LibimageRuntime().Pull(ctx, container.Image, pullPolicy, pullOptions) + existsLocally, err := ic.Libpod.LibimageRuntime().Exists(container.Image) if err != nil { return nil, err } + if (len(buildFile) > 0 && !existsLocally) || (len(buildFile) > 0 && options.Build) { + buildOpts := new(buildahDefine.BuildOptions) + commonOpts := new(buildahDefine.CommonBuildOptions) + buildOpts.ConfigureNetwork = buildahDefine.NetworkDefault + buildOpts.Isolation = buildahDefine.IsolationChroot + buildOpts.CommonBuildOpts = commonOpts + buildOpts.Output = container.Image + if _, _, err := ic.Libpod.Build(ctx, *buildOpts, []string{buildFile}...); err != nil { + return nil, err + } + i, _, err := ic.Libpod.LibimageRuntime().LookupImage(container.Image, new(libimage.LookupImageOptions)) + if err != nil { + return nil, err + } + pulledImage = i + } else { + // NOTE: set the pull policy to "newer". This will cover cases + // where the "latest" tag requires a pull and will also + // transparently handle "localhost/" prefixed files which *may* + // refer to a locally built image OR an image running a + // registry on localhost. + pullPolicy := config.PullPolicyNewer + if len(container.ImagePullPolicy) > 0 { + // Make sure to lower the strings since K8s pull policy + // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). + rawPolicy := string(container.ImagePullPolicy) + pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) + if err != nil { + return nil, err + } + } + // This ensures the image is the image store + pullOptions := &libimage.PullOptions{} + pullOptions.AuthFilePath = options.Authfile + pullOptions.CertDirPath = options.CertDir + pullOptions.SignaturePolicyPath = options.SignaturePolicy + pullOptions.Writer = writer + pullOptions.Username = options.Username + pullOptions.Password = options.Password + pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify + + pulledImages, err := ic.Libpod.LibimageRuntime().Pull(ctx, container.Image, pullPolicy, pullOptions) + if err != nil { + return nil, err + } + pulledImage = pulledImages[0] + } // Handle kube annotations for k, v := range annotations { @@ -318,7 +350,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY specgenOpts := kube.CtrSpecGenOptions{ Container: container, - Image: pulledImages[0], + Image: pulledImage, Volumes: volumes, PodID: pod.ID(), PodName: podName, @@ -509,3 +541,48 @@ func sortKubeKinds(documentList [][]byte) ([][]byte, error) { return sortedDocumentList, nil } +func imageNamePrefix(imageName string) string { + prefix := imageName + s := strings.Split(prefix, ":") + if len(s) > 0 { + prefix = s[0] + } + s = strings.Split(prefix, "/") + if len(s) > 0 { + prefix = s[len(s)-1] + } + s = strings.Split(prefix, "@") + if len(s) > 0 { + prefix = s[0] + } + return prefix +} + +func getBuildFile(imageName string, cwd string) (string, error) { + buildDirName := imageNamePrefix(imageName) + containerfilePath := filepath.Join(cwd, buildDirName, "Containerfile") + dockerfilePath := filepath.Join(cwd, buildDirName, "Dockerfile") + + _, err := os.Stat(filepath.Join(containerfilePath)) + if err == nil { + logrus.Debugf("building %s with %s", imageName, containerfilePath) + return containerfilePath, nil + } + // If the error is not because the file does not exist, take + // a mulligan and try Dockerfile. If that also fails, return that + // error + if err != nil && !os.IsNotExist(err) { + logrus.Errorf("%v: unable to check for %s", err, containerfilePath) + } + + _, err = os.Stat(filepath.Join(dockerfilePath)) + if err == nil { + logrus.Debugf("building %s with %s", imageName, dockerfilePath) + return dockerfilePath, nil + } + // Strike two + if os.IsNotExist(err) { + return "", nil + } + return "", err +} diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 2e48e1763..eb4bd102b 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -841,3 +841,18 @@ func (p *PodmanTestIntegration) buildImage(dockerfile, imageName string, layers output := session.OutputToStringArray() return output[len(output)-1] } + +func writeYaml(content string, fileName string) error { + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(content) + if err != nil { + return err + } + + return nil +} diff --git a/test/e2e/play_build_test.go b/test/e2e/play_build_test.go new file mode 100644 index 000000000..16f2687f3 --- /dev/null +++ b/test/e2e/play_build_test.go @@ -0,0 +1,243 @@ +// +build !remote + +// build for play kube is not supported on remote yet. + +package integration + +import ( + "os" + "path/filepath" + + . "github.com/containers/podman/v3/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman play kube with build", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + podmanTest.SeedImages() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + var testYAML = ` +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2021-08-05T17:55:51Z" + labels: + app: foobar + name: top_pod +spec: + containers: + - command: + - top + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + image: foobar + name: foobar + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + tty: true + workingDir: / + dnsConfig: {} +status: {} +` + + var playBuildFile = ` +FROM quay.io/libpod/alpine_nginx:latest +RUN apk update && apk add strace +LABEL homer=dad +` + var prebuiltImage = ` +FROM quay.io/libpod/alpine_nginx:latest +RUN apk update && apk add strace +LABEL marge=mom +` + It("Check that image is built using Dockerfile", func() { + // Setup + yamlDir := filepath.Join(tempdir, RandomString(12)) + err := os.Mkdir(yamlDir, 0755) + err = writeYaml(testYAML, filepath.Join(yamlDir, "top.yaml")) + Expect(err).To(BeNil()) + app1Dir := filepath.Join(yamlDir, "foobar") + err = os.Mkdir(app1Dir, 0755) + Expect(err).To(BeNil()) + err = writeYaml(playBuildFile, filepath.Join(app1Dir, "Dockerfile")) + Expect(err).To(BeNil()) + + // Switch to temp dir and restore it afterwards + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + Expect(os.Chdir(yamlDir)).To(BeNil()) + defer func() { (Expect(os.Chdir(cwd)).To(BeNil())) }() + + session := podmanTest.Podman([]string{"play", "kube", "top.yaml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + exists := podmanTest.Podman([]string{"image", "exists", "foobar"}) + exists.WaitWithDefaultTimeout() + Expect(exists).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"container", "inspect", "top_pod-foobar"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + inspectData := inspect.InspectContainerToJSON() + Expect(len(inspectData)).To(BeNumerically(">", 0)) + Expect(inspectData[0].Config.Labels["homer"]).To(Equal("dad")) + }) + + It("Check that image is built using Containerfile", func() { + // Setup + yamlDir := filepath.Join(tempdir, RandomString(12)) + err := os.Mkdir(yamlDir, 0755) + err = writeYaml(testYAML, filepath.Join(yamlDir, "top.yaml")) + Expect(err).To(BeNil()) + app1Dir := filepath.Join(yamlDir, "foobar") + err = os.Mkdir(app1Dir, 0755) + Expect(err).To(BeNil()) + err = writeYaml(playBuildFile, filepath.Join(app1Dir, "Containerfile")) + Expect(err).To(BeNil()) + + // Switch to temp dir and restore it afterwards + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + Expect(os.Chdir(yamlDir)).To(BeNil()) + defer func() { (Expect(os.Chdir(cwd)).To(BeNil())) }() + + session := podmanTest.Podman([]string{"play", "kube", "top.yaml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + exists := podmanTest.Podman([]string{"image", "exists", "foobar"}) + exists.WaitWithDefaultTimeout() + Expect(exists).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"container", "inspect", "top_pod-foobar"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + inspectData := inspect.InspectContainerToJSON() + Expect(len(inspectData)).To(BeNumerically(">", 0)) + Expect(inspectData[0].Config.Labels["homer"]).To(Equal("dad")) + }) + + It("Do not build image if already in the local store", func() { + // Setup + yamlDir := filepath.Join(tempdir, RandomString(12)) + err := os.Mkdir(yamlDir, 0755) + err = writeYaml(testYAML, filepath.Join(yamlDir, "top.yaml")) + Expect(err).To(BeNil()) + + // build an image called foobar but make sure it doesnt have + // the same label as the yaml buildfile, so we can check that + // the image is NOT rebuilt. + err = writeYaml(prebuiltImage, filepath.Join(yamlDir, "Containerfile")) + Expect(err).To(BeNil()) + + app1Dir := filepath.Join(yamlDir, "foobar") + err = os.Mkdir(app1Dir, 0755) + Expect(err).To(BeNil()) + err = writeYaml(playBuildFile, filepath.Join(app1Dir, "Containerfile")) + Expect(err).To(BeNil()) + + // Switch to temp dir and restore it afterwards + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + Expect(os.Chdir(yamlDir)).To(BeNil()) + defer func() { (Expect(os.Chdir(cwd)).To(BeNil())) }() + + // Build the image into the local store + build := podmanTest.Podman([]string{"build", "-t", "foobar", "-f", "Containerfile"}) + build.WaitWithDefaultTimeout() + Expect(build).Should(Exit(0)) + + session := podmanTest.Podman([]string{"play", "kube", "top.yaml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"container", "inspect", "top_pod-foobar"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + inspectData := inspect.InspectContainerToJSON() + Expect(len(inspectData)).To(BeNumerically(">", 0)) + Expect(inspectData[0].Config.Labels["homer"]).To(Equal("")) + Expect(inspectData[0].Config.Labels["marge"]).To(Equal("mom")) + }) + + It("--build should override image in store", func() { + // Setup + yamlDir := filepath.Join(tempdir, RandomString(12)) + err := os.Mkdir(yamlDir, 0755) + err = writeYaml(testYAML, filepath.Join(yamlDir, "top.yaml")) + Expect(err).To(BeNil()) + + // build an image called foobar but make sure it doesnt have + // the same label as the yaml buildfile, so we can check that + // the image is NOT rebuilt. + err = writeYaml(prebuiltImage, filepath.Join(yamlDir, "Containerfile")) + Expect(err).To(BeNil()) + + app1Dir := filepath.Join(yamlDir, "foobar") + err = os.Mkdir(app1Dir, 0755) + Expect(err).To(BeNil()) + err = writeYaml(playBuildFile, filepath.Join(app1Dir, "Containerfile")) + Expect(err).To(BeNil()) + + // Switch to temp dir and restore it afterwards + cwd, err := os.Getwd() + Expect(err).To(BeNil()) + Expect(os.Chdir(yamlDir)).To(BeNil()) + defer func() { (Expect(os.Chdir(cwd)).To(BeNil())) }() + + // Build the image into the local store + build := podmanTest.Podman([]string{"build", "-t", "foobar", "-f", "Containerfile"}) + build.WaitWithDefaultTimeout() + Expect(build).Should(Exit(0)) + + session := podmanTest.Podman([]string{"play", "kube", "--build", "top.yaml"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"container", "inspect", "top_pod-foobar"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + inspectData := inspect.InspectContainerToJSON() + Expect(len(inspectData)).To(BeNumerically(">", 0)) + Expect(inspectData[0].Config.Labels["homer"]).To(Equal("dad")) + Expect(inspectData[0].Config.Labels["marge"]).To(Equal("")) + }) + +}) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index e3096d932..eec4b43a5 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -512,21 +512,6 @@ var ( defaultSecret = []byte(`{"FOO":"Zm9v","BAR":"YmFy"}`) ) -func writeYaml(content string, fileName string) error { - f, err := os.Create(fileName) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(content) - if err != nil { - return err - } - - return nil -} - // getKubeYaml returns a kubernetes YAML document. func getKubeYaml(kind string, object interface{}) (string, error) { var yamlTemplate string |