diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index afc94b6b7..65d8512e1 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -11,7 +11,7 @@ blocks: task: env_vars: - name: GIMME_GO_VERSION - value: "1.14.2" + value: "1.16.3" jobs: - name: Tests commands: @@ -32,4 +32,5 @@ blocks: - make ignite ignite-spawn ignited bin/amd64/Dockerfile GO_MAKE_TARGET=local - make test - make root-test + - bash e2e/util/setup-private-registry.sh # This is required for registry auth e2e tests. - make e2e-nobuild # this depends on Semaphore CI's support for nested virtualization diff --git a/e2e/registry_auth_test.go b/e2e/registry_auth_test.go new file mode 100644 index 000000000..046e8ee6d --- /dev/null +++ b/e2e/registry_auth_test.go @@ -0,0 +1,201 @@ +package e2e + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/assert" + + "github.com/weaveworks/ignite/e2e/util" + "github.com/weaveworks/ignite/pkg/runtime" +) + +const ( + testOSImage = "localhost:5000/weaveworks/ignite-ubuntu:test" + testKernelImage = "localhost:5000/weaveworks/ignite-kernel:test" +) + +// client config with auth info for the registry setup in +// e2e/util/setup-private-registry.sh. +// NOTE: Update the auth token if the credentials in setup-private-registry.sh +// is updated. +const clientConfigContent = ` +{ + "auths": { + "localhost:5000": { + "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + } +} +` + +func TestPullFromAuthRegistry(t *testing.T) { + assert.Assert(t, e2eHome != "", "IGNITE_E2E_HOME should be set") + + // Create a client config directory to use in test. + ccDir, err := ioutil.TempDir("", "ignite-test") + assert.NilError(t, err) + defer os.RemoveAll(ccDir) + + templateConfig := `--- +apiVersion: ignite.weave.works/v1alpha4 +kind: Configuration +metadata: + name: test-config +spec: + clientConfigDir: %s +` + igniteConfigContent := fmt.Sprintf(templateConfig, ccDir) + + cases := []struct { + name string + runtime runtime.Name + configWithAuthPath string + clientConfigFlag string + igniteConfig string + wantErr bool + }{ + { + name: "no auth info - containerd", + runtime: runtime.RuntimeContainerd, + wantErr: true, + }, + { + name: "no auth info - docker", + runtime: runtime.RuntimeDocker, + wantErr: true, + }, + { + name: "client config flag - containerd", + runtime: runtime.RuntimeContainerd, + configWithAuthPath: ccDir, + clientConfigFlag: ccDir, + }, + { + name: "client config flag - docker", + runtime: runtime.RuntimeDocker, + configWithAuthPath: ccDir, + clientConfigFlag: ccDir, + }, + { + name: "client config in ignite config - containerd", + runtime: runtime.RuntimeContainerd, + configWithAuthPath: ccDir, + igniteConfig: igniteConfigContent, + }, + { + name: "client config in ignite config - docker", + runtime: runtime.RuntimeDocker, + configWithAuthPath: ccDir, + igniteConfig: igniteConfigContent, + }, + // Following sets the client config dir to a location without a valid + // client config file, although the client config dir in the ignite + // config is correct, the import fails due to bad configuration by the + // flag override. + { + name: "flag override client config - containerd", + runtime: runtime.RuntimeContainerd, + configWithAuthPath: ccDir, + clientConfigFlag: "/tmp", + igniteConfig: igniteConfigContent, + wantErr: true, + }, + { + name: "flag override client config - docker", + runtime: runtime.RuntimeDocker, + configWithAuthPath: ccDir, + clientConfigFlag: "/tmp", + igniteConfig: igniteConfigContent, + wantErr: true, + }, + // Following set the client config dir via flag without any actual + // client config. Import fails due to missing auth info in the given + // client config dir. + { + name: "invalid client config - containerd", + runtime: runtime.RuntimeContainerd, + configWithAuthPath: "", + clientConfigFlag: ccDir, + wantErr: true, + }, + { + name: "invalid client config - docker", + runtime: runtime.RuntimeDocker, + configWithAuthPath: "", + clientConfigFlag: ccDir, + wantErr: true, + }, + } + + for _, rt := range cases { + rt := rt + t.Run(rt.name, func(t *testing.T) { + igniteCmd := util.NewCommand(t, igniteBin) + + // Remove images from ignite store and runtime store. Remove + // individually because an error in deleting one image cancels the + // whole command. + // TODO: Improve image rm to not fail completely when there are + // multiple images and some are not found. + util.RmiCompletely(testOSImage, igniteCmd, rt.runtime) + util.RmiCompletely(testKernelImage, igniteCmd, rt.runtime) + + // Write client config if given. + if len(rt.configWithAuthPath) > 0 { + // Ensure the directory exists and create a config file in the + // directory. + assert.NilError(t, os.MkdirAll(rt.configWithAuthPath, 0755)) + configPath := filepath.Join(rt.configWithAuthPath, "config.json") + assert.NilError(t, os.WriteFile(configPath, []byte(clientConfigContent), 0600)) + defer os.Remove(configPath) + } + + // Write ignite config if provided. + var igniteConfigPath string + if len(rt.igniteConfig) > 0 { + igniteFile, err := ioutil.TempFile("", "ignite-config-file-test") + if err != nil { + t.Fatalf("failed to create a file: %v", err) + } + igniteConfigPath = igniteFile.Name() + + _, err = igniteFile.WriteString(rt.igniteConfig) + assert.NilError(t, err) + assert.NilError(t, igniteFile.Close()) + + defer os.Remove(igniteFile.Name()) + } + + // Construct the ignite image import command. + imageImportCmdArgs := []string{"--runtime", rt.runtime.String()} + if len(rt.clientConfigFlag) > 0 { + imageImportCmdArgs = append(imageImportCmdArgs, "--client-config-dir", rt.clientConfigFlag) + } + if len(igniteConfigPath) > 0 { + imageImportCmdArgs = append(imageImportCmdArgs, "--ignite-config", igniteConfigPath) + } + + // Run image import. + _, importErr := igniteCmd.New(). + With("image", "import", testOSImage). + With(imageImportCmdArgs...). + Cmd.CombinedOutput() + if (importErr != nil) != rt.wantErr { + t.Error("expected OS image import to fail") + } + + // Run kernel import. + _, importErr = igniteCmd.New(). + With("image", "import", testKernelImage). + With(imageImportCmdArgs...). + Cmd.CombinedOutput() + if (importErr != nil) != rt.wantErr { + t.Error("expected kernel image import to fail") + } + }) + } +} diff --git a/e2e/util/image.go b/e2e/util/image.go new file mode 100644 index 000000000..e48f409a3 --- /dev/null +++ b/e2e/util/image.go @@ -0,0 +1,40 @@ +package util + +import ( + "os/exec" + + "github.com/weaveworks/ignite/pkg/runtime" +) + +// RmiDocker removes an image from docker content store. +func RmiDocker(img string) { + _, _ = exec.Command( + "docker", + "rmi", img, + ).CombinedOutput() +} + +// RmiContainerd removes an image from containerd content store. +func RmiContainerd(img string) { + _, _ = exec.Command( + "ctr", "-n", "firecracker", + "image", "rm", img, + ).CombinedOutput() +} + +// rmiCompletely removes a given image completely, from ignite image store and +// runtime image store. +func RmiCompletely(img string, cmd *Command, rt runtime.Name) { + // Remote from ignite content store. + _, _ = cmd.New(). + With("image", "rm", img). + Cmd.CombinedOutput() + + // Remove from runtime content store. + switch rt { + case runtime.RuntimeContainerd: + RmiContainerd(img) + case runtime.RuntimeDocker: + RmiDocker(img) + } +} diff --git a/e2e/util/setup-private-registry.sh b/e2e/util/setup-private-registry.sh new file mode 100644 index 000000000..7d1fcf13c --- /dev/null +++ b/e2e/util/setup-private-registry.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +# This script runs a local private docker registry with self-signed certificate +# and basic auth. + +REGISTRY_SECRET_PATH=/tmp/ignite-test-registry +PRIVATE_KEY=${REGISTRY_SECRET_PATH}/certs/domain.key +CERT=${REGISTRY_SECRET_PATH}/certs/domain.crt +HTPASSWD=${REGISTRY_SECRET_PATH}/auth/htpasswd +USERNAME=testuser +PASSWORD=testpassword +REGISTRY_ADDRESS=https://localhost:5000 +OS_IMG=weaveworks/ignite-ubuntu:latest +LOCAL_OS_IMG=localhost:5000/weaveworks/ignite-ubuntu:test +KERNEL_IMG=weaveworks/ignite-kernel:5.4.108 +LOCAL_KERNEL_IMG=localhost:5000/weaveworks/ignite-kernel:test + +# Clear any existing registry secret and create new directories. +rm -rf ${REGISTRY_SECRET_PATH} +mkdir -p ${REGISTRY_SECRET_PATH}/{certs,auth} + +# Generate key and cert. +openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ + -subj "/C=US/ST=Foo/L=Bar/O=Weave" \ + -keyout ${PRIVATE_KEY} -out ${CERT} +chmod 400 ${PRIVATE_KEY} + +# Create htpasswd file. +docker run --rm \ + --entrypoint htpasswd \ + httpd:2 -Bbn ${USERNAME} ${PASSWORD} > ${HTPASSWD} + +# Run the registry. +docker run -d --rm \ + --name registry \ + -v ${REGISTRY_SECRET_PATH}/auth:/auth \ + -v ${REGISTRY_SECRET_PATH}/certs:/certs \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \ + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ + -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \ + -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \ + -p 5000:5000 \ + registry:2 + +# Login, push test images to download in tests and logout. +docker login -u ${USERNAME} -p ${PASSWORD} ${REGISTRY_ADDRESS} +docker pull ${OS_IMG} +docker pull ${KERNEL_IMG} +docker tag ${OS_IMG} ${LOCAL_OS_IMG} +docker tag ${KERNEL_IMG} ${LOCAL_KERNEL_IMG} +docker push ${LOCAL_OS_IMG} +docker push ${LOCAL_KERNEL_IMG} +docker rmi ${LOCAL_OS_IMG} ${LOCAL_KERNEL_IMG} +docker logout ${REGISTRY_ADDRESS} diff --git a/pkg/runtime/containerd/client.go b/pkg/runtime/containerd/client.go index 1380e7ae7..2c48377d7 100644 --- a/pkg/runtime/containerd/client.go +++ b/pkg/runtime/containerd/client.go @@ -2,9 +2,11 @@ package containerd import ( "context" + "crypto/tls" "fmt" "io" "io/ioutil" + "net/http" "os" "os/exec" "path/filepath" @@ -159,6 +161,17 @@ func newRemoteResolver(refHostname string, configPath string) (remotes.Resolver, docker.WithAuthorizer(authz), } + // TODO: Make this opt-in via a flag option. + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + client := &http.Client{ + Transport: tr, + } + regOpts = append(regOpts, docker.WithClient(client)) + // TODO: Add option to skip verifying HTTPS cert. resolverOpts := docker.ResolverOptions{ Hosts: docker.ConfigureDefaultRegistries(regOpts...),