From 15691e84e1e82698b77b4fb6bab52a458b13c583 Mon Sep 17 00:00:00 2001 From: Amber Brown Date: Thu, 29 Jun 2023 12:50:44 +1000 Subject: [PATCH] improve tests and move pull secret stuff --- pkg/containerinstall/install.go | 47 +-------- pkg/containerinstall/install_test.go | 140 +++++++++++++++------------ pkg/containerinstall/manager.go | 6 +- pkg/util/pullsecret/extract.go | 56 +++++++++++ pkg/util/pullsecret/extract_test.go | 59 +++++++++++ 5 files changed, 200 insertions(+), 108 deletions(-) create mode 100644 pkg/util/pullsecret/extract.go create mode 100644 pkg/util/pullsecret/extract_test.go diff --git a/pkg/containerinstall/install.go b/pkg/containerinstall/install.go index 547b0c3d557..485ac9ca6c1 100644 --- a/pkg/containerinstall/install.go +++ b/pkg/containerinstall/install.go @@ -6,14 +6,11 @@ package containerinstall import ( "bytes" "context" - "encoding/base64" "encoding/json" - "errors" "fmt" "os" "path/filepath" "runtime" - "strings" "time" "github.com/Azure/go-autorest/autorest/to" @@ -24,7 +21,6 @@ import ( "github.com/opencontainers/runtime-spec/specs-go" "github.com/Azure/ARO-RP/pkg/api" - "github.com/Azure/ARO-RP/pkg/env" "github.com/Azure/ARO-RP/pkg/util/steps" ) @@ -44,18 +40,14 @@ var ( } ) -type auths struct { - Auths map[string]map[string]interface{} `json:"auths,omitempty"` -} - func (m *manager) Install(ctx context.Context, sub *api.SubscriptionDocument, doc *api.OpenShiftClusterDocument, version *api.OpenShiftVersion) error { s := []steps.Step{ steps.Action(func(context.Context) error { options := &images.PullOptions{ Quiet: to.BoolPtr(true), Policy: to.StringPtr("always"), - Username: to.StringPtr(m.pullSecret[0]), - Password: to.StringPtr(m.pullSecret[1]), + Username: to.StringPtr(m.pullSecret.Username), + Password: to.StringPtr(m.pullSecret.Password), } _, err := images.Pull(m.conn, version.Properties.InstallerPullspec, options) @@ -109,8 +101,8 @@ func (m *manager) startContainer(ctx context.Context, version *api.OpenShiftVers "OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE": version.Properties.OpenShiftPullspec, } - for _, i := range devEnvVars { - s.Env["ARO_"+i] = os.Getenv(i) + for _, envvar := range devEnvVars { + s.Env["ARO_"+envvar] = os.Getenv(envvar) } s.Mounts = append(s.Mounts, specs.Mount{ @@ -222,34 +214,3 @@ func (m *manager) cleanupContainers(ctx context.Context) error { } return nil } - -func pullSecretFromEnv(_env env.Interface) (out [2]string, err error) { - pullSecrets := &auths{} - err = json.Unmarshal([]byte(os.Getenv("PULL_SECRET")), pullSecrets) - if err != nil { - return - } - - auth, ok := pullSecrets.Auths[_env.ACRDomain()] - if !ok { - return out, fmt.Errorf("missing %s key in PULL_SECRET", _env.ACRDomain()) - } - - token, ok := auth["auth"] - if !ok { - return out, errors.New("maformed auth token") - } - - decoded, err := base64.StdEncoding.DecodeString(token.(string)) - if err != nil { - return out, err - } - - split := strings.Split(string(decoded), ":") - if len(split) != 2 { - return out, fmt.Errorf("not username:pass in %s config", _env.ACRDomain()) - } - - out = [2]string{split[0], split[1]} - return out, nil -} diff --git a/pkg/containerinstall/install_test.go b/pkg/containerinstall/install_test.go index 62ec7a1bb19..4e661f81f6e 100644 --- a/pkg/containerinstall/install_test.go +++ b/pkg/containerinstall/install_test.go @@ -7,13 +7,16 @@ import ( "context" "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/Azure/go-autorest/autorest/to" "github.com/containers/podman/v4/pkg/bindings/containers" "github.com/containers/podman/v4/pkg/bindings/images" "github.com/containers/podman/v4/pkg/specgen" - "github.com/onsi/gomega" "github.com/onsi/gomega/types" "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" "github.com/Azure/ARO-RP/pkg/util/uuid" testlog "github.com/Azure/ARO-RP/test/util/log" @@ -21,66 +24,77 @@ import ( const TEST_PULLSPEC = "registry.access.redhat.com/ubi8/go-toolset:1.18.4" -func TestPodman(t *testing.T) { - ctx := context.Background() - conn, err := getConnection(ctx) - if err != nil { - t.Skipf("unable to access podman: %v", err) - } - - hook, log := testlog.New() - randomName := uuid.DefaultGenerator.Generate() - - s := specgen.NewSpecGenerator(TEST_PULLSPEC, false) - s.Name = randomName - s.Entrypoint = []string{"/bin/bash", "-c", "echo 'hello'"} - - _, err = images.Pull(conn, TEST_PULLSPEC, &images.PullOptions{Policy: to.StringPtr("missing")}) - if err != nil { - t.Fatal(err) - } - - id, err := runContainer(conn, log, s) - if err != nil { - t.Fatal(err) - } - - exit, err := containers.Wait(conn, id, nil) - if err != nil { - t.Error(err) - } - if exit != 0 { - t.Errorf("exit code was %d, not 0", exit) - } - - err = getContainerLogs(conn, log, id) - if err != nil { - t.Error(err) - } - - _, err = containers.Remove(conn, id, &containers.RemoveOptions{Force: to.BoolPtr(true)}) - if err != nil { - t.Error(err) - } - - entries := []map[string]types.GomegaMatcher{ - - { - "msg": gomega.Equal("created container " + randomName + " with ID " + id), - "level": gomega.Equal(logrus.InfoLevel), - }, - { - "msg": gomega.Equal("started container " + id), - "level": gomega.Equal(logrus.InfoLevel), - }, - { - "msg": gomega.Equal("stdout: hello\n"), - "level": gomega.Equal(logrus.InfoLevel), - }, - } - - err = testlog.AssertLoggingOutput(hook, entries) - if err != nil { - t.Fatal(err) - } +var _ = Describe("Podman", Ordered, func() { + var err error + var conn context.Context + var hook *test.Hook + var log *logrus.Entry + var containerName string + var containerID string + + BeforeAll(func(ctx context.Context) { + var err error + conn, err = getConnection(ctx) + if err != nil { + Skip("unable to access podman: %v") + } + + hook, log = testlog.New() + containerName = uuid.DefaultGenerator.Generate() + }) + + It("can pull images", func() { + _, err = images.Pull(conn, TEST_PULLSPEC, &images.PullOptions{Policy: to.StringPtr("missing")}) + Expect(err).To(BeNil()) + }) + + It("can start a container", func() { + s := specgen.NewSpecGenerator(TEST_PULLSPEC, false) + s.Name = containerName + s.Entrypoint = []string{"/bin/bash", "-c", "echo 'hello'"} + + containerID, err = runContainer(conn, log, s) + Expect(err).To(BeNil()) + }) + + It("can wait for completion", func() { + exit, err := containers.Wait(conn, containerID, nil) + Expect(err).To(BeNil()) + Expect(exit).To(Equal(0), "exit code was %d, not 0", exit) + }) + + It("can fetch container logs", func() { + err = getContainerLogs(conn, log, containerID) + Expect(err).To(BeNil()) + + entries := []map[string]types.GomegaMatcher{ + { + "msg": Equal("created container " + containerName + " with ID " + containerID), + "level": Equal(logrus.InfoLevel), + }, + { + "msg": Equal("started container " + containerID), + "level": Equal(logrus.InfoLevel), + }, + { + "msg": Equal("stdout: hello\n"), + "level": Equal(logrus.InfoLevel), + }, + } + + err = testlog.AssertLoggingOutput(hook, entries) + Expect(err).To(BeNil()) + }) + + AfterAll(func() { + if containerID != "" { + _, err = containers.Remove(conn, containerID, &containers.RemoveOptions{Force: to.BoolPtr(true)}) + Expect(err).To(BeNil()) + } + }) +}) + +func TestContainerInstall(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ContainerInstall Suite") } diff --git a/pkg/containerinstall/manager.go b/pkg/containerinstall/manager.go index 0674ad251f2..618488ab3ce 100644 --- a/pkg/containerinstall/manager.go +++ b/pkg/containerinstall/manager.go @@ -6,11 +6,13 @@ package containerinstall import ( "context" "errors" + "os" "github.com/sirupsen/logrus" "github.com/Azure/ARO-RP/pkg/api" "github.com/Azure/ARO-RP/pkg/env" + "github.com/Azure/ARO-RP/pkg/util/pullsecret" ) type ContainerInstaller interface { @@ -23,7 +25,7 @@ type manager struct { env env.Interface clusterUUID string - pullSecret [2]string + pullSecret *pullsecret.UserPass success bool } @@ -34,7 +36,7 @@ func New(ctx context.Context, log *logrus.Entry, env env.Interface, clusterUUID return nil, errors.New("running cluster installs in a container is only run in development") } - pullSecret, err := pullSecretFromEnv(env) + pullSecret, err := pullsecret.Extract(os.Getenv("PULL_SECRET"), env.ACRDomain()) if err != nil { return nil, err } diff --git a/pkg/util/pullsecret/extract.go b/pkg/util/pullsecret/extract.go new file mode 100644 index 00000000000..576d7af8566 --- /dev/null +++ b/pkg/util/pullsecret/extract.go @@ -0,0 +1,56 @@ +package pullsecret + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" +) + +type UserPass struct { + Username string + Password string +} + +func userPassFromBase64(secret string) (*UserPass, error) { + decoded, err := base64.StdEncoding.DecodeString(secret) + if err != nil { + return nil, errors.New("malformed auth token") + } + + split := strings.Split(string(decoded), ":") + if len(split) != 2 { + return nil, errors.New("auth token not in format of username:password") + } + + return &UserPass{ + Username: split[0], + Password: split[1], + }, nil +} + +// Extract decodes a username and password for a given domain from a +// JSON-encoded pull secret (e.g. from docker auth) +func Extract(rawPullSecret, domain string) (*UserPass, error) { + pullSecrets := &pullSecret{} + err := json.Unmarshal([]byte(rawPullSecret), pullSecrets) + if err != nil { + return nil, errors.New("malformed pullsecret (invalid JSON)") + } + + auth, ok := pullSecrets.Auths[domain] + if !ok { + return nil, fmt.Errorf("missing '%s' key in pullsecret", domain) + } + + token, ok := auth["auth"] + if !ok { + return nil, errors.New("malformed pullsecret (no auth key)") + } + + return userPassFromBase64(token.(string)) +} diff --git a/pkg/util/pullsecret/extract_test.go b/pkg/util/pullsecret/extract_test.go new file mode 100644 index 00000000000..2745960e21d --- /dev/null +++ b/pkg/util/pullsecret/extract_test.go @@ -0,0 +1,59 @@ +package pullsecret + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Extract()", func() { + It("correctly decodes a pullsecret", func() { + pullSecret := "{\"auths\": {\"example.com\": {\"auth\": \"dGVzdHVzZXI6dGVzdHBhc3M=\"}}}" + + correctlyExtracted, err := Extract(pullSecret, "example.com") + Expect(err).To(BeNil()) + Expect(correctlyExtracted).To(Equal(&UserPass{Username: "testuser", Password: "testpass"})) + }) + + It("errors if no pullsecret for that name exists", func() { + pullSecret := "{\"auths\": {\"example.com\": {\"auth\": \"dGVzdHVzZXI6dGVzdHBhc3M=\"}}}" + + _, err := Extract(pullSecret, "missingexample.com") + Expect(err).To(MatchError("missing 'missingexample.com' key in pullsecret")) + }) + + It("errors if the json is invalid", func() { + _, err := Extract("\"", "example.com") + Expect(err).To(MatchError("malformed pullsecret (invalid JSON)")) + }) + + It("errors if the base64 is invalid", func() { + pullSecret := "{\"auths\": {\"example.com\": {\"auth\": \"5\"}}}" + + _, err := Extract(pullSecret, "example.com") + Expect(err).To(MatchError("malformed auth token")) + }) + + It("errors if the base64 does not contain a username and password", func() { + pullSecret := "{\"auths\": {\"example.com\": {\"auth\": \"c29tZXRoaW5nZWxzZQ==\"}}}" + + _, err := Extract(pullSecret, "example.com") + Expect(err).To(MatchError("auth token not in format of username:password")) + }) + + It("errors if pullsecret has no auth key for domain", func() { + pullSecret := "{\"auths\": {\"example.com\": {\"password\": \"dGVzdHVzZXI6dGVzdHBhc3M=\"}}}" + + _, err := Extract(pullSecret, "example.com") + Expect(err).To(MatchError("malformed pullsecret (no auth key)")) + }) +}) + +func TestPullSecret(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "PullSecret Suite") +}