diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43f64fe75..cd6675016 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,5 +34,7 @@ jobs: run: make build - name: Run unit tests run: make test + - name: Run e2e tests + run: make e2e - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 \ No newline at end of file + uses: codecov/codecov-action@v3 diff --git a/Makefile b/Makefile index 13696cbce..8cb73b83d 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ GO_BUILD_FLAGS = --ldflags="$(LDFLAGS)" .PHONY: help help: - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' .PHONY: all all: build @@ -43,6 +43,13 @@ build: $(addprefix bin/,$(COMMANDS)) ## builds binaries test: vendor check-line-endings ## run unit tests go test -race -v -coverprofile=coverage.txt -covermode=atomic ./... + +.PHONY: e2e +e2e: build ## build notation cli and run e2e test + NOTATION_BIN_PATH=`pwd`/bin/$(COMMANDS); \ + cd ./test/e2e; \ + ./run.sh zot $$NOTATION_BIN_PATH + .PHONY: clean clean: git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf diff --git a/go.mod b/go.mod index 1af3fae1b..728313595 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/veraison/go-cose v1.0.0-rc.2 oras.land/oras-go/v2 v2.0.0-rc.6 ) @@ -22,6 +21,7 @@ require ( github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/veraison/go-cose v1.0.0-rc.2 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.3.0 // indirect golang.org/x/sync v0.1.0 // indirect diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 000000000..04f67c74d --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,28 @@ +# A Quick Introduction on how Notation end-to-end test works + +## Framework +Using [Ginkgo](https://onsi.github.io/ginkgo/) as the e2e framework, which is based on the Golang standard testing library. + +Using [Gomega](https://onsi.github.io/gomega/) as the matching library. + +## Introduction + +### Dataļ¼štestdata contains data needed by e2e tests, including: +- *config*: notation test key and cert files. +- *registry*: OCI layout files and registry config files. +### For developer +- *Test registry*: a test registry started before running tests. +- *Config isolation*: notation needs a few configuration files in user level directory, which can be isolated by modify `XDG_CONFIG_HOME` environment variable. In Notation E2E test framework, a VirtualHost abstraction is designed for isolating user level configuration. +- *Parallelization*: In order to speed up testing, Ginkgo will launch several processes to run e2e test cases. +- *Randomization*: By default, Ginkgo will run specs in a suite in random order. Please make sure the test cases can be runned independently. If the test cases depend on the execution order, consider using [Ordered Containers](https://onsi.github.io/ginkgo/#ordered-containers). + + +## Setting up +### Github Actions +- Please check `Run e2e tests` steps in **workflows/build.yml** for detail. +### Local environment +- Install Golang. +- Install Docker. +- Clone the repository. +- Run `cd ./test/e2e` +- Run `./run.sh ` \ No newline at end of file diff --git a/test/e2e/go.mod b/test/e2e/go.mod new file mode 100644 index 000000000..5b75e9e8d --- /dev/null +++ b/test/e2e/go.mod @@ -0,0 +1,20 @@ +module github.com/notaryproject/notation/test/e2e + +go 1.19 + +require ( + github.com/onsi/ginkgo/v2 v2.3.0 + github.com/onsi/gomega v1.22.1 + oras.land/oras-go/v2 v2.0.0-rc.6 +) + +require ( + github.com/google/go-cmp v0.5.8 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + golang.org/x/net v0.1.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test/e2e/go.sum b/test/e2e/go.sum new file mode 100644 index 000000000..e518d8e54 --- /dev/null +++ b/test/e2e/go.sum @@ -0,0 +1,26 @@ +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/onsi/ginkgo/v2 v2.3.0 h1:kUMoxMoQG3ogk/QWyKh3zibV7BKZ+xBpWil1cTylVqc= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +oras.land/oras-go/v2 v2.0.0-rc.6 h1:jGWysqm8flq+X0Vj8bZ6rkASAqTab5k18Mx9hEjFc8g= +oras.land/oras-go/v2 v2.0.0-rc.6/go.mod h1:iVExH1NxrccIxjsiq17L91WCZ4KIw6jVQyCLsZsu1gc= diff --git a/test/e2e/internal/notation/file.go b/test/e2e/internal/notation/file.go new file mode 100644 index 000000000..48411a004 --- /dev/null +++ b/test/e2e/internal/notation/file.go @@ -0,0 +1,45 @@ +package notation + +import ( + "encoding/json" + "io" + "os" +) + +// copyFile copies the source file to the destination file +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + si, err := in.Stat() + if err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + if err := out.Sync(); err != nil { + return err + } + return out.Chmod(si.Mode()) +} + +// saveJSON marshals the data and save to the given path. +func saveJSON(data any, path string) error { + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + return json.NewEncoder(f).Encode(data) +} diff --git a/test/e2e/internal/notation/host.go b/test/e2e/internal/notation/host.go new file mode 100644 index 000000000..90421c150 --- /dev/null +++ b/test/e2e/internal/notation/host.go @@ -0,0 +1,134 @@ +package notation + +import ( + "os" + "path/filepath" + + "github.com/notaryproject/notation/test/e2e/internal/utils" +) + +// CoreTestFunc is the test function running in a VirtualHost. +// +// notation is an Executor isolated by $XDG_CONFIG_HOME. +// artifact is a generated artifact in a new repository. +// vhost is the VirtualHost instance. +type CoreTestFunc func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) + +// Host creates a virtualized notation testing host by modify +// the "XDG_CONFIG_HOME" environment variable of the Executor. +// +// options is the required testing environment options +// fn is the callback function containing the testing logic. +func Host(options []utils.HostOption, fn CoreTestFunc) { + // create a notation vhost + vhost, err := createNotationHost(NotationBinPath, options...) + if err != nil { + panic(err) + } + + // generate a repository with an artifact + artifact := GenerateArtifact("", "") + + // run the main logic + fn(vhost.Executor, artifact, vhost) +} + +// OldNotation create an old version notation ExecOpts in a VirtualHost +// for testing forward compatibility. +func OldNotation(options ...utils.HostOption) *utils.ExecOpts { + if len(options) == 0 { + options = BaseOptions() + } + + vhost, err := createNotationHost(NotationOldBinPath, options...) + if err != nil { + panic(err) + } + + return vhost.Executor +} + +func createNotationHost(path string, options ...utils.HostOption) (*utils.VirtualHost, error) { + vhost, err := utils.NewVirtualHost(path, CreateNotationDirOption()) + if err != nil { + return nil, err + } + + // set additional options + vhost.SetOption(options...) + return vhost, nil +} + +// Opts is a grammar sugar to generate a list of HostOption. +func Opts(options ...utils.HostOption) []utils.HostOption { + return options +} + +// BaseOptions returns a list of base Options for a valid notation. +// testing environment. +func BaseOptions() []utils.HostOption { + return Opts( + AuthOption("", ""), + AddKeyOption("e2e.key", "e2e.crt"), + AddTrustStoreOption("e2e", filepath.Join(NotationE2ELocalKeysDir, "e2e.crt")), + AddTrustPolicyOption("trustpolicy.json"), + ) +} + +// CreateNotationDirOption creates the notation directory in temp user dir. +func CreateNotationDirOption() utils.HostOption { + return func(vhost *utils.VirtualHost) error { + return os.MkdirAll(vhost.AbsolutePath(NotationDirName), os.ModePerm) + } +} + +// AuthOption sets the auth environment variables for notation. +func AuthOption(username, password string) utils.HostOption { + if username == "" { + username = TestRegistry.Username + } + if password == "" { + password = TestRegistry.Password + } + return func(vhost *utils.VirtualHost) error { + vhost.UpdateEnv(authEnv(username, password)) + return nil + } +} + +// AddKeyOption adds the test signingkeys.json, key and cert files to +// the notation directory. +func AddKeyOption(keyName, certName string) utils.HostOption { + return func(vhost *utils.VirtualHost) error { + return AddTestKeyPairs(vhost.AbsolutePath(NotationDirName), keyName, certName) + } +} + +// AddTrustStoreOption added the test cert to the trust store. +func AddTrustStoreOption(namedstore string, srcCertPath string) utils.HostOption { + return func(vhost *utils.VirtualHost) error { + vhost.Executor. + Exec("cert", "add", "--type", "ca", "--store", namedstore, srcCertPath). + MatchKeyWords("Successfully added following certificates") + return nil + } +} + +// AddTrustPolicyOption added a valid trust policy for testing +func AddTrustPolicyOption(trustpolicyName string) utils.HostOption { + return func(vhost *utils.VirtualHost) error { + return copyFile( + filepath.Join(NotationE2ETrustPolicyDir, trustpolicyName), + vhost.AbsolutePath(NotationDirName, TrustPolicyName), + ) + } +} + +// authEnv creates an auth info +// (By setting $NOTATION_USERNAME and $NOTATION_PASSWORD) +func authEnv(username, password string) map[string]string { + return map[string]string{ + "NOTATION_USERNAME": username, + "NOTATION_PASSWORD": password, + } +} diff --git a/test/e2e/internal/notation/init.go b/test/e2e/internal/notation/init.go new file mode 100644 index 000000000..25bee29c6 --- /dev/null +++ b/test/e2e/internal/notation/init.go @@ -0,0 +1,87 @@ +package notation + +import ( + "fmt" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + NotationDirName = "notation" + TrustPolicyName = "trustpolicy.json" + TrustStoreDirName = "truststore" + TrustStoreTypeCA = "ca" +) + +const ( + envKeyRegistryHost = "NOTATION_E2E_REGISTRY_HOST" + envKeyRegistryUsername = "NOTATION_E2E_REGISTRY_USERNAME" + envKeyRegistryPassword = "NOTATION_E2E_REGISTRY_PASSWORD" + envKeyNotationBinPath = "NOTATION_E2E_BINARY_PATH" + envKeyNotationOldBinPath = "NOTATION_E2E_OLD_BINARY_PATH" + envKeyNotationConfigPath = "NOTATION_E2E_CONFIG_PATH" + envKeyOCILayoutPath = "NOTATION_E2E_OCI_LAYOUT_PATH" + envKeyTestRepo = "NOTATION_E2E_TEST_REPO" + envKeyTestTag = "NOTATION_E2E_TEST_TAG" +) + +var ( + // NotationBinPath is the notation binary path. + NotationBinPath string + // NotationOldBinPath is the path of an old version notation binary for + // testing forward compatibility. + NotationOldBinPath string + NotationE2EConfigPath string + NotationE2ELocalKeysDir string + NotationE2ETrustPolicyDir string +) + +var ( + OCILayoutPath string + TestRepoUri string + TestTag string +) + +func init() { + RegisterFailHandler(Fail) + setUpRegistry() + setUpNotationValues() +} + +func setUpRegistry() { + setValue(envKeyRegistryHost, &TestRegistry.Host) + setValue(envKeyRegistryUsername, &TestRegistry.Username) + setValue(envKeyRegistryPassword, &TestRegistry.Password) + + setPathValue(envKeyOCILayoutPath, &OCILayoutPath) + setValue(envKeyTestRepo, &TestRepoUri) + setValue(envKeyTestTag, &TestTag) +} + +func setUpNotationValues() { + // set Notation binary path + setPathValue(envKeyNotationBinPath, &NotationBinPath) + setPathValue(envKeyNotationOldBinPath, &NotationOldBinPath) + + // set Notation configuration paths + setPathValue(envKeyNotationConfigPath, &NotationE2EConfigPath) + NotationE2ETrustPolicyDir = filepath.Join(NotationE2EConfigPath, "trustpolicys") + NotationE2ELocalKeysDir = filepath.Join(NotationE2EConfigPath, LocalKeysDirName) +} + +func setPathValue(envKey string, value *string) { + setValue(envKey, value) + if !filepath.IsAbs(*value) { + panic(fmt.Sprintf("env %s=%q is not a absolute path", envKey, *value)) + } +} + +func setValue(envKey string, value *string) { + if *value = os.Getenv(envKey); *value == "" { + panic(fmt.Sprintf("env %s is empty", envKey)) + } + fmt.Printf("set test value $%s=%s\n", envKey, *value) +} diff --git a/test/e2e/internal/notation/key.go b/test/e2e/internal/notation/key.go new file mode 100644 index 000000000..6ad2f1831 --- /dev/null +++ b/test/e2e/internal/notation/key.go @@ -0,0 +1,65 @@ +package notation + +import ( + "os" + "path/filepath" +) + +const ( + SigningKeysFileName = "signingkeys.json" + LocalKeysDirName = "localkeys" +) + +// X509KeyPair contains the paths of a public/private key pair files. +type X509KeyPair struct { + KeyPath string `json:"keyPath"` + CertificatePath string `json:"certPath"` +} + +// KeySuite is a named key suite. +type KeySuite struct { + Name string `json:"name"` + *X509KeyPair +} + +// SigningKeys reflects the signingkeys.json file. +type SigningKeys struct { + Default string `json:"default"` + Keys []KeySuite `json:"keys"` +} + +// AddTestKeyPairs creates the signingkeys.json file and the localkeys directory +// with e2e.key and e2e.crt +func AddTestKeyPairs(dir, keyName, certName string) error { + // create signingkeys.json files + if err := saveJSON( + genTestSigningKey(dir), + filepath.Join(dir, SigningKeysFileName)); err != nil { + return err + } + + // create localkeys directory + localKeysDir := filepath.Join(dir, LocalKeysDirName) + os.MkdirAll(localKeysDir, 0700) + + // copy key and cert files + if err := copyFile(filepath.Join(NotationE2ELocalKeysDir, keyName), filepath.Join(localKeysDir, "e2e.key")); err != nil { + return err + } + return copyFile(filepath.Join(NotationE2ELocalKeysDir, certName), filepath.Join(localKeysDir, "e2e.crt")) +} + +func genTestSigningKey(dir string) *SigningKeys { + return &SigningKeys{ + Default: "e2e", + Keys: []KeySuite{ + { + Name: "e2e", + X509KeyPair: &X509KeyPair{ + KeyPath: filepath.Join(dir, "localkeys", "e2e.key"), + CertificatePath: filepath.Join(dir, "localkeys", "e2e.crt"), + }, + }, + }, + } +} diff --git a/test/e2e/internal/notation/registry.go b/test/e2e/internal/notation/registry.go new file mode 100644 index 000000000..b2cdb9bee --- /dev/null +++ b/test/e2e/internal/notation/registry.go @@ -0,0 +1,142 @@ +package notation + +import ( + "context" + "fmt" + "hash/maphash" + "net" + "os" + "path/filepath" + + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/oci" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" +) + +type Registry struct { + Host string + Username string + Password string +} + +// CreateArtifact copies a local OCI layout to the registry to create +// a new artifact with a new repository. +// +// srcRepoName is the repo name in ./testdata/registry/oci_layout folder. +// destRepoName is the repo name to be created in the registry. +func (r *Registry) CreateArtifact(srcRepoName, destRepoName string) (*Artifact, error) { + ctx := context.Background() + // create a local store from OCI layout directory. + srcStore, err := oci.NewFromFS(ctx, os.DirFS(filepath.Join(OCILayoutPath, srcRepoName))) + if err != nil { + return nil, err + } + + // create the artifact struct + artifact := &Artifact{ + Registry: r, + Repo: destRepoName, + Tag: TestTag, + } + + // create the remote.repository + destRepo, err := newRepository(artifact.ReferenceWithTag()) + if err != nil { + return nil, err + } + + // copy data + desc, err := oras.ExtendedCopy(ctx, srcStore, artifact.Tag, destRepo, "", oras.DefaultExtendedCopyOptions) + if err != nil { + return nil, err + } + artifact.Digest = desc.Digest.String() + return artifact, err +} + +var TestRegistry = Registry{} + +// Artifact describes an artifact in a repository. +type Artifact struct { + *Registry + // Repo is the repository name. + Repo string + // Tag is the tag of the artifact. + Tag string + // Digest is the digest of the artifact. + Digest string +} + +// GenerateArtifact generates a new artifact with a new repository by copying +// the source repository in the OCILayoutPath to be a new repository. +func GenerateArtifact(srcRepo, newRepo string) *Artifact { + if srcRepo == "" { + srcRepo = TestRepoUri + } + + if newRepo == "" { + // generate new repo + newRepo = newRepoName() + } + + artifact, err := TestRegistry.CreateArtifact(srcRepo, newRepo) + if err != nil { + panic(err) + } + return artifact +} + +// ReferenceWithTag returns the /: +func (r *Artifact) ReferenceWithTag() string { + return fmt.Sprintf("%s/%s:%s", r.Host, r.Repo, r.Tag) +} + +// ReferenceWithDigest returns the /@: +func (r *Artifact) ReferenceWithDigest() string { + return fmt.Sprintf("%s/%s@%s", r.Host, r.Repo, r.Digest) +} + +func newRepoName() string { + var newRepo string + seed := maphash.MakeSeed() + newRepo = fmt.Sprintf("%s-%d", TestRepoUri, maphash.Bytes(seed, nil)) + return newRepo +} + +func newRepository(reference string) (*remote.Repository, error) { + ref, err := registry.ParseReference(reference) + if err != nil { + return nil, err + } + + repo := &remote.Repository{ + Client: authClient(ref), + Reference: ref, + PlainHTTP: false, + } + if host, _, _ := net.SplitHostPort(ref.Host()); host == "localhost" { + repo.PlainHTTP = true + } + + return repo, nil +} + +func authClient(ref registry.Reference) *auth.Client { + return &auth.Client{ + Credential: func(ctx context.Context, registry string) (auth.Credential, error) { + switch registry { + case ref.Host(): + return auth.Credential{ + Username: TestRegistry.Username, + Password: TestRegistry.Password, + }, nil + default: + return auth.EmptyCredential, nil + } + }, + Cache: auth.NewCache(), + ClientID: "notation", + } +} diff --git a/test/e2e/internal/utils/exec.go b/test/e2e/internal/utils/exec.go new file mode 100644 index 000000000..df1e2ca94 --- /dev/null +++ b/test/e2e/internal/utils/exec.go @@ -0,0 +1,176 @@ +// copied and adopted from https://github.com/oras-project/oras with +// modification +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +const ( + DefaultTimeout = 10 * time.Second + // If the command hasn't exited yet, ginkgo session ExitCode is -1 + notResponding = -1 +) + +// ExecOpts is an option used to execute a command. +type ExecOpts struct { + binPath string + workDir string + timeout time.Duration + + stdin io.Reader + exitCode int + + text string + + // env is the environment variables used by the command. + env map[string]string +} + +// Binary returns default execution option for customized binary. +func Binary(binPath string) *ExecOpts { + return &ExecOpts{ + binPath: binPath, + timeout: DefaultTimeout, + exitCode: 0, + env: make(map[string]string), + } +} + +// ExpectFailure sets failure exit code checking for the execution. +func (opts *ExecOpts) ExpectFailure() *ExecOpts { + // set to 1 but only check if it's positive + opts.exitCode = 1 + return opts +} + +// ExpectBlocking consistently check if the execution is blocked. +func (opts *ExecOpts) ExpectBlocking() *ExecOpts { + opts.exitCode = notResponding + return opts +} + +// WithTimeOut sets timeout for the execution. +func (opts *ExecOpts) WithTimeOut(timeout time.Duration) *ExecOpts { + opts.timeout = timeout + return opts +} + +// WithDescription sets description text for the execution. +func (opts *ExecOpts) WithDescription(text string) *ExecOpts { + opts.text = text + return opts +} + +// WithWorkDir sets working directory for the execution. +func (opts *ExecOpts) WithWorkDir(path string) *ExecOpts { + opts.workDir = path + return opts +} + +// WithInput redirects stdin to r for the execution. +func (opts *ExecOpts) WithInput(r io.Reader) *ExecOpts { + opts.stdin = r + return opts +} + +// WithEnv update the environment variables. +func (opts *ExecOpts) WithEnv(env map[string]string) *ExecOpts { + if env == nil { + return opts + } + if opts.env == nil { + opts.env = make(map[string]string) + } + for key, value := range env { + opts.env[key] = value + } + return opts +} + +// Exec run the execution based on opts. +func (opts *ExecOpts) Exec(args ...string) *Matcher { + if opts == nil { + // this should be a code error but can only be caught during runtime + panic("Nil option for command execution") + } + + if opts.text == "" { + // set default description text + switch opts.exitCode { + case notResponding: + opts.text = "block" + case 0: + opts.text = "pass" + default: + opts.text = "fail" + } + } + description := fmt.Sprintf("\n>> should %s: %s %s >>", opts.text, opts.binPath, strings.Join(args, " ")) + ginkgo.By(description) + + var cmd *exec.Cmd + cmd = exec.Command(opts.binPath, args...) + + // set environment variables + cmd.Env = append(cmd.Env, os.Environ()...) + for key, val := range opts.env { + cmd.Env = append(cmd.Env, fmt.Sprintf("%v=%v", key, val)) + } + + // set stdin + cmd.Stdin = opts.stdin + if opts.workDir != "" { + // switch working directory + wd, err := os.Getwd() + Expect(err).ShouldNot(HaveOccurred()) + Expect(os.Chdir(opts.workDir)).ShouldNot(HaveOccurred()) + defer os.Chdir(wd) + } + fmt.Println(description) + session, err := gexec.Start(cmd, os.Stdout, os.Stderr) + Expect(err).ShouldNot(HaveOccurred()) + if opts.exitCode == notResponding { + Consistently(session.ExitCode).WithTimeout(opts.timeout).Should(Equal(notResponding)) + session.Kill() + } else { + exitCode := session.Wait(opts.timeout).ExitCode() + Expect(opts.exitCode == 0).To(Equal(exitCode == 0)) + } + + // clear ExecOpts state + opts.Clear() + + return NewMatcher(session) +} + +// Clear clears the ExecOpts to get ready for the next execution. +func (opts *ExecOpts) Clear() { + opts.exitCode = 0 + opts.timeout = DefaultTimeout + opts.workDir = "" + opts.stdin = nil + opts.text = "" +} diff --git a/test/e2e/internal/utils/host.go b/test/e2e/internal/utils/host.go new file mode 100644 index 000000000..02e4be598 --- /dev/null +++ b/test/e2e/internal/utils/host.go @@ -0,0 +1,74 @@ +package utils + +import ( + "path/filepath" + + "github.com/onsi/ginkgo/v2" +) + +// VirtualHost is a virtualized host machine isolated by environment variable. +type VirtualHost struct { + Executor *ExecOpts + + userDir string + env map[string]string +} + +// NewVirtualHost creates a temporary user-level directory and updates +// the "XDG_CONFIG_HOME" environment variable for Executor of the VirtualHost. +func NewVirtualHost(binPath string, options ...HostOption) (*VirtualHost, error) { + vhost := &VirtualHost{ + Executor: Binary(binPath), + } + + // setup a temp user directory + vhost.userDir = ginkgo.GinkgoT().TempDir() + + // set user dir environment variables + vhost.UpdateEnv(UserConfigEnv(vhost.userDir)) + + // set options + vhost.SetOption(options...) + return vhost, nil +} + +// AbsolutePath returns the absolute path for the given path +// elements that are relative to the user directory. +func (h *VirtualHost) AbsolutePath(elem ...string) string { + userElem := []string{h.userDir} + userElem = append(userElem, elem...) + return filepath.Join(userElem...) +} + +// UpdateEnv updates the environment variables for the VirtualHost. +func (h *VirtualHost) UpdateEnv(env map[string]string) { + if h.env == nil { + h.env = make(map[string]string) + } + for key, value := range env { + h.env[key] = value + } + // update ExecOpts.env + h.Executor.WithEnv(h.env) +} + +// SetOption sets the options for the host. +func (h *VirtualHost) SetOption(options ...HostOption) { + for _, option := range options { + if err := option(h); err != nil { + panic(err) + } + } +} + +// HostOption is a function to set the host configuration. +type HostOption func(vhost *VirtualHost) error + +// UserConfigEnv creates environment variable for changing +// user config dir (By setting $XDG_CONFIG_HOME). +func UserConfigEnv(dir string) map[string]string { + // create and set user dir for linux + return map[string]string{ + "XDG_CONFIG_HOME": dir, + } +} diff --git a/test/e2e/internal/utils/matcher.go b/test/e2e/internal/utils/matcher.go new file mode 100644 index 000000000..103d8d9dc --- /dev/null +++ b/test/e2e/internal/utils/matcher.go @@ -0,0 +1,65 @@ +package utils + +import ( + "fmt" + "strings" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +// Matcher contains the execution result for matching. +type Matcher struct { + Session *gexec.Session + stdout string + stderr string +} + +// NewMatcher returns a new Matcher. +func NewMatcher(session *gexec.Session) *Matcher { + return &Matcher{ + Session: session, + stdout: string(session.Out.Contents()), + stderr: string(session.Err.Contents()), + } +} + +// MatchContent matches the content with the stdout. +func (m *Matcher) MatchContent(content string) *Matcher { + Expect(m.stdout).Should(Equal(content)) + return m +} + +// MatchErrContent matches the content with stderr. +func (m *Matcher) MatchErrContent(content string) *Matcher { + Expect(m.stderr).Should(Equal(content)) + return m +} + +// MatchKeyWords matches given keywords with the stdout. +func (m *Matcher) MatchKeyWords(keywords ...string) *Matcher { + matchKeyWords(m.stdout, keywords) + return m +} + +// MatchErrKeyWords matches given keywords with the stderr. +func (m *Matcher) MatchErrKeyWords(keywords ...string) *Matcher { + matchKeyWords(m.stderr, keywords) + return m +} + +// MatchErrKeyWords matches given keywords with the stderr. +func matchKeyWords(content string, keywords []string) { + var missed []string + lowered := strings.ToLower(content) + for _, w := range keywords { + if !strings.Contains(lowered, strings.ToLower(w)) { + missed = append(missed, w) + } + } + + if len(missed) != 0 { + fmt.Printf("Keywords missed: %v\n", missed) + panic("failed to match all keywords") + } +} diff --git a/test/e2e/internal/utils/validator/validator.go b/test/e2e/internal/utils/validator/validator.go new file mode 100644 index 000000000..e3e1932f2 --- /dev/null +++ b/test/e2e/internal/utils/validator/validator.go @@ -0,0 +1,20 @@ +package validator + +import ( + "os" + + . "github.com/onsi/gomega" +) + +// CheckFileExist checks file exists. +func CheckFileExist(f string) { + _, err := os.Stat(f) + Expect(err).ShouldNot(HaveOccurred()) +} + +// CheckFileNotExist checks file not exist. +func CheckFileNotExist(f string) { + _, err := os.Stat(f) + Expect(err).Should(HaveOccurred()) + Expect(os.IsNotExist(err)).To(BeTrue()) +} diff --git a/test/e2e/run.sh b/test/e2e/run.sh new file mode 100755 index 000000000..897e4828f --- /dev/null +++ b/test/e2e/run.sh @@ -0,0 +1,94 @@ +#!/bin/bash -e + +SUPPORTED_REGISTRY=("zot" "dockerhub") + +function help { + echo "Usage" + echo " run.sh [old-notation-binary-path]" + echo "" + echo "Arguments" + echo " registry-name is the registry to use for running the E2E test. Currently support: ${SUPPORTED_REGISTRY[@]}." + echo " notation-binary-path is the path of the notation executable binary file." + echo " old-notation-binary-path is the path of an old notation executable bianry file. If it is not set, an RC.1 Notation will be downloaded automatically." +} + +# check registry name +REGISTRY_NAME=$1 +if [ -z "$REGISTRY_NAME" ]; then + echo "registry name is missing." + help + exit 1 +fi + +# check notation binary path. +export NOTATION_E2E_BINARY_PATH=$(if [ ! -z "$2" ]; then realpath $2; fi) +if [ ! -f "$NOTATION_E2E_BINARY_PATH" ];then + echo "notation binary path doesn't exist." + help + exit 1 +fi + +# check old notation binary path for forward compatibility test. +export NOTATION_E2E_OLD_BINARY_PATH=$(if [ ! -z "$3" ]; then realpath $3; fi) +if [ ! -f "$NOTATION_E2E_OLD_BINARY_PATH" ];then + OLD_NOTATION_DIR=/tmp/notation_old + export NOTATION_E2E_OLD_BINARY_PATH=$OLD_NOTATION_DIR/notation + mkdir -p $OLD_NOTATION_DIR + + echo "Old notation binary path doesn't exist." + echo "Try to use old notation binary at $NOTATION_E2E_OLD_BINARY_PATH" + + if [ ! -f $NOTATION_E2E_OLD_BINARY_PATH ]; then + TAG=1.0.0-rc.1 # without 'v' + echo "Didn't find old notation binary locally. Try to download notation v$TAG." + + TAR_NAME=notation_${TAG}_linux_amd64.tar.gz + URL=https://github.com/notaryproject/notation/releases/download/v${TAG}/$TAR_NAME + wget $URL -P $OLD_NOTATION_DIR + tar -xf $OLD_NOTATION_DIR/$TAR_NAME -C $OLD_NOTATION_DIR + + if [ ! -f $NOTATION_E2E_OLD_BINARY_PATH ]; then + echo "Failed to download old notation binary for forward compatibility test." + exit 1 + fi + echo "Downloaded notation v$TAG at $NOTATION_E2E_OLD_BINARY_PATH" + fi +fi + +# install dependency +go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.3.0 + +# setup registry +case $REGISTRY_NAME in + + "zot") + source ./scripts/zot.sh + ;; + + "dockerhub") + source ./scripts/dockerhub.sh + ;; + + *) + echo "invalid registry" + help + exit 1 + ;; +esac + +setup_registry + +# defer cleanup registry +function cleanup { + cleanup_registry +} +trap cleanup EXIT + +# set environment variable for E2E testing +export NOTATION_E2E_CONFIG_PATH=`pwd`/testdata/config +export NOTATION_E2E_OCI_LAYOUT_PATH=`pwd`/testdata/registry/oci_layout +export NOTATION_E2E_TEST_REPO=e2e +export NOTATION_E2E_TEST_TAG=v1 + +# run tests +ginkgo -r -p -v diff --git a/test/e2e/scripts/dockerhub.sh b/test/e2e/scripts/dockerhub.sh new file mode 100644 index 000000000..6a0814678 --- /dev/null +++ b/test/e2e/scripts/dockerhub.sh @@ -0,0 +1,58 @@ +#!/bin/bash -e +# this script called by ../run.sh +# +# DockerHub has image pulling limit and request rate limit. Please improve your +# subscription level if you confronts those issues. +# +# Usage +# export DOCKER_USERNAME=xxx +# export DOCKER_PASSOWRD=xxx +# ./run.sh dockerhub [old-notation-binary-path] + +# check required environment variable +if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_PASSWORD" ]; then + echo "\$DOCKER_USERNAME or \$DOCKER_PASSWORD is not set" + exit 1 +fi + +# set environment variables for E2E testing +export NOTATION_E2E_REGISTRY_HOST=docker.io/$DOCKER_USERNAME +export NOTATION_E2E_REGISTRY_USERNAME=$DOCKER_USERNAME +export NOTATION_E2E_REGISTRY_PASSWORD=$DOCKER_PASSWORD + +function setup_registry { + echo "use $NOTATION_E2E_REGISTRY_HOST" +} + +function cleanup_registry { + echo "cleaning dockerhub" + # get token + # reference: https://docs.docker.com/docker-hub/api/latest/#tag/authentication + HUB_TOKEN=$(curl -s -H "Content-Type: application/json" -X POST -d "{\"username\": \"$DOCKER_USERNAME\", \"password\": \"$DOCKER_PASSWORD\"}" https://hub.docker.com/v2/users/login/ | jq -r .token) + + for (( page=1;;page++ )); do + # page query the repositorys' name + resp=`curl -s -X GET \ + -H "Accept: application/json" \ + -H "Authorization: JWT $HUB_TOKEN" \ + "https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/?page_size=100&&page=$page"` + + # check the last page + if [[ "$resp" == *"object not found"* ]]; then + break + fi + + # parse json and extract e2e repoName + e2eRepos=(`echo $resp | jq -r '.results|.[]|.name' | grep 'e2e-'`) + echo "repositories: ${e2eRepos[@]}" + + for repoName in "${e2eRepos[@]}"; do + # run delete + curl -X DELETE \ + -H "Accept: application/json" \ + -H "Authorization: JWT $HUB_TOKEN" \ + https://hub.docker.com/v2/repositories/$DOCKER_USERNAME/$repoName/ && \ + echo "$NOTATION_E2E_REGISTRY_HOST/$repoName deleted." + done + done +} \ No newline at end of file diff --git a/test/e2e/scripts/zot.sh b/test/e2e/scripts/zot.sh new file mode 100644 index 000000000..e0c9f9c18 --- /dev/null +++ b/test/e2e/scripts/zot.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e +# this script called by ../run.sh +# +# Usage +# ./run.sh zot [old-notation-binary-path] + +REG_HOST=localhost +REG_PORT=5000 +ZOT_CONTAINER_NAME=zot + +# set environment variables for E2E testing +export NOTATION_E2E_REGISTRY_HOST=$REG_HOST:$REG_PORT +export NOTATION_E2E_REGISTRY_USERNAME=testuser +export NOTATION_E2E_REGISTRY_PASSWORD=testpassword + +function setup_registry { + # start zot + docker run -d -p $REG_PORT:$REG_PORT -it --name $ZOT_CONTAINER_NAME \ + --mount type=bind,source=`pwd`/testdata/registry/zot/,target=/etc/zot \ + --rm ghcr.io/project-zot/zot-minimal-linux-amd64:latest +} + +function cleanup_registry { + docker container stop $ZOT_CONTAINER_NAME 1>/dev/null && echo "Zot stopped" +} \ No newline at end of file diff --git a/test/e2e/suite/command/command_test.go b/test/e2e/suite/command/command_test.go new file mode 100644 index 000000000..ed143e223 --- /dev/null +++ b/test/e2e/suite/command/command_test.go @@ -0,0 +1,13 @@ +package command + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Command Suite") +} diff --git a/test/e2e/suite/command/sign.go b/test/e2e/suite/command/sign.go new file mode 100644 index 000000000..3f40921df --- /dev/null +++ b/test/e2e/suite/command/sign.go @@ -0,0 +1,29 @@ +package command + +import ( + . "github.com/notaryproject/notation/test/e2e/internal/notation" + "github.com/notaryproject/notation/test/e2e/internal/utils" + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("notation sign", func() { + It("with JWS signature format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.ReferenceWithTag(), "--signature-format", "jws"). + MatchKeyWords("Successfully signed") + + OldNotation().Exec("verify", artifact.ReferenceWithTag()). + MatchKeyWords("Successfully verified") + }) + }) + + It("with COSE signature format", func() { + Host(BaseOptions(), func(notation *utils.ExecOpts, artifact *Artifact, vhost *utils.VirtualHost) { + notation.Exec("sign", artifact.ReferenceWithTag(), "--signature-format", "cose"). + MatchKeyWords("Successfully signed") + + OldNotation().Exec("verify", artifact.ReferenceWithTag()). + MatchKeyWords("Successfully verified") + }) + }) +}) diff --git a/test/e2e/suite/scenario/scenario_test.go b/test/e2e/suite/scenario/scenario_test.go new file mode 100644 index 000000000..52f77cfd9 --- /dev/null +++ b/test/e2e/suite/scenario/scenario_test.go @@ -0,0 +1,13 @@ +package scenario_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestScenario(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Scenario Suite") +} diff --git a/test/e2e/testdata/config/localkeys/e2e.crt b/test/e2e/testdata/config/localkeys/e2e.crt new file mode 100644 index 000000000..c148e07c0 --- /dev/null +++ b/test/e2e/testdata/config/localkeys/e2e.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOjCCAiKgAwIBAgIBUTANBgkqhkiG9w0BAQsFADBLMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEM +MAoGA1UEAxMDZTJlMCAXDTIyMTIxOTAyNDMzOVoYDzIxMjIxMjE5MDI0MzM5WjBL +MQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzAN +BgNVBAoTBk5vdGFyeTEMMAoGA1UEAxMDZTJlMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAwNrKIzpESDkY7Kah+sWKeXDNE7yUBaqW58cyMtkpk9/Q6QLb +ndMxdt8VjD3y4bSBhZXLdUkW0S4hSNZXQwkYm3yooxJXj+uOI9NnEPt/gAkPYri4 +TdKpTFSBzE4uJ0HrYDBUir3Dars/CXiusmdCnSWKsgm05ItkHdHEGtor716aWdnG +uFNvyzJyaC3XLFpo1OwEwTyxf4Yix4UtvwNDB4TOJRH84avSoFCua8xbRpiBJ0Qo +X6zY0Yr9qbvMBQIcmpQ3uWOiEeVMDjRE0XzhiWSevVfZTPJcYpsAyBTJGJLIcHU7 +JMYnwKP6IzUg5T589lvRVD7up4sAC7izOKGzPQIDAQABoycwJTAOBgNVHQ8BAf8E +BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAKac +aJ+O9rTIdfLHCdneLQ6RN92Id5dl8PGh/Xig3AWSOtLgIiRRsdxVH5PZkGOHosel +lQG5fCvQOwB0x8O+2YDKLOfgVIJWd6X85NdvyCdX2ElYRmvX9ON5WVguGLluwkOf +J26M8d8ftXrcc97qaKk4EHS8R/LCWqZNDRiRCA0OtNP9cUkKFaIG1hgWgEieVWnx +rCyUDbTX3uCiJKNzSOmI3psF3WIhabU7/7gBm95nWhgwS91qAxavccVkY6hqAPj9 +tjJH5UPI5RR8kDB5rWiCIx1YHuH+z1eAYUHWVvZvneVniNBI8qGoGBz9HkrX5ecf ++V7zaxyy0FoWlEX1z8k= +-----END CERTIFICATE----- diff --git a/test/e2e/testdata/config/localkeys/e2e.key b/test/e2e/testdata/config/localkeys/e2e.key new file mode 100644 index 000000000..5dde4f605 --- /dev/null +++ b/test/e2e/testdata/config/localkeys/e2e.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDA2sojOkRIORjs +pqH6xYp5cM0TvJQFqpbnxzIy2SmT39DpAtud0zF23xWMPfLhtIGFlct1SRbRLiFI +1ldDCRibfKijEleP644j02cQ+3+ACQ9iuLhN0qlMVIHMTi4nQetgMFSKvcNquz8J +eK6yZ0KdJYqyCbTki2Qd0cQa2ivvXppZ2ca4U2/LMnJoLdcsWmjU7ATBPLF/hiLH +hS2/A0MHhM4lEfzhq9KgUK5rzFtGmIEnRChfrNjRiv2pu8wFAhyalDe5Y6IR5UwO +NETRfOGJZJ69V9lM8lximwDIFMkYkshwdTskxifAo/ojNSDlPnz2W9FUPu6niwAL +uLM4obM9AgMBAAECggEAOmFA9Z8K0o4uRF1BBYfNHmwOOJ5KdNiqK+m6AXiJxJDp +TdAmqUUoYSKxBC4wmzCoUMcCuzNv68y6GLGB4vIa996psgu4ZAHbmm7BcXugoiKb +/LeMW0qdI1UlobI8+HdVCF45CXLeeC3MrFJTGAB4Qtf9f12+27xyhzBb3AEVcbU8 +HbYEAX5i8yxHekkm6P6Tbvl0sN2WRd7iGzB32ozVNCBJjXRuP78DrTGwLFUpXgoE +0FGTBAiOiHvSSU2SOEAo0OJYNjgDUatYHGgXUpVLa0Xwl8hTj3+GzxsYGVPOHlD4 +ylpGL+O1zwpeDbEsCKwdLzdw3zy1xpCdQLIvnCKuRQKBgQDbwNYU3OtPZcIva3NZ +Y3uxovLRZcabvQ8ygO6ZWr64ci4Tso+R/tXNwHyjTfAXHYHnZK+Zecj7gMZocg+1 +LBmzzBoevuby25v/D2ShfbtRbQn8LPtsfGo2EWi8WpHOVHUVsfdo5Nf3Mr5Ru5vj +Q5t8GpuEZfEJTGJ9A+mmwl1WkwKBgQDgqiYqJ0AroNhSsmTfIS2G5tyFBZHJr3SG +niwb9aWYNdtOj7K/EZVW4DKjEooV2IzaiH61JnDYTusGyGq2IpI5Orm5sGCPV/T2 +v8KYffuG8RHfLU/aLVx3Uc3YwJ0kkrS1/d/e8e2FxlLPfWCfQFBUfz+EyHMfmcNI +hTP3SFag7wKBgBLUw4OmKsPzBGDr5EaUr9TZEdW/0vUljlfVlJyvmghUQH6Pnp30 +KH4pMIUN+LUCYk9h4WpVdVOYBWiN3aq4zLsLknFwCnplN0j0GLt9+r9PiLuDX0g8 +oR1hAIijejskIaRqS7lBYwBb6NM4MHOZJ4nK/eiqk60oTohH/Y28uiLhAoGALqxK +d3FmDGpC5pM3D3/GBIOLhnsMuXig3aKYiUp0F3YA1IZX3QfbHYxAGM6uLzGouXGn +2RxeTyG+A8+5E5OFCyyfDuuMc9sTAfv+gk2R5ovIabPxJeNMlWFCQWhqfQGZV0Gh +s6BQ9vynkYF7hDoJNjSlToGSIRuBjVxW2mWF0bMCgYAtdYBkuKp89hIbP1EMJelt +Ig4h6hQcywQd5jRkQXMp9RyVZ2ypvR4fA+aM0MQE6mVPa6BEXNzm1myGFN1Ob0LM +JvfMJyjKxHcGIcJEx64wtHMoZF8n/6JSt2CkHfSk3jjZlW+U/Mu1A07JbIKya8Dg +GLuZnP4KhavycZfHMENsXg== +-----END PRIVATE KEY----- diff --git a/test/e2e/testdata/config/trustpolicys/trustpolicy.json b/test/e2e/testdata/config/trustpolicys/trustpolicy.json new file mode 100644 index 000000000..e886d6d32 --- /dev/null +++ b/test/e2e/testdata/config/trustpolicys/trustpolicy.json @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "trustPolicies": [ + { + "name": "e2e", + "registryScopes": [ "*" ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:e2e" ], + "trustedIdentities": [ + "*" + ] + } + ] +} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 new file mode 100644 index 000000000..765bc38c9 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 @@ -0,0 +1 @@ +Awesome Notation diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/273243a7a64e9312761ca0aa8f43b6ba805e677a561558143b6e92981c487339 b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/273243a7a64e9312761ca0aa8f43b6ba805e677a561558143b6e92981c487339 new file mode 100644 index 000000000..f9aebfb13 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/273243a7a64e9312761ca0aa8f43b6ba805e677a561558143b6e92981c487339 @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"application/vnd.cncf.notary.signature","blobs":[{"mediaType":"application/jose+json","digest":"sha256:a10cfe44db01fffab281c3f1077d7663cdda573940d89fdb10b750788250d775","size":2074}],"subject":{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f","size":417},"annotations":{"io.cncf.notary.x509chain.thumbprint#S256":"[\"e38aea13f2b051f7183cca64a51ee676f4d4456f41d9fecf15eb367efc7343b9\"]","org.opencontainers.artifact.created":"2022-12-30T03:11:19Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/a10cfe44db01fffab281c3f1077d7663cdda573940d89fdb10b750788250d775 b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/a10cfe44db01fffab281c3f1077d7663cdda573940d89fdb10b750788250d775 new file mode 100644 index 000000000..cd054b936 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/a10cfe44db01fffab281c3f1077d7663cdda573940d89fdb10b750788250d775 @@ -0,0 +1 @@ +{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6Y2MyYWU0ZTkxYTMxYTc3MDg2ZWRiZGJmNDcxMWRlNDhlNWZhM2ViZGFjYWQzNDAzZTYxNzc3YTllMWE1M2I2ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5vY2kuYXJ0aWZhY3QubWFuaWZlc3QudjEranNvbiIsInNpemUiOjQxN319","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIyLTEyLTMwVDExOjExOjE5KzA4OjAwIn0","header":{"x5c":["MIIDOjCCAiKgAwIBAgIBUTANBgkqhkiG9w0BAQsFADBLMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEMMAoGA1UEAxMDZTJlMCAXDTIyMTIxOTAyNDMzOVoYDzIxMjIxMjE5MDI0MzM5WjBLMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEMMAoGA1UEAxMDZTJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwNrKIzpESDkY7Kah+sWKeXDNE7yUBaqW58cyMtkpk9/Q6QLbndMxdt8VjD3y4bSBhZXLdUkW0S4hSNZXQwkYm3yooxJXj+uOI9NnEPt/gAkPYri4TdKpTFSBzE4uJ0HrYDBUir3Dars/CXiusmdCnSWKsgm05ItkHdHEGtor716aWdnGuFNvyzJyaC3XLFpo1OwEwTyxf4Yix4UtvwNDB4TOJRH84avSoFCua8xbRpiBJ0QoX6zY0Yr9qbvMBQIcmpQ3uWOiEeVMDjRE0XzhiWSevVfZTPJcYpsAyBTJGJLIcHU7JMYnwKP6IzUg5T589lvRVD7up4sAC7izOKGzPQIDAQABoycwJTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAKacaJ+O9rTIdfLHCdneLQ6RN92Id5dl8PGh/Xig3AWSOtLgIiRRsdxVH5PZkGOHosellQG5fCvQOwB0x8O+2YDKLOfgVIJWd6X85NdvyCdX2ElYRmvX9ON5WVguGLluwkOfJ26M8d8ftXrcc97qaKk4EHS8R/LCWqZNDRiRCA0OtNP9cUkKFaIG1hgWgEieVWnxrCyUDbTX3uCiJKNzSOmI3psF3WIhabU7/7gBm95nWhgwS91qAxavccVkY6hqAPj9tjJH5UPI5RR8kDB5rWiCIx1YHuH+z1eAYUHWVvZvneVniNBI8qGoGBz9HkrX5ecf+V7zaxyy0FoWlEX1z8k="],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"sgygIQ3vLgM8XGUm_iM1hEogNE53TgtH91mgqyRRmkUnj93rUCEcrUN2u1gDFvKe0Z6-zI-I60Z95HloOngZ1nSu2Nyezx5evWBDS-stYZ9m0jyzGKOruTXDY-a0V4Jk1louK3t5LrcSJ2tt_lY6SOvZ__5Gv3KQQDBBluA_R74F-m-OhosRA60mS4dkAHOePOiDIwIFUUZWKYF9Afrqv-uAsQtfH70RCisjLLIgFDYtmCIUd2rP7ZzrybLj2CtHLuflSfh6QK0UBqhJSH8FSTWcXUE263plfIbYx-mZ6KPNOOYmSsLLgYUhdc9dSZk56BVi7ZF_X6CefzIsw9_adA"} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f new file mode 100644 index 000000000..45028368d --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"application/vnd.notation.config","blobs":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578","size":17,"annotations":{"org.opencontainers.image.title":"awesome-notation.txt"}}],"annotations":{"org.opencontainers.artifact.created":"2022-12-22T01:11:12Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/index.json b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/index.json new file mode 100644 index 000000000..f39913580 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f","size":417,"annotations":{"org.opencontainers.image.ref.name":"v1"}},{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:273243a7a64e9312761ca0aa8f43b6ba805e677a561558143b6e92981c487339","size":618}]} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/oci-layout b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/oci-layout new file mode 100644 index 000000000..1343d370f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e-valid-signature/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 b/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 new file mode 100644 index 000000000..765bc38c9 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578 @@ -0,0 +1 @@ +Awesome Notation diff --git a/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f b/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f new file mode 100644 index 000000000..45028368d --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e/blobs/sha256/cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"application/vnd.notation.config","blobs":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:0d10372ee4448bf319929720bc465c1a6242cc68e82e12c152bc10d541cce578","size":17,"annotations":{"org.opencontainers.image.title":"awesome-notation.txt"}}],"annotations":{"org.opencontainers.artifact.created":"2022-12-22T01:11:12Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e/index.json b/test/e2e/testdata/registry/oci_layout/e2e/index.json new file mode 100644 index 000000000..430948531 --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:cc2ae4e91a31a77086edbdbf4711de48e5fa3ebdacad3403e61777a9e1a53b6f","size":417,"annotations":{"org.opencontainers.image.ref.name":"v1"}}]} \ No newline at end of file diff --git a/test/e2e/testdata/registry/oci_layout/e2e/oci-layout b/test/e2e/testdata/registry/oci_layout/e2e/oci-layout new file mode 100644 index 000000000..1343d370f --- /dev/null +++ b/test/e2e/testdata/registry/oci_layout/e2e/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion":"1.0.0"} \ No newline at end of file diff --git a/test/e2e/testdata/registry/zot/config.json b/test/e2e/testdata/registry/zot/config.json new file mode 100644 index 000000000..fcb3e75ff --- /dev/null +++ b/test/e2e/testdata/registry/zot/config.json @@ -0,0 +1,17 @@ +{ + "storage": { + "rootDirectory": "/var/lib/registry" + }, + "http": { + "address": "0.0.0.0", + "port": "5000", + "auth": { + "htpasswd": { + "path": "/etc/zot/htpasswd" + } + } + }, + "log": { + "level": "debug" + } +} \ No newline at end of file diff --git a/test/e2e/testdata/registry/zot/htpasswd b/test/e2e/testdata/registry/zot/htpasswd new file mode 100644 index 000000000..d58df4ed6 --- /dev/null +++ b/test/e2e/testdata/registry/zot/htpasswd @@ -0,0 +1,2 @@ +testuser:$2y$05$E8rldH.g4uS278rwECRHBOxkbrgdkthdMa9vHqOcS525QxRCgYrS2 +