github.com/containers/podman/v4@v4.9.4/test/e2e/push_test.go (about) 1 package integration 2 3 import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "strings" 9 10 . "github.com/containers/podman/v4/test/utils" 11 "github.com/containers/storage/pkg/archive" 12 . "github.com/onsi/ginkgo/v2" 13 . "github.com/onsi/gomega" 14 . "github.com/onsi/gomega/gexec" 15 ) 16 17 var _ = Describe("Podman push", func() { 18 19 BeforeEach(func() { 20 podmanTest.AddImageToRWStore(ALPINE) 21 }) 22 23 It("podman push to containers/storage", func() { 24 SkipIfRemote("Remote push does not support containers-storage transport") 25 session := podmanTest.Podman([]string{"push", "-q", ALPINE, "containers-storage:busybox:test"}) 26 session.WaitWithDefaultTimeout() 27 Expect(session).Should(ExitCleanly()) 28 29 session = podmanTest.Podman([]string{"rmi", ALPINE}) 30 session.WaitWithDefaultTimeout() 31 Expect(session).Should(ExitCleanly()) 32 }) 33 34 It("podman push to dir", func() { 35 SkipIfRemote("Remote push does not support dir transport") 36 bbdir := filepath.Join(podmanTest.TempDir, "busybox") 37 session := podmanTest.Podman([]string{"push", "-q", "--remove-signatures", ALPINE, 38 fmt.Sprintf("dir:%s", bbdir)}) 39 session.WaitWithDefaultTimeout() 40 Expect(session).Should(ExitCleanly()) 41 42 bbdir = filepath.Join(podmanTest.TempDir, "busybox") 43 session = podmanTest.Podman([]string{"push", "-q", "--format", "oci", ALPINE, 44 fmt.Sprintf("dir:%s", bbdir)}) 45 session.WaitWithDefaultTimeout() 46 Expect(session).Should(ExitCleanly()) 47 }) 48 49 It("podman push to oci with compression-format and compression-level", func() { 50 SkipIfRemote("Remote push does not support dir transport") 51 bbdir := filepath.Join(podmanTest.TempDir, "busybox-oci") 52 53 // Invalid compression format specified, it must fail 54 session := podmanTest.Podman([]string{"push", "-q", "--compression-format=gzip", "--compression-level=40", ALPINE, fmt.Sprintf("oci:%s", bbdir)}) 55 session.WaitWithDefaultTimeout() 56 Expect(session).Should(Exit(125)) 57 output := session.ErrorToString() 58 Expect(output).To(ContainSubstring("invalid compression level")) 59 60 session = podmanTest.Podman([]string{"push", "-q", "--compression-format=zstd", "--remove-signatures", ALPINE, 61 fmt.Sprintf("oci:%s", bbdir)}) 62 session.WaitWithDefaultTimeout() 63 Expect(session).Should(ExitCleanly()) 64 65 foundZstdFile := false 66 67 blobsDir := filepath.Join(bbdir, "blobs/sha256") 68 69 blobs, err := os.ReadDir(blobsDir) 70 Expect(err).ToNot(HaveOccurred()) 71 72 for _, f := range blobs { 73 blobPath := filepath.Join(blobsDir, f.Name()) 74 75 sourceFile, err := os.ReadFile(blobPath) 76 Expect(err).ToNot(HaveOccurred()) 77 78 compressionType := archive.DetectCompression(sourceFile) 79 if compressionType == archive.Zstd { 80 foundZstdFile = true 81 break 82 } 83 } 84 Expect(foundZstdFile).To(BeTrue(), "found zstd file") 85 }) 86 87 It("push test --force-compression", func() { 88 if podmanTest.Host.Arch == "ppc64le" { 89 Skip("No registry image for ppc64le") 90 } 91 if isRootless() { 92 err := podmanTest.RestoreArtifact(REGISTRY_IMAGE) 93 Expect(err).ToNot(HaveOccurred()) 94 } 95 lock := GetPortLock("5000") 96 defer lock.Unlock() 97 session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", "5000:5000", REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"}) 98 session.WaitWithDefaultTimeout() 99 Expect(session).Should(ExitCleanly()) 100 101 if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { 102 Skip("Cannot start docker registry.") 103 } 104 105 session = podmanTest.Podman([]string{"build", "-t", "imageone", "build/basicalpine"}) 106 session.WaitWithDefaultTimeout() 107 Expect(session).Should(ExitCleanly()) 108 109 push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", "imageone", "localhost:5000/image"}) 110 push.WaitWithDefaultTimeout() 111 Expect(push).Should(ExitCleanly()) 112 113 skopeoInspect := []string{"inspect", "--tls-verify=false", "--raw", "docker://localhost:5000/image:latest"} 114 skopeo := SystemExec("skopeo", skopeoInspect) 115 skopeo.WaitWithDefaultTimeout() 116 Expect(skopeo).Should(ExitCleanly()) 117 output := skopeo.OutputToString() 118 // Default compression is gzip and push with `--force-compression=false` no traces of `zstd` should be there. 119 Expect(output).ToNot(ContainSubstring("zstd")) 120 121 push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--force-compression=false", "--compression-format", "zstd", "--remove-signatures", "imageone", "localhost:5000/image"}) 122 push.WaitWithDefaultTimeout() 123 Expect(push).Should(ExitCleanly()) 124 125 skopeo = SystemExec("skopeo", skopeoInspect) 126 skopeo.WaitWithDefaultTimeout() 127 Expect(skopeo).Should(ExitCleanly()) 128 output = skopeo.OutputToString() 129 // Although `--compression-format` is `zstd` but still no traces of `zstd` should be in image 130 // since blobs must be reused from last `gzip` image. 131 Expect(output).ToNot(ContainSubstring("zstd")) 132 133 push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--compression-format", "zstd", "--force-compression", "--remove-signatures", "imageone", "localhost:5000/image"}) 134 push.WaitWithDefaultTimeout() 135 Expect(push).Should(ExitCleanly()) 136 137 skopeo = SystemExec("skopeo", skopeoInspect) 138 skopeo.WaitWithDefaultTimeout() 139 Expect(skopeo).Should(ExitCleanly()) 140 output = skopeo.OutputToString() 141 // Should contain `zstd` layer, substring `zstd` is enough to confirm in skopeo inspect output that `zstd` layer is present. 142 Expect(output).To(ContainSubstring("zstd")) 143 }) 144 145 It("podman push to local registry", func() { 146 if podmanTest.Host.Arch == "ppc64le" { 147 Skip("No registry image for ppc64le") 148 } 149 if isRootless() { 150 err := podmanTest.RestoreArtifact(REGISTRY_IMAGE) 151 Expect(err).ToNot(HaveOccurred()) 152 } 153 lock := GetPortLock("5000") 154 defer lock.Unlock() 155 session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", "5000:5000", REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"}) 156 session.WaitWithDefaultTimeout() 157 Expect(session).Should(ExitCleanly()) 158 159 if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) { 160 Skip("Cannot start docker registry.") 161 } 162 163 push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) 164 push.WaitWithDefaultTimeout() 165 Expect(push).Should(ExitCleanly()) 166 167 push = podmanTest.Podman([]string{"push", "--compression-format=gzip", "--compression-level=1", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) 168 push.WaitWithDefaultTimeout() 169 Expect(push).Should(Exit(0)) 170 output := push.ErrorToString() 171 Expect(output).To(ContainSubstring("Copying blob ")) 172 Expect(output).To(ContainSubstring("Copying config ")) 173 Expect(output).To(ContainSubstring("Writing manifest to image destination")) 174 175 bitSize := 1024 176 keyFileName := filepath.Join(podmanTest.TempDir, "key") 177 publicKeyFileName, _, err := WriteRSAKeyPair(keyFileName, bitSize) 178 Expect(err).ToNot(HaveOccurred()) 179 180 if !IsRemote() { // Remote does not support --encryption-key 181 push = podmanTest.Podman([]string{"push", "-q", "--encryption-key", "jwe:" + publicKeyFileName, "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) 182 push.WaitWithDefaultTimeout() 183 Expect(push).Should(ExitCleanly()) 184 } 185 186 // Test --digestfile option 187 digestFile := filepath.Join(podmanTest.TempDir, "digestfile.txt") 188 push2 := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--digestfile=" + digestFile, "--remove-signatures", ALPINE, "localhost:5000/my-alpine"}) 189 push2.WaitWithDefaultTimeout() 190 fi, err := os.Lstat(digestFile) 191 Expect(err).ToNot(HaveOccurred()) 192 Expect(fi.Name()).To(Equal("digestfile.txt")) 193 Expect(push2).Should(ExitCleanly()) 194 195 if !IsRemote() { // Remote does not support signing 196 By("pushing and pulling with --sign-by-sigstore-private-key") 197 // Ideally, this should set SystemContext.RegistriesDirPath, but Podman currently doesn’t 198 // expose that as an option. So, for now, modify /etc/directly, and skip testing sigstore if 199 // we don’t have permission to do so. 200 systemRegistriesDAddition := "/etc/containers/registries.d/podman-test-only-temporary-addition.yaml" 201 cmd := exec.Command("cp", "testdata/sigstore-registries.d-fragment.yaml", systemRegistriesDAddition) 202 output, err := cmd.CombinedOutput() 203 if err != nil { 204 GinkgoWriter.Printf("Skipping sigstore tests because /etc/containers/registries.d isn’t writable: %s\n", string(output)) 205 } else { 206 defer func() { 207 err := os.Remove(systemRegistriesDAddition) 208 Expect(err).ToNot(HaveOccurred()) 209 }() 210 // Generate a signature verification policy file 211 policyPath := generatePolicyFile(podmanTest.TempDir) 212 defer os.Remove(policyPath) 213 214 // Verify that the policy rejects unsigned images 215 push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/sigstore-signed"}) 216 push.WaitWithDefaultTimeout() 217 Expect(push).Should(ExitCleanly()) 218 219 pull := podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5000/sigstore-signed"}) 220 pull.WaitWithDefaultTimeout() 221 Expect(pull).To(ExitWithError()) 222 Expect(pull.ErrorToString()).To(ContainSubstring("A signature was required, but no signature exists")) 223 224 // Sign an image, and verify it is accepted. 225 push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", "--sign-by-sigstore-private-key", "testdata/sigstore-key.key", "--sign-passphrase-file", "testdata/sigstore-key.key.pass", ALPINE, "localhost:5000/sigstore-signed"}) 226 push.WaitWithDefaultTimeout() 227 Expect(push).Should(ExitCleanly()) 228 229 pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5000/sigstore-signed"}) 230 pull.WaitWithDefaultTimeout() 231 Expect(pull).Should(ExitCleanly()) 232 233 By("pushing and pulling with --sign-by-sigstore") 234 // Verify that the policy rejects unsigned images 235 push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/sigstore-signed-params"}) 236 push.WaitWithDefaultTimeout() 237 Expect(push).Should(ExitCleanly()) 238 239 pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5000/sigstore-signed-params"}) 240 pull.WaitWithDefaultTimeout() 241 Expect(pull).To(ExitWithError()) 242 Expect(pull.ErrorToString()).To(ContainSubstring("A signature was required, but no signature exists")) 243 244 // Sign an image, and verify it is accepted. 245 push = podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", "--sign-by-sigstore", "testdata/sigstore-signing-params.yaml", ALPINE, "localhost:5000/sigstore-signed-params"}) 246 push.WaitWithDefaultTimeout() 247 Expect(push).Should(ExitCleanly()) 248 249 pull = podmanTest.Podman([]string{"pull", "-q", "--tls-verify=false", "--signature-policy", policyPath, "localhost:5000/sigstore-signed-params"}) 250 pull.WaitWithDefaultTimeout() 251 Expect(pull).Should(ExitCleanly()) 252 } 253 } 254 }) 255 256 It("podman push from local storage with nothing-allowed signature policy", func() { 257 SkipIfRemote("Remote push does not support dir transport") 258 denyAllPolicy := filepath.Join(INTEGRATION_ROOT, "test/deny.json") 259 260 inspect := podmanTest.Podman([]string{"inspect", "--format={{.ID}}", ALPINE}) 261 inspect.WaitWithDefaultTimeout() 262 Expect(inspect).Should(ExitCleanly()) 263 imageID := inspect.OutputToString() 264 265 // FIXME FIXME 266 push := podmanTest.Podman([]string{"push", "--signature-policy", denyAllPolicy, "-q", imageID, "dir:" + filepath.Join(podmanTest.TempDir, imageID)}) 267 push.WaitWithDefaultTimeout() 268 Expect(push).Should(ExitCleanly()) 269 }) 270 271 It("podman push to local registry with authorization", func() { 272 SkipIfRootless("/etc/containers/certs.d not writable") 273 if podmanTest.Host.Arch == "ppc64le" { 274 Skip("No registry image for ppc64le") 275 } 276 authPath := filepath.Join(podmanTest.TempDir, "auth") 277 err = os.Mkdir(authPath, os.ModePerm) 278 Expect(err).ToNot(HaveOccurred()) 279 err = os.MkdirAll("/etc/containers/certs.d/localhost:5000", os.ModePerm) 280 Expect(err).ToNot(HaveOccurred()) 281 defer os.RemoveAll("/etc/containers/certs.d/localhost:5000") 282 283 cwd, _ := os.Getwd() 284 certPath := filepath.Join(cwd, "../", "certs") 285 286 lock := GetPortLock("5000") 287 defer lock.Unlock() 288 htpasswd := SystemExec("htpasswd", []string{"-Bbn", "podmantest", "test"}) 289 htpasswd.WaitWithDefaultTimeout() 290 Expect(htpasswd).Should(ExitCleanly()) 291 292 f, err := os.Create(filepath.Join(authPath, "htpasswd")) 293 Expect(err).ToNot(HaveOccurred()) 294 defer f.Close() 295 296 _, err = f.WriteString(htpasswd.OutputToString()) 297 Expect(err).ToNot(HaveOccurred()) 298 err = f.Sync() 299 Expect(err).ToNot(HaveOccurred()) 300 301 session := podmanTest.Podman([]string{"run", "-d", "-p", "5000:5000", "--name", "registry", "-v", 302 strings.Join([]string{authPath, "/auth", "z"}, ":"), "-e", "REGISTRY_AUTH=htpasswd", "-e", 303 "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", "-e", "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", 304 "-v", strings.Join([]string{certPath, "/certs", "z"}, ":"), "-e", "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt", 305 "-e", "REGISTRY_HTTP_TLS_KEY=/certs/domain.key", REGISTRY_IMAGE}) 306 session.WaitWithDefaultTimeout() 307 Expect(session).Should(ExitCleanly()) 308 309 Expect(WaitContainerReady(podmanTest, "registry", "listening on", 20, 1)).To(BeTrue(), "registry container ready") 310 311 push := podmanTest.Podman([]string{"push", "--tls-verify=true", "--format=v2s2", "--creds=podmantest:test", ALPINE, "localhost:5000/tlstest"}) 312 push.WaitWithDefaultTimeout() 313 Expect(push).To(ExitWithError()) 314 Expect(push.ErrorToString()).To(ContainSubstring("x509: certificate signed by unknown authority")) 315 316 push = podmanTest.Podman([]string{"push", "--creds=podmantest:test", "--tls-verify=false", ALPINE, "localhost:5000/tlstest"}) 317 push.WaitWithDefaultTimeout() 318 Expect(push).Should(Exit(0)) 319 Expect(push.ErrorToString()).To(ContainSubstring("Writing manifest to image destination")) 320 321 setup := SystemExec("cp", []string{filepath.Join(certPath, "domain.crt"), "/etc/containers/certs.d/localhost:5000/ca.crt"}) 322 Expect(setup).Should(ExitCleanly()) 323 324 push = podmanTest.Podman([]string{"push", "--creds=podmantest:wrongpasswd", ALPINE, "localhost:5000/credstest"}) 325 push.WaitWithDefaultTimeout() 326 Expect(push).To(ExitWithError()) 327 Expect(push.ErrorToString()).To(ContainSubstring("/credstest: authentication required")) 328 329 if !IsRemote() { 330 // remote does not support --cert-dir 331 push = podmanTest.Podman([]string{"push", "--tls-verify=true", "--creds=podmantest:test", "--cert-dir=fakedir", ALPINE, "localhost:5000/certdirtest"}) 332 push.WaitWithDefaultTimeout() 333 Expect(push).To(ExitWithError()) 334 Expect(push.ErrorToString()).To(ContainSubstring("x509: certificate signed by unknown authority")) 335 } 336 337 push = podmanTest.Podman([]string{"push", "--creds=podmantest:test", ALPINE, "localhost:5000/defaultflags"}) 338 push.WaitWithDefaultTimeout() 339 Expect(push).Should(Exit(0)) 340 Expect(push.ErrorToString()).To(ContainSubstring("Writing manifest to image destination")) 341 342 // create and push manifest 343 session = podmanTest.Podman([]string{"manifest", "create", "localhost:5000/manifesttest"}) 344 session.WaitWithDefaultTimeout() 345 Expect(session).Should(ExitCleanly()) 346 347 session = podmanTest.Podman([]string{"manifest", "push", "--creds=podmantest:test", "--tls-verify=false", "--all", "localhost:5000/manifesttest"}) 348 session.WaitWithDefaultTimeout() 349 Expect(session).Should(Exit(0)) 350 Expect(session.ErrorToString()).To(ContainSubstring("Writing manifest list to image destination")) 351 }) 352 353 It("podman push and encrypt to oci", func() { 354 SkipIfRemote("Remote push neither supports oci transport, nor encryption") 355 356 bbdir := filepath.Join(podmanTest.TempDir, "busybox-oci") 357 358 bitSize := 1024 359 keyFileName := filepath.Join(podmanTest.TempDir, "key") 360 publicKeyFileName, _, err := WriteRSAKeyPair(keyFileName, bitSize) 361 Expect(err).ToNot(HaveOccurred()) 362 363 session := podmanTest.Podman([]string{"push", "-q", "--encryption-key", "jwe:" + publicKeyFileName, ALPINE, fmt.Sprintf("oci:%s", bbdir)}) 364 session.WaitWithDefaultTimeout() 365 Expect(session).Should(ExitCleanly()) 366 367 session = podmanTest.Podman([]string{"rmi", ALPINE}) 368 session.WaitWithDefaultTimeout() 369 Expect(session).Should(ExitCleanly()) 370 }) 371 372 It("podman push to docker-archive", func() { 373 SkipIfRemote("Remote push does not support docker-archive transport") 374 tarfn := filepath.Join(podmanTest.TempDir, "alp.tar") 375 session := podmanTest.Podman([]string{"push", "-q", ALPINE, 376 fmt.Sprintf("docker-archive:%s:latest", tarfn)}) 377 session.WaitWithDefaultTimeout() 378 Expect(session).Should(ExitCleanly()) 379 }) 380 381 It("podman push to docker daemon", func() { 382 SkipIfRemote("Remote push does not support docker-daemon transport") 383 SkipIfRootless("rootless user has no permission to use default docker.sock") 384 setup := SystemExec("bash", []string{"-c", "systemctl status docker 2>&1"}) 385 386 if setup.LineInOutputContains("Active: inactive") { 387 setup = SystemExec("systemctl", []string{"start", "docker"}) 388 Expect(setup).Should(ExitCleanly()) 389 defer func() { 390 stop := SystemExec("systemctl", []string{"stop", "docker"}) 391 Expect(stop).Should(Exit(0)) 392 }() 393 } else if setup.ExitCode() != 0 { 394 Skip("Docker is not available") 395 } 396 397 session := podmanTest.Podman([]string{"push", "-q", ALPINE, "docker-daemon:alpine:podmantest"}) 398 session.WaitWithDefaultTimeout() 399 Expect(session).Should(ExitCleanly()) 400 401 check := SystemExec("docker", []string{"images", "--format", "{{.Repository}}:{{.Tag}}"}) 402 Expect(check).Should(ExitCleanly()) 403 Expect(check.OutputToString()).To(ContainSubstring("alpine:podmantest")) 404 405 clean := SystemExec("docker", []string{"rmi", "alpine:podmantest"}) 406 Expect(clean).Should(ExitCleanly()) 407 }) 408 409 It("podman push to oci-archive", func() { 410 SkipIfRemote("Remote push does not support oci-archive transport") 411 tarfn := filepath.Join(podmanTest.TempDir, "alp.tar") 412 session := podmanTest.Podman([]string{"push", "-q", ALPINE, 413 fmt.Sprintf("oci-archive:%s:latest", tarfn)}) 414 session.WaitWithDefaultTimeout() 415 Expect(session).Should(ExitCleanly()) 416 }) 417 418 It("podman push to docker-archive no reference", func() { 419 SkipIfRemote("Remote push does not support docker-archive transport") 420 tarfn := filepath.Join(podmanTest.TempDir, "alp.tar") 421 session := podmanTest.Podman([]string{"push", "-q", ALPINE, 422 fmt.Sprintf("docker-archive:%s", tarfn)}) 423 session.WaitWithDefaultTimeout() 424 Expect(session).Should(ExitCleanly()) 425 }) 426 427 It("podman push to oci-archive no reference", func() { 428 SkipIfRemote("Remote push does not support oci-archive transport") 429 ociarc := filepath.Join(podmanTest.TempDir, "alp-oci") 430 session := podmanTest.Podman([]string{"push", "-q", ALPINE, 431 fmt.Sprintf("oci-archive:%s", ociarc)}) 432 433 session.WaitWithDefaultTimeout() 434 Expect(session).Should(ExitCleanly()) 435 }) 436 437 })