Merge pull request #332 from mtrmac/schema12

Run-time manifest type support (e.g. schema1/schema2) autodetection re-vendor + integration tests
This commit is contained in:
Antonio Murdaca
2017-05-10 11:04:28 +02:00
committed by GitHub
14 changed files with 494 additions and 200 deletions

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@@ -22,11 +21,15 @@ func init() {
check.Suite(&CopySuite{}) check.Suite(&CopySuite{})
} }
const v2DockerRegistryURL = "localhost:5555" // Update also policy.json const (
v2DockerRegistryURL = "localhost:5555" // Update also policy.json
v2s1DockerRegistryURL = "localhost:5556"
)
type CopySuite struct { type CopySuite struct {
cluster *openshiftCluster cluster *openshiftCluster
registry *testRegistryV2 registry *testRegistryV2
s1Registry *testRegistryV2
gpgHome string gpgHome string
} }
@@ -37,7 +40,7 @@ func (s *CopySuite) SetUpSuite(c *check.C) {
s.cluster = startOpenshiftCluster(c) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place. s.cluster = startOpenshiftCluster(c) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
for _, stream := range []string{"unsigned", "personal", "official", "naming", "cosigned", "compression"} { for _, stream := range []string{"unsigned", "personal", "official", "naming", "cosigned", "compression", "schema1", "schema2"} {
isJSON := fmt.Sprintf(`{ isJSON := fmt.Sprintf(`{
"kind": "ImageStream", "kind": "ImageStream",
"apiVersion": "v1", "apiVersion": "v1",
@@ -49,7 +52,9 @@ func (s *CopySuite) SetUpSuite(c *check.C) {
runCommandWithInput(c, isJSON, "oc", "create", "-f", "-") runCommandWithInput(c, isJSON, "oc", "create", "-f", "-")
} }
s.registry = setupRegistryV2At(c, v2DockerRegistryURL, false, false) // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place. // FIXME: Set up TLS for the docker registry port instead of using "--tls-verify=false" all over the place.
s.registry = setupRegistryV2At(c, v2DockerRegistryURL, false, false)
s.s1Registry = setupRegistryV2At(c, v2s1DockerRegistryURL, false, true)
gpgHome, err := ioutil.TempDir("", "skopeo-gpg") gpgHome, err := ioutil.TempDir("", "skopeo-gpg")
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
@@ -75,31 +80,14 @@ func (s *CopySuite) TearDownSuite(c *check.C) {
if s.registry != nil { if s.registry != nil {
s.registry.Close() s.registry.Close()
} }
if s.s1Registry != nil {
s.s1Registry.Close()
}
if s.cluster != nil { if s.cluster != nil {
s.cluster.tearDown() s.cluster.tearDown(c)
} }
} }
// fileFromFixtureFixture applies edits to inputPath and returns a path to the temporary file.
// Callers should defer os.Remove(the_returned_path)
func fileFromFixture(c *check.C, inputPath string, edits map[string]string) string {
contents, err := ioutil.ReadFile(inputPath)
c.Assert(err, check.IsNil)
for template, value := range edits {
contents = bytes.Replace(contents, []byte(template), []byte(value), -1)
}
file, err := ioutil.TempFile("", "policy.json")
c.Assert(err, check.IsNil)
path := file.Name()
_, err = file.Write(contents)
c.Assert(err, check.IsNil)
err = file.Close()
c.Assert(err, check.IsNil)
return path
}
func (s *CopySuite) TestCopyFailsWithManifestList(c *check.C) { func (s *CopySuite) TestCopyFailsWithManifestList(c *check.C) {
assertSkopeoFails(c, ".*can not copy docker://estesp/busybox:latest: manifest contains multiple images.*", "copy", "docker://estesp/busybox:latest", "dir:somedir") assertSkopeoFails(c, ".*can not copy docker://estesp/busybox:latest: manifest contains multiple images.*", "copy", "docker://estesp/busybox:latest", "dir:somedir")
} }
@@ -556,3 +544,58 @@ func (s *CopySuite) TestCopyNoPanicOnHTTPResponseWOTLSVerifyFalse(c *check.C) {
assertSkopeoFails(c, ".*server gave HTTP response to HTTPS client.*", assertSkopeoFails(c, ".*server gave HTTP response to HTTPS client.*",
"copy", ourRegistry+"foobar", "dir:test") "copy", ourRegistry+"foobar", "dir:test")
} }
func (s *CopySuite) TestCopySchemaConversion(c *check.C) {
// Test conversion / schema autodetection both for the OpenShift embedded registry…
s.testCopySchemaConversionRegistries(c, "docker://localhost:5005/myns/schema1", "docker://localhost:5006/myns/schema2")
// … and for various docker/distribution registry versions.
if false {
// FIXME: This does not currently work, because the schema1-only docker/distribution registry we have (unlike newer versions)
// enforces that a schema1 manifest contains a matching tag field. Our _s2→s1 conversion_ code does set the tag correctly,
// but a mere copy of schema1→schema1 changing the tag does not update the manifest.
// So, enabling this test results in a successful schema2→schema1 conversion, followed by a schema1→schema1 copy failure.
s.testCopySchemaConversionRegistries(c, "docker://"+v2s1DockerRegistryURL+"/schema1", "docker://"+v2DockerRegistryURL+"/schema2")
}
}
func (s *CopySuite) testCopySchemaConversionRegistries(c *check.C, schema1Registry, schema2Registry string) {
topDir, err := ioutil.TempDir("", "schema-conversion")
c.Assert(err, check.IsNil)
defer os.RemoveAll(topDir)
for _, subdir := range []string{"input1", "input2", "dest2"} {
err := os.MkdirAll(filepath.Join(topDir, subdir), 0755)
c.Assert(err, check.IsNil)
}
input1Dir := filepath.Join(topDir, "input1")
input2Dir := filepath.Join(topDir, "input2")
destDir := filepath.Join(topDir, "dest2")
// Ensure we are working with a schema2 image.
// dir: accepts any manifest format, i.e. this makes …/input2 a schema2 source which cannot be asked to produce schema1 like ordinary docker: registries can.
assertSkopeoSucceeds(c, "", "copy", "docker://busybox", "dir:"+input2Dir)
verifyManifestMIMEType(c, input2Dir, manifest.DockerV2Schema2MediaType)
// 2→2 (the "f2t2" in tag means "from 2 to 2")
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "dir:"+input2Dir, schema2Registry+":f2t2")
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", schema2Registry+":f2t2", "dir:"+destDir)
verifyManifestMIMEType(c, destDir, manifest.DockerV2Schema2MediaType)
// 2→1; we will use the result as a schema1 image for further tests.
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "dir:"+input2Dir, schema1Registry+":f2t1")
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", schema1Registry+":f2t1", "dir:"+input1Dir)
verifyManifestMIMEType(c, input1Dir, manifest.DockerV2Schema1SignedMediaType)
// 1→1
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "dir:"+input1Dir, schema1Registry+":f1t1")
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", schema1Registry+":f1t1", "dir:"+destDir)
verifyManifestMIMEType(c, destDir, manifest.DockerV2Schema1SignedMediaType)
// 1→2: image stays unmodified schema1
assertSkopeoSucceeds(c, "", "copy", "--dest-tls-verify=false", "dir:"+input1Dir, schema2Registry+":f1t2")
assertSkopeoSucceeds(c, "", "copy", "--src-tls-verify=false", schema2Registry+":f1t2", "dir:"+destDir)
verifyManifestMIMEType(c, destDir, manifest.DockerV2Schema1SignedMediaType)
}
// Verify manifest in a dir: image at dir is expectedMIMEType.
func verifyManifestMIMEType(c *check.C, dir string, expectedMIMEType string) {
manifestBlob, err := ioutil.ReadFile(filepath.Join(dir, "manifest.json"))
c.Assert(err, check.IsNil)
mimeType := manifest.GuessMIMEType(manifestBlob)
c.Assert(mimeType, check.Equals, expectedMIMEType)
}

View File

@@ -21,35 +21,34 @@ var adminKUBECONFIG = map[string]string{
// openshiftCluster is an OpenShift API master and integrated registry // openshiftCluster is an OpenShift API master and integrated registry
// running on localhost. // running on localhost.
type openshiftCluster struct { type openshiftCluster struct {
c *check.C
workingDir string workingDir string
master *exec.Cmd processes []*exec.Cmd // Processes to terminate on teardown; append to the end, terminate from end to the start.
registry *exec.Cmd
} }
// startOpenshiftCluster creates a new openshiftCluster. // startOpenshiftCluster creates a new openshiftCluster.
// WARNING: This affects state in users' home directory! Only run // WARNING: This affects state in users' home directory! Only run
// in isolated test environment. // in isolated test environment.
func startOpenshiftCluster(c *check.C) *openshiftCluster { func startOpenshiftCluster(c *check.C) *openshiftCluster {
cluster := &openshiftCluster{c: c} cluster := &openshiftCluster{}
dir, err := ioutil.TempDir("", "openshift-cluster") dir, err := ioutil.TempDir("", "openshift-cluster")
cluster.c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
cluster.workingDir = dir cluster.workingDir = dir
cluster.startMaster() cluster.startMaster(c)
cluster.startRegistry() cluster.prepareRegistryConfig(c)
cluster.ocLoginToProject() cluster.startRegistry(c)
cluster.dockerLogin() cluster.ocLoginToProject(c)
cluster.relaxImageSignerPermissions() cluster.dockerLogin(c)
cluster.relaxImageSignerPermissions(c)
return cluster return cluster
} }
// clusterCmd creates an exec.Cmd in c.workingDir with current environment modified by environment // clusterCmd creates an exec.Cmd in cluster.workingDir with current environment modified by environment
func (c *openshiftCluster) clusterCmd(env map[string]string, name string, args ...string) *exec.Cmd { func (cluster *openshiftCluster) clusterCmd(env map[string]string, name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...) cmd := exec.Command(name, args...)
cmd.Dir = c.workingDir cmd.Dir = cluster.workingDir
cmd.Env = os.Environ() cmd.Env = os.Environ()
for key, value := range env { for key, value := range env {
cmd.Env = modifyEnviron(cmd.Env, key, value) cmd.Env = modifyEnviron(cmd.Env, key, value)
@@ -58,19 +57,20 @@ func (c *openshiftCluster) clusterCmd(env map[string]string, name string, args .
} }
// startMaster starts the OpenShift master (etcd+API server) and waits for it to be ready, or terminates on failure. // startMaster starts the OpenShift master (etcd+API server) and waits for it to be ready, or terminates on failure.
func (c *openshiftCluster) startMaster() { func (cluster *openshiftCluster) startMaster(c *check.C) {
c.master = c.clusterCmd(nil, "openshift", "start", "master") cmd := cluster.clusterCmd(nil, "openshift", "start", "master")
stdout, err := c.master.StdoutPipe() cluster.processes = append(cluster.processes, cmd)
stdout, err := cmd.StdoutPipe()
// Send both to the same pipe. This might cause the two streams to be mixed up, // Send both to the same pipe. This might cause the two streams to be mixed up,
// but logging actually goes only to stderr - this primarily ensure we log any // but logging actually goes only to stderr - this primarily ensure we log any
// unexpected output to stdout. // unexpected output to stdout.
c.master.Stderr = c.master.Stdout cmd.Stderr = cmd.Stdout
err = c.master.Start() err = cmd.Start()
c.c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
portOpen, terminatePortCheck := newPortChecker(c.c, 8443) portOpen, terminatePortCheck := newPortChecker(c, 8443)
defer func() { defer func() {
c.c.Logf("Terminating port check") c.Logf("Terminating port check")
terminatePortCheck <- true terminatePortCheck <- true
}() }()
@@ -78,12 +78,12 @@ func (c *openshiftCluster) startMaster() {
logCheckFound := make(chan bool) logCheckFound := make(chan bool)
go func() { go func() {
defer func() { defer func() {
c.c.Logf("Log checker exiting") c.Logf("Log checker exiting")
}() }()
scanner := bufio.NewScanner(stdout) scanner := bufio.NewScanner(stdout)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
c.c.Logf("Log line: %s", line) c.Logf("Log line: %s", line)
if strings.Contains(line, "Started Origin Controllers") { if strings.Contains(line, "Started Origin Controllers") {
logCheckFound <- true logCheckFound <- true
return return
@@ -92,7 +92,7 @@ func (c *openshiftCluster) startMaster() {
// Note: we can block before we get here. // Note: we can block before we get here.
select { select {
case <-terminateLogCheck: case <-terminateLogCheck:
c.c.Logf("terminated") c.Logf("terminated")
return return
default: default:
// Do not block here and read the next line. // Do not block here and read the next line.
@@ -101,31 +101,31 @@ func (c *openshiftCluster) startMaster() {
logCheckFound <- false logCheckFound <- false
}() }()
defer func() { defer func() {
c.c.Logf("Terminating log check") c.Logf("Terminating log check")
terminateLogCheck <- true terminateLogCheck <- true
}() }()
gotPortCheck := false gotPortCheck := false
gotLogCheck := false gotLogCheck := false
for !gotPortCheck || !gotLogCheck { for !gotPortCheck || !gotLogCheck {
c.c.Logf("Waiting for master") c.Logf("Waiting for master")
select { select {
case <-portOpen: case <-portOpen:
c.c.Logf("port check done") c.Logf("port check done")
gotPortCheck = true gotPortCheck = true
case found := <-logCheckFound: case found := <-logCheckFound:
c.c.Logf("log check done, found: %t", found) c.Logf("log check done, found: %t", found)
if !found { if !found {
c.c.Fatal("log check done, success message not found") c.Fatal("log check done, success message not found")
} }
gotLogCheck = true gotLogCheck = true
} }
} }
c.c.Logf("OK, master started!") c.Logf("OK, master started!")
} }
// startRegistry starts the OpenShift registry and waits for it to be ready, or terminates on failure. // prepareRegistryConfig creates a registry service account and a related k8s client configuration in ${cluster.workingDir}/openshift.local.registry.
func (c *openshiftCluster) startRegistry() { func (cluster *openshiftCluster) prepareRegistryConfig(c *check.C) {
// This partially mimics the objects created by (oadm registry), except that we run the // This partially mimics the objects created by (oadm registry), except that we run the
// server directly as an ordinary process instead of a pod with an implicitly attached service account. // server directly as an ordinary process instead of a pod with an implicitly attached service account.
saJSON := `{ saJSON := `{
@@ -135,90 +135,118 @@ func (c *openshiftCluster) startRegistry() {
"name": "registry" "name": "registry"
} }
}` }`
cmd := c.clusterCmd(adminKUBECONFIG, "oc", "create", "-f", "-") cmd := cluster.clusterCmd(adminKUBECONFIG, "oc", "create", "-f", "-")
runExecCmdWithInput(c.c, cmd, saJSON) runExecCmdWithInput(c, cmd, saJSON)
cmd = c.clusterCmd(adminKUBECONFIG, "oadm", "policy", "add-cluster-role-to-user", "system:registry", "-z", "registry") cmd = cluster.clusterCmd(adminKUBECONFIG, "oadm", "policy", "add-cluster-role-to-user", "system:registry", "-z", "registry")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", string(out))) c.Assert(err, check.IsNil, check.Commentf("%s", string(out)))
c.c.Assert(string(out), check.Equals, "cluster role \"system:registry\" added: \"registry\"\n") c.Assert(string(out), check.Equals, "cluster role \"system:registry\" added: \"registry\"\n")
cmd = c.clusterCmd(adminKUBECONFIG, "oadm", "create-api-client-config", "--client-dir=openshift.local.registry", "--basename=openshift-registry", "--user=system:serviceaccount:default:registry") cmd = cluster.clusterCmd(adminKUBECONFIG, "oadm", "create-api-client-config", "--client-dir=openshift.local.registry", "--basename=openshift-registry", "--user=system:serviceaccount:default:registry")
out, err = cmd.CombinedOutput() out, err = cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", string(out))) c.Assert(err, check.IsNil, check.Commentf("%s", string(out)))
c.c.Assert(string(out), check.Equals, "") c.Assert(string(out), check.Equals, "")
}
//KUBECONFIG=openshift.local.registry/openshift-registry.kubeconfig DOCKER_REGISTRY_URL=127.0.0.1:5000 // startRegistry starts the OpenShift registry with configPart on port, waits for it to be ready, and returns the process object, or terminates on failure.
c.registry = c.clusterCmd(map[string]string{ func (cluster *openshiftCluster) startRegistryProcess(c *check.C, port int, configPath string) *exec.Cmd {
cmd := cluster.clusterCmd(map[string]string{
"KUBECONFIG": "openshift.local.registry/openshift-registry.kubeconfig", "KUBECONFIG": "openshift.local.registry/openshift-registry.kubeconfig",
"DOCKER_REGISTRY_URL": "127.0.0.1:5000", "DOCKER_REGISTRY_URL": fmt.Sprintf("127.0.0.1:%d", port),
}, "dockerregistry", "/atomic-registry-config.yml") }, "dockerregistry", configPath)
consumeAndLogOutputs(c.c, "registry", c.registry) consumeAndLogOutputs(c, fmt.Sprintf("registry-%d", port), cmd)
err = c.registry.Start() err := cmd.Start()
c.c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
portOpen, terminatePortCheck := newPortChecker(c.c, 5000) portOpen, terminatePortCheck := newPortChecker(c, port)
defer func() { defer func() {
terminatePortCheck <- true terminatePortCheck <- true
}() }()
c.c.Logf("Waiting for registry to start") c.Logf("Waiting for registry to start")
<-portOpen <-portOpen
c.c.Logf("OK, Registry port open") c.Logf("OK, Registry port open")
return cmd
}
// startRegistry starts the OpenShift registry and waits for it to be ready, or terminates on failure.
func (cluster *openshiftCluster) startRegistry(c *check.C) {
// Our “primary” registry
cluster.processes = append(cluster.processes, cluster.startRegistryProcess(c, 5000, "/atomic-registry-config.yml"))
// A registry configured with acceptschema2:false
schema1Config := fileFromFixture(c, "/atomic-registry-config.yml", map[string]string{
"addr: :5000": "addr: :5005",
"rootdirectory: /registry": "rootdirectory: /registry-schema1",
// The default configuration currently already contains acceptschema2: false
})
// Make sure the configuration contains "acceptschema2: false", because eventually it will be enabled upstream and this function will need to be updated.
configContents, err := ioutil.ReadFile(schema1Config)
c.Assert(err, check.IsNil)
c.Assert(string(configContents), check.Matches, "(?s).*acceptschema2: false.*")
cluster.processes = append(cluster.processes, cluster.startRegistryProcess(c, 5005, schema1Config))
// A registry configured with acceptschema2:true
schema2Config := fileFromFixture(c, "/atomic-registry-config.yml", map[string]string{
"addr: :5000": "addr: :5006",
"rootdirectory: /registry": "rootdirectory: /registry-schema2",
"acceptschema2: false": "acceptschema2: true",
})
cluster.processes = append(cluster.processes, cluster.startRegistryProcess(c, 5006, schema2Config))
} }
// ocLogin runs (oc login) and (oc new-project) on the cluster, or terminates on failure. // ocLogin runs (oc login) and (oc new-project) on the cluster, or terminates on failure.
func (c *openshiftCluster) ocLoginToProject() { func (cluster *openshiftCluster) ocLoginToProject(c *check.C) {
c.c.Logf("oc login") c.Logf("oc login")
cmd := c.clusterCmd(nil, "oc", "login", "--certificate-authority=openshift.local.config/master/ca.crt", "-u", "myuser", "-p", "mypw", "https://localhost:8443") cmd := cluster.clusterCmd(nil, "oc", "login", "--certificate-authority=openshift.local.config/master/ca.crt", "-u", "myuser", "-p", "mypw", "https://localhost:8443")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", out)) c.Assert(err, check.IsNil, check.Commentf("%s", out))
c.c.Assert(string(out), check.Matches, "(?s).*Login successful.*") // (?s) : '.' will also match newlines c.Assert(string(out), check.Matches, "(?s).*Login successful.*") // (?s) : '.' will also match newlines
outString := combinedOutputOfCommand(c.c, "oc", "new-project", "myns") outString := combinedOutputOfCommand(c, "oc", "new-project", "myns")
c.c.Assert(outString, check.Matches, `(?s).*Now using project "myns".*`) // (?s) : '.' will also match newlines c.Assert(outString, check.Matches, `(?s).*Now using project "myns".*`) // (?s) : '.' will also match newlines
} }
// dockerLogin simulates (docker login) to the cluster, or terminates on failure. // dockerLogin simulates (docker login) to the cluster, or terminates on failure.
// We do not run (docker login) directly, because that requires a running daemon and a docker package. // We do not run (docker login) directly, because that requires a running daemon and a docker package.
func (c *openshiftCluster) dockerLogin() { func (cluster *openshiftCluster) dockerLogin(c *check.C) {
dockerDir := filepath.Join(homedir.Get(), ".docker") dockerDir := filepath.Join(homedir.Get(), ".docker")
err := os.Mkdir(dockerDir, 0700) err := os.Mkdir(dockerDir, 0700)
c.c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
out := combinedOutputOfCommand(c.c, "oc", "config", "view", "-o", "json", "-o", "jsonpath={.users[*].user.token}") out := combinedOutputOfCommand(c, "oc", "config", "view", "-o", "json", "-o", "jsonpath={.users[*].user.token}")
c.c.Logf("oc config value: %s", out) c.Logf("oc config value: %s", out)
configJSON := fmt.Sprintf(`{ authValue := base64.StdEncoding.EncodeToString([]byte("unused:" + out))
"auths": { auths := []string{}
"localhost:5000": { for _, port := range []int{5000, 5005, 5006} {
auths = append(auths, fmt.Sprintf(`"localhost:%d": {
"auth": "%s", "auth": "%s",
"email": "unused" "email": "unused"
}`, port, authValue))
} }
} configJSON := `{"auths": {` + strings.Join(auths, ",") + `}}`
}`, base64.StdEncoding.EncodeToString([]byte("unused:"+out)))
err = ioutil.WriteFile(filepath.Join(dockerDir, "config.json"), []byte(configJSON), 0600) err = ioutil.WriteFile(filepath.Join(dockerDir, "config.json"), []byte(configJSON), 0600)
c.c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)
} }
// relaxImageSignerPermissions opens up the system:image-signer permissions so that // relaxImageSignerPermissions opens up the system:image-signer permissions so that
// anyone can work with signatures // anyone can work with signatures
// FIXME: This also allows anyone to DoS anyone else; this design is really not all // FIXME: This also allows anyone to DoS anyone else; this design is really not all
// that workable, but it is the best we can do for now. // that workable, but it is the best we can do for now.
func (c *openshiftCluster) relaxImageSignerPermissions() { func (cluster *openshiftCluster) relaxImageSignerPermissions(c *check.C) {
cmd := c.clusterCmd(adminKUBECONFIG, "oadm", "policy", "add-cluster-role-to-group", "system:image-signer", "system:authenticated") cmd := cluster.clusterCmd(adminKUBECONFIG, "oadm", "policy", "add-cluster-role-to-group", "system:image-signer", "system:authenticated")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
c.c.Assert(err, check.IsNil, check.Commentf("%s", string(out))) c.Assert(err, check.IsNil, check.Commentf("%s", string(out)))
c.c.Assert(string(out), check.Equals, "cluster role \"system:image-signer\" added: \"system:authenticated\"\n") c.Assert(string(out), check.Equals, "cluster role \"system:image-signer\" added: \"system:authenticated\"\n")
} }
// tearDown stops the cluster services and deletes (only some!) of the state. // tearDown stops the cluster services and deletes (only some!) of the state.
func (c *openshiftCluster) tearDown() { func (cluster *openshiftCluster) tearDown(c *check.C) {
if c.registry != nil && c.registry.Process != nil { for i := len(cluster.processes) - 1; i >= 0; i-- {
c.registry.Process.Kill() cluster.processes[i].Process.Kill()
} }
if c.master != nil && c.master.Process != nil { if cluster.workingDir != "" {
c.master.Process.Kill() os.RemoveAll(cluster.workingDir)
}
if c.workingDir != "" {
os.RemoveAll(c.workingDir)
} }
} }

View File

@@ -1,7 +1,9 @@
package main package main
import ( import (
"bytes"
"io" "io"
"io/ioutil"
"net" "net"
"os/exec" "os/exec"
"strings" "strings"
@@ -150,3 +152,25 @@ func modifyEnviron(env []string, name, value string) []string {
} }
return append(res, prefix+value) return append(res, prefix+value)
} }
// fileFromFixtureFixture applies edits to inputPath and returns a path to the temporary file.
// Callers should defer os.Remove(the_returned_path)
func fileFromFixture(c *check.C, inputPath string, edits map[string]string) string {
contents, err := ioutil.ReadFile(inputPath)
c.Assert(err, check.IsNil)
for template, value := range edits {
updated := bytes.Replace(contents, []byte(template), []byte(value), -1)
c.Assert(bytes.Equal(updated, contents), check.Equals, false, check.Commentf("Replacing %s in %#v failed", template, string(contents))) // Verify that the template has matched something and we are not silently ignoring it.
contents = updated
}
file, err := ioutil.TempFile("", "policy.json")
c.Assert(err, check.IsNil)
path := file.Name()
_, err = file.Write(contents)
c.Assert(err, check.IsNil)
err = file.Close()
c.Assert(err, check.IsNil)
return path
}

View File

@@ -7,13 +7,13 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"reflect" "reflect"
"strings"
"time" "time"
pb "gopkg.in/cheggaaa/pb.v1" pb "gopkg.in/cheggaaa/pb.v1"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/containers/image/image" "github.com/containers/image/image"
"github.com/containers/image/manifest"
"github.com/containers/image/pkg/compression" "github.com/containers/image/pkg/compression"
"github.com/containers/image/signature" "github.com/containers/image/signature"
"github.com/containers/image/transports" "github.com/containers/image/transports"
@@ -22,11 +22,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// preferredManifestMIMETypes lists manifest MIME types in order of our preference, if we can't use the original manifest and need to convert.
// Prefer v2s2 to v2s1 because v2s2 does not need to be changed when uploading to a different location.
// Include v2s1 signed but not v2s1 unsigned, because docker/distribution requires a signature even if the unsigned MIME type is used.
var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType}
type digestingReader struct { type digestingReader struct {
source io.Reader source io.Reader
digester digest.Digester digester digest.Digester
@@ -186,8 +181,12 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
canModifyManifest := len(sigs) == 0 canModifyManifest := len(sigs) == 0
manifestUpdates := types.ManifestUpdateOptions{} manifestUpdates := types.ManifestUpdateOptions{}
manifestUpdates.InformationOnly.Destination = dest
if err := determineManifestConversion(&manifestUpdates, src, destSupportedManifestMIMETypes, canModifyManifest); err != nil { // We compute preferredManifestMIMEType only to show it in error messages.
// Without having to add this context in an error message, we would be happy enough to know only that no conversion is needed.
preferredManifestMIMEType, otherManifestMIMETypeCandidates, err := determineManifestConversion(&manifestUpdates, src, destSupportedManifestMIMETypes, canModifyManifest)
if err != nil {
return err return err
} }
@@ -210,54 +209,58 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
return err return err
} }
pendingImage := src // With docker/distribution registries we do not know whether the registry accepts schema2 or schema1 only;
if !reflect.DeepEqual(manifestUpdates, types.ManifestUpdateOptions{InformationOnly: manifestUpdates.InformationOnly}) { // and at least with the OpenShift registry "acceptschema2" option, there is no way to detect the support
// without actually trying to upload something and getting a types.ManifestTypeRejectedError.
// So, try the preferred manifest MIME type. If the process succeeds, fine…
manifest, err := ic.copyUpdatedConfigAndManifest()
if err != nil {
logrus.Debugf("Writing manifest using preferred type %s failed: %v", preferredManifestMIMEType, err)
// … if it fails, _and_ the failure is because the manifest is rejected, we may have other options.
if _, isManifestRejected := errors.Cause(err).(types.ManifestTypeRejectedError); !isManifestRejected || len(otherManifestMIMETypeCandidates) == 0 {
// We dont have other options.
// In principle the code below would handle this as well, but the resulting error message is fairly ugly. Do
// Dont bother the user with MIME types if we have no choice.
return err
}
// If the original MIME type is acceptable, determineManifestConversion always uses it as preferredManifestMIMEType.
// So if we are here, we will definitely be trying to convert the manifest.
// With !canModifyManifest, that would just be a string of repeated failures for the same reason,
// so lets bail out early and with a better error message.
if !canModifyManifest { if !canModifyManifest {
return errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden") return errors.Wrap(err, "Writing manifest failed (and converting it is not possible)")
}
manifestUpdates.InformationOnly.Destination = dest
pendingImage, err = src.UpdatedImage(manifestUpdates)
if err != nil {
return errors.Wrap(err, "Error creating an updated image manifest")
}
}
manifest, _, err := pendingImage.Manifest()
if err != nil {
return errors.Wrap(err, "Error reading manifest")
} }
if err := ic.copyConfig(pendingImage); err != nil { // errs is a list of errors when trying various manifest types. Also serves as an "upload succeeded" flag when set to nil.
return err errs := []string{fmt.Sprintf("%s(%v)", preferredManifestMIMEType, err)}
for _, manifestMIMEType := range otherManifestMIMETypeCandidates {
logrus.Debugf("Trying to use manifest type %s…", manifestMIMEType)
manifestUpdates.ManifestMIMEType = manifestMIMEType
attemptedManifest, err := ic.copyUpdatedConfigAndManifest()
if err != nil {
logrus.Debugf("Upload of manifest type %s failed: %v", manifestMIMEType, err)
errs = append(errs, fmt.Sprintf("%s(%v)", manifestMIMEType, err))
continue
}
// We have successfully uploaded a manifest.
manifest = attemptedManifest
errs = nil // Mark this as a success so that we don't abort below.
break
}
if errs != nil {
return fmt.Errorf("Uploading manifest failed, attempted the following formats: %s", strings.Join(errs, ", "))
}
} }
if options.SignBy != "" { if options.SignBy != "" {
mech, err := signature.NewGPGSigningMechanism() newSig, err := createSignature(dest, manifest, options.SignBy, reportWriter)
if err != nil { if err != nil {
return errors.Wrap(err, "Error initializing GPG") return err
}
defer mech.Close()
if err := mech.SupportsSigning(); err != nil {
return errors.Wrap(err, "Signing not supported")
}
dockerReference := dest.Reference().DockerReference()
if dockerReference == nil {
return errors.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(dest.Reference()))
}
writeReport("Signing manifest\n")
newSig, err := signature.SignDockerManifest(manifest, dockerReference.String(), mech, options.SignBy)
if err != nil {
return errors.Wrap(err, "Error creating signature")
} }
sigs = append(sigs, newSig) sigs = append(sigs, newSig)
} }
writeReport("Writing manifest to image destination\n")
if err := dest.PutManifest(manifest); err != nil {
return errors.Wrap(err, "Error writing manifest")
}
writeReport("Storing signatures\n") writeReport("Storing signatures\n")
if err := dest.PutSignatures(sigs); err != nil { if err := dest.PutSignatures(sigs); err != nil {
return errors.Wrap(err, "Error writing signatures") return errors.Wrap(err, "Error writing signatures")
@@ -322,6 +325,45 @@ func layerDigestsDiffer(a, b []types.BlobInfo) bool {
return false return false
} }
// copyUpdatedConfigAndManifest updates the image per ic.manifestUpdates, if necessary,
// stores the resulting config and manifest ot the destination, and returns the stored manifest.
func (ic *imageCopier) copyUpdatedConfigAndManifest() ([]byte, error) {
pendingImage := ic.src
if !reflect.DeepEqual(*ic.manifestUpdates, types.ManifestUpdateOptions{InformationOnly: ic.manifestUpdates.InformationOnly}) {
if !ic.canModifyManifest {
return nil, errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden")
}
if !ic.diffIDsAreNeeded && ic.src.UpdatedImageNeedsLayerDiffIDs(*ic.manifestUpdates) {
// We have set ic.diffIDsAreNeeded based on the preferred MIME type returned by determineManifestConversion.
// So, this can only happen if we are trying to upload using one of the other MIME type candidates.
// Because UpdatedImageNeedsLayerDiffIDs only when converting from s1 to s2, this case should only arise
// when ic.dest.SupportedManifestMIMETypes() includes both s1 and s2, the upload using s1 failed, and we are now trying s2.
// Supposedly s2-only registries do not exist or are extremely rare, so failing with this error message is good enough for now.
// If handling such registries turned out to be necessary, we could compute ic.diffIDsAreNeeded based on the full list of manifest MIME type candidates.
return nil, errors.Errorf("Can not convert image to %s, preparing DiffIDs for this case is not supported", ic.manifestUpdates.ManifestMIMEType)
}
pi, err := ic.src.UpdatedImage(*ic.manifestUpdates)
if err != nil {
return nil, errors.Wrap(err, "Error creating an updated image manifest")
}
pendingImage = pi
}
manifest, _, err := pendingImage.Manifest()
if err != nil {
return nil, errors.Wrap(err, "Error reading manifest")
}
if err := ic.copyConfig(pendingImage); err != nil {
return nil, err
}
fmt.Fprintf(ic.reportWriter, "Writing manifest to image destination\n")
if err := ic.dest.PutManifest(manifest); err != nil {
return nil, errors.Wrap(err, "Error writing manifest")
}
return manifest, nil
}
// copyConfig copies config.json, if any, from src to dest. // copyConfig copies config.json, if any, from src to dest.
func (ic *imageCopier) copyConfig(src types.Image) error { func (ic *imageCopier) copyConfig(src types.Image) error {
srcInfo := src.ConfigInfo() srcInfo := src.ConfigInfo()
@@ -575,41 +617,3 @@ func compressGoroutine(dest *io.PipeWriter, src io.Reader) {
_, err = io.Copy(zipper, src) // Sets err to nil, i.e. causes dest.Close() _, err = io.Copy(zipper, src) // Sets err to nil, i.e. causes dest.Close()
} }
// determineManifestConversion updates manifestUpdates to convert manifest to a supported MIME type, if necessary and canModifyManifest.
// Note that the conversion will only happen later, through src.UpdatedImage
func determineManifestConversion(manifestUpdates *types.ManifestUpdateOptions, src types.Image, destSupportedManifestMIMETypes []string, canModifyManifest bool) error {
if len(destSupportedManifestMIMETypes) == 0 {
return nil // Anything goes
}
supportedByDest := map[string]struct{}{}
for _, t := range destSupportedManifestMIMETypes {
supportedByDest[t] = struct{}{}
}
_, srcType, err := src.Manifest()
if err != nil { // This should have been cached?!
return errors.Wrap(err, "Error reading manifest")
}
if _, ok := supportedByDest[srcType]; ok {
logrus.Debugf("Manifest MIME type %s is declared supported by the destination", srcType)
return nil
}
// OK, we should convert the manifest.
if !canModifyManifest {
logrus.Debugf("Manifest MIME type %s is not supported by the destination, but we can't modify the manifest, hoping for the best...")
return nil // Take our chances - FIXME? Or should we fail without trying?
}
var chosenType = destSupportedManifestMIMETypes[0] // This one is known to be supported.
for _, t := range preferredManifestMIMETypes {
if _, ok := supportedByDest[t]; ok {
chosenType = t
break
}
}
logrus.Debugf("Will convert manifest from MIME type %s to %s", srcType, chosenType)
manifestUpdates.ManifestMIMEType = chosenType
return nil
}

102
vendor/github.com/containers/image/copy/manifest.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package copy
import (
"strings"
"github.com/Sirupsen/logrus"
"github.com/containers/image/manifest"
"github.com/containers/image/types"
"github.com/pkg/errors"
)
// preferredManifestMIMETypes lists manifest MIME types in order of our preference, if we can't use the original manifest and need to convert.
// Prefer v2s2 to v2s1 because v2s2 does not need to be changed when uploading to a different location.
// Include v2s1 signed but not v2s1 unsigned, because docker/distribution requires a signature even if the unsigned MIME type is used.
var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType}
// orderedSet is a list of strings (MIME types in our case), with each string appearing at most once.
type orderedSet struct {
list []string
included map[string]struct{}
}
// newOrderedSet creates a correctly initialized orderedSet.
// [Sometimes it would be really nice if Golang had constructors…]
func newOrderedSet() *orderedSet {
return &orderedSet{
list: []string{},
included: map[string]struct{}{},
}
}
// append adds s to the end of os, only if it is not included already.
func (os *orderedSet) append(s string) {
if _, ok := os.included[s]; !ok {
os.list = append(os.list, s)
os.included[s] = struct{}{}
}
}
// determineManifestConversion updates manifestUpdates to convert manifest to a supported MIME type, if necessary and canModifyManifest.
// Note that the conversion will only happen later, through src.UpdatedImage
// Returns the preferred manifest MIME type (whether we are converting to it or using it unmodified),
// and a list of other possible alternatives, in order.
func determineManifestConversion(manifestUpdates *types.ManifestUpdateOptions, src types.Image, destSupportedManifestMIMETypes []string, canModifyManifest bool) (string, []string, error) {
_, srcType, err := src.Manifest()
if err != nil { // This should have been cached?!
return "", nil, errors.Wrap(err, "Error reading manifest")
}
if len(destSupportedManifestMIMETypes) == 0 {
return srcType, []string{}, nil // Anything goes; just use the original as is, do not try any conversions.
}
supportedByDest := map[string]struct{}{}
for _, t := range destSupportedManifestMIMETypes {
supportedByDest[t] = struct{}{}
}
// destSupportedManifestMIMETypes is a static guess; a particular registry may still only support a subset of the types.
// So, build a list of types to try in order of decreasing preference.
// FIXME? This treats manifest.DockerV2Schema1SignedMediaType and manifest.DockerV2Schema1MediaType as distinct,
// although we are not really making any conversion, and it is very unlikely that a destination would support one but not the other.
// In practice, schema1 is probably the lowest common denominator, so we would expect to try the first one of the MIME types
// and never attempt the other one.
prioritizedTypes := newOrderedSet()
// First of all, prefer to keep the original manifest unmodified.
if _, ok := supportedByDest[srcType]; ok {
prioritizedTypes.append(srcType)
}
if !canModifyManifest {
// We could also drop the !canModifyManifest parameter and have the caller
// make the choice; it is already doing that to an extent, to improve error
// messages. But it is nice to hide the “if !canModifyManifest, do no conversion”
// special case in here; the caller can then worry (or not) only about a good UI.
logrus.Debugf("We can't modify the manifest, hoping for the best...")
return srcType, []string{}, nil // Take our chances - FIXME? Or should we fail without trying?
}
// Then use our list of preferred types.
for _, t := range preferredManifestMIMETypes {
if _, ok := supportedByDest[t]; ok {
prioritizedTypes.append(t)
}
}
// Finally, try anything else the destination supports.
for _, t := range destSupportedManifestMIMETypes {
prioritizedTypes.append(t)
}
logrus.Debugf("Manifest has MIME type %s, ordered candidate list [%s]", srcType, strings.Join(prioritizedTypes.list, ", "))
if len(prioritizedTypes.list) == 0 { // Coverage: destSupportedManifestMIMETypes is not empty (or we would have exited in the “Anything goes” case above), so this should never happen.
return "", nil, errors.New("Internal error: no candidate MIME types")
}
preferredType := prioritizedTypes.list[0]
if preferredType != srcType {
manifestUpdates.ManifestMIMEType = preferredType
} else {
logrus.Debugf("... will first try using the original manifest unmodified")
}
return preferredType, prioritizedTypes.list[1:], nil
}

35
vendor/github.com/containers/image/copy/sign.go generated vendored Normal file
View File

@@ -0,0 +1,35 @@
package copy
import (
"fmt"
"io"
"github.com/containers/image/signature"
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/pkg/errors"
)
// createSignature creates a new signature of manifest at (identified by) dest using keyIdentity.
func createSignature(dest types.ImageDestination, manifest []byte, keyIdentity string, reportWriter io.Writer) ([]byte, error) {
mech, err := signature.NewGPGSigningMechanism()
if err != nil {
return nil, errors.Wrap(err, "Error initializing GPG")
}
defer mech.Close()
if err := mech.SupportsSigning(); err != nil {
return nil, errors.Wrap(err, "Signing not supported")
}
dockerReference := dest.Reference().DockerReference()
if dockerReference == nil {
return nil, errors.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(dest.Reference()))
}
fmt.Fprintf(reportWriter, "Signing manifest\n")
newSig, err := signature.SignDockerManifest(manifest, dockerReference.String(), mech, keyIdentity)
if err != nil {
return nil, errors.Wrap(err, "Error creating signature")
}
return newSig, nil
}

View File

@@ -118,6 +118,10 @@ func (d *dirImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo,
return info, nil return info, nil
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *dirImageDestination) PutManifest(manifest []byte) error { func (d *dirImageDestination) PutManifest(manifest []byte) error {
return ioutil.WriteFile(d.ref.manifestPath(), manifest, 0644) return ioutil.WriteFile(d.ref.manifestPath(), manifest, 0644)
} }

View File

@@ -16,6 +16,9 @@ import (
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -209,6 +212,10 @@ func (d *dockerImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInf
return info, nil return info, nil
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *dockerImageDestination) PutManifest(m []byte) error { func (d *dockerImageDestination) PutManifest(m []byte) error {
digest, err := manifest.Digest(m) digest, err := manifest.Digest(m)
if err != nil { if err != nil {
@@ -233,16 +240,31 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != http.StatusCreated { if res.StatusCode != http.StatusCreated {
body, err := ioutil.ReadAll(res.Body) err = errors.Wrapf(client.HandleErrorResponse(res), "Error uploading manifest to %s", path)
if err == nil { if isManifestInvalidError(errors.Cause(err)) {
logrus.Debugf("Error body %s", string(body)) err = types.ManifestTypeRejectedError{Err: err}
} }
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res) return err
return errors.Errorf("Error uploading manifest to %s, status %d", path, res.StatusCode)
} }
return nil return nil
} }
// isManifestInvalidError returns true iff err from client.HandleErrorReponse is a “manifest invalid” error.
func isManifestInvalidError(err error) bool {
errors, ok := err.(errcode.Errors)
if !ok || len(errors) == 0 {
return false
}
ec, ok := errors[0].(errcode.ErrorCoder)
if !ok {
return false
}
// ErrorCodeManifestInvalid is returned by OpenShift with acceptschema2=false.
// ErrorCodeTagInvalid is returned by docker/distribution (at least as of ec87e9b6971d831f0eff752ddb54fb64693e51cd)
// when uploading to a tag (because it cant find a matching tag inside the manifest)
return ec.ErrorCode() == v2.ErrorCodeManifestInvalid || ec.ErrorCode() == v2.ErrorCodeTagInvalid
}
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error { func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
// Do not fail if we dont really need to support signatures. // Do not fail if we dont really need to support signatures.
if len(signatures) == 0 { if len(signatures) == 0 {

View File

@@ -156,10 +156,13 @@ func (d *Destination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo, error) {
return info, nil return info, nil
} }
// PutManifest sends the given manifest blob to the destination. // PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate // FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// between schema versions. // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *Destination) PutManifest(m []byte) error { func (d *Destination) PutManifest(m []byte) error {
// We do not bother with types.ManifestTypeRejectedError; our .SupportedManifestMIMETypes() above is already providing only one alternative,
// so the caller trying a different manifest kind would be pointless.
var man schema2Manifest var man schema2Manifest
if err := json.Unmarshal(m, &man); err != nil { if err := json.Unmarshal(m, &man); err != nil {
return errors.Wrap(err, "Error parsing manifest") return errors.Wrap(err, "Error parsing manifest")

View File

@@ -138,6 +138,10 @@ func (d *ociImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo,
return info, nil return info, nil
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *ociImageDestination) PutManifest(m []byte) error { func (d *ociImageDestination) PutManifest(m []byte) error {
digest, err := manifest.Digest(m) digest, err := manifest.Digest(m)
if err != nil { if err != nil {

View File

@@ -383,6 +383,10 @@ func (d *openshiftImageDestination) ReapplyBlob(info types.BlobInfo) (types.Blob
return d.docker.ReapplyBlob(info) return d.docker.ReapplyBlob(info)
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *openshiftImageDestination) PutManifest(m []byte) error { func (d *openshiftImageDestination) PutManifest(m []byte) error {
manifestDigest, err := manifest.Digest(m) manifestDigest, err := manifest.Digest(m)
if err != nil { if err != nil {

View File

@@ -218,6 +218,10 @@ func (d *ostreeImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInf
return info, nil return info, nil
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (d *ostreeImageDestination) PutManifest(manifest []byte) error { func (d *ostreeImageDestination) PutManifest(manifest []byte) error {
d.manifest = string(manifest) d.manifest = string(manifest)

View File

@@ -420,6 +420,10 @@ func (s *storageImageDestination) SupportedManifestMIMETypes() []string {
return nil return nil
} }
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
func (s *storageImageDestination) PutManifest(manifest []byte) error { func (s *storageImageDestination) PutManifest(manifest []byte) error {
s.Manifest = make([]byte, len(manifest)) s.Manifest = make([]byte, len(manifest))
copy(s.Manifest, manifest) copy(s.Manifest, manifest)

View File

@@ -167,8 +167,11 @@ type ImageDestination interface {
HasBlob(info BlobInfo) (bool, int64, error) HasBlob(info BlobInfo) (bool, int64, error)
// ReapplyBlob informs the image destination that a blob for which HasBlob previously returned true would have been passed to PutBlob if it had returned false. Like HasBlob and unlike PutBlob, the digest can not be empty. If the blob is a filesystem layer, this signifies that the changes it describes need to be applied again when composing a filesystem tree. // ReapplyBlob informs the image destination that a blob for which HasBlob previously returned true would have been passed to PutBlob if it had returned false. Like HasBlob and unlike PutBlob, the digest can not be empty. If the blob is a filesystem layer, this signifies that the changes it describes need to be applied again when composing a filesystem tree.
ReapplyBlob(info BlobInfo) (BlobInfo, error) ReapplyBlob(info BlobInfo) (BlobInfo, error)
// PutManifest writes manifest to the destination.
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions. // FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
PutManifest([]byte) error // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
PutManifest(manifest []byte) error
PutSignatures(signatures [][]byte) error PutSignatures(signatures [][]byte) error
// Commit marks the process of storing the image as successful and asks for the image to be persisted. // Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics: // WARNING: This does not have any transactional semantics:
@@ -177,6 +180,16 @@ type ImageDestination interface {
Commit() error Commit() error
} }
// ManifestTypeRejectedError is returned by ImageDestination.PutManifest if the destination is in principle available,
// refuses specifically this manifest type, but may accept a different manifest type.
type ManifestTypeRejectedError struct { // We only use a struct to allow a type assertion, without limiting the contents of the error otherwise.
Err error
}
func (e ManifestTypeRejectedError) Error() string {
return e.Err.Error()
}
// UnparsedImage is an Image-to-be; until it is verified and accepted, it only caries its identity and caches manifest and signature blobs. // UnparsedImage is an Image-to-be; until it is verified and accepted, it only caries its identity and caches manifest and signature blobs.
// Thus, an UnparsedImage can be created from an ImageSource simply by fetching blobs without interpreting them, // Thus, an UnparsedImage can be created from an ImageSource simply by fetching blobs without interpreting them,
// allowing cryptographic signature verification to happen first, before even fetching the manifest, or parsing anything else. // allowing cryptographic signature verification to happen first, before even fetching the manifest, or parsing anything else.