Skip to content

Commit

Permalink
Merge pull request #594 from bcrochet/release-1.1-cp
Browse files Browse the repository at this point in the history
Bug fixes from main
  • Loading branch information
bcrochet authored May 4, 2022
2 parents 5d47270 + fd5a656 commit a4cd06d
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 45 deletions.
34 changes: 13 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,36 +70,28 @@ For more information on how to configure the execution of `preflight`, see

### Authenticating to Registries

The `preflight` command will automatically utilize a credential file at
`$DOCKER_CONFIG/config.json` (default: `~/.docker/config.json`) to access images
in private registries.
If a registry requires authentication, one must set the environment variable
`PFTL_DOCKERCONFIG` or pass the `--docker-config` parameter on the command line.
This should be the full path to a properly formatted Docker config.json.

#### Remote Checks

In some cases (e.g. *DeployableByOLM*), `preflight` will also pass credentials
to the cluster used for testing (i.e. the cluster that is accessible through the
current-context of the provided `KUBECONFIG`).

We anticipate that the credentials in `$DOCKER_CONFIG/config.json` may contain
more access than what is needed for `preflight` execution. To avoid passing more
credentials than needed into a cluster for those checks, `preflight` will also
accept a full path to a dockerconfigjson that should be passed through to a
remote cluster via the `PFLT_DOCKERCONFIG` environment variable.
We anticipate that the credentials in `${DOCKER_CONFIG}/config.json` or
`${XDG_RUNTIME_DIR}/containers/auth.json` may contain more access than what is
needed for `preflight` execution. It is recommended to generate a dockerconfigjson
with only the credentials necessary to retrieve the image under test to avoid
passing more credentials than needed into a cluster for those checks. `preflight`
accepts a full path to a dockerconfigjson that would be passed through to a remote
cluster via the `PFLT_DOCKERCONFIG` environment variable or the `--docker-config`
command line parameter.

If this variable is unset, `preflight` will assume that the images in scope
(e.g. PFLT_INDEXIMAGE value, and the test target itself) are already accessible
from the cluster used for testing.

#### Podman Users

[Podman](https://podman.io/) stores credentials at
`${XDG_RUNTIME_DIR}/containers/auth.json`, which can also be used by executing
the following:

```shell
ln -sf ${XDG_RUNTIME_DIR}/containers/auth.json ${XDG_RUNTIME_DIR}/containers/config.json
DOCKER_CONFIG=${XDG_RUNTIME_DIR}/containers
```
(e.g. PFLT_INDEXIMAGE value, and the test target itself) are located in a public
registry and already accessible from the cluster used for testing.

## Installation

Expand Down
77 changes: 77 additions & 0 deletions certification/internal/authn/keychain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package authn

import (
"os"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/types"
craneauthn "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

type preflightKeychain struct{}

var PreflightKeychain craneauthn.Keychain = &preflightKeychain{}

// Resolve returns an Authenticator with credentials, or Anonymous if no suitable credentials
// are found for the target. This implements the Keychain interface from go-containerregistry,
// and will be passed to crane,.
//
// If the viper config is empty, assume Anonymous.
// If the file cannot be found or read, that constitues an error.
func (k *preflightKeychain) Resolve(target craneauthn.Resource) (craneauthn.Authenticator, error) {
log.Trace("entering preflight keychain Resolve")

configFile := viper.GetString("dockerConfig")
if configFile == "" {
// No file specified. No auth expected
return craneauthn.Anonymous, nil
}

r, err := os.Open(configFile)
if os.IsNotExist(err) {
log.Errorf("could not find authfile: %s", configFile)
return nil, err
}
if err != nil {
log.Errorf("Could not open authfile: %s", configFile)
return nil, err
}
defer r.Close()
cf, err := config.LoadFromReader(r)
if err != nil {
log.Errorf("Could not load authfile: %s", configFile)
return nil, err
}

var cfg, empty types.AuthConfig
for _, key := range []string{
target.String(),
target.RegistryStr(),
} {
if key == name.DefaultRegistry {
key = craneauthn.DefaultAuthKey
}

cfg, err = cf.GetAuthConfig(key)
if err != nil {
return nil, err
}
if cfg != empty {
break
}
}
if cfg == empty {
return craneauthn.Anonymous, nil
}

return craneauthn.FromConfig(craneauthn.AuthConfig{
Username: cfg.Username,
Password: cfg.Password,
Auth: cfg.Auth,
IdentityToken: cfg.IdentityToken,
RegistryToken: cfg.RegistryToken,
}), nil
}
209 changes: 209 additions & 0 deletions certification/internal/authn/keychain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright 2018 Google LLC All Rights Reserved.
//
// 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 authn

import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"

craneauthn "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

var (
fresh = 0
testRegistry, _ = name.NewRegistry("test.io", name.WeakValidation)
testRepo, _ = name.NewRepository("test.io/my-repo", name.WeakValidation)
defaultRegistry, _ = name.NewRegistry(name.DefaultRegistry, name.WeakValidation)
)

// setupConfigDir sets up an isolated configDir() for this test.
func setupConfigDir(t *testing.T) string {
tmpdir := os.Getenv("TEST_TMPDIR")
if tmpdir == "" {
var err error
tmpdir, err = ioutil.TempDir("", "keychain_test")
if err != nil {
t.Fatalf("creating temp dir: %v", err)
}
}

fresh++
p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh))
if err := os.Mkdir(p, 0o777); err != nil {
t.Fatalf("mkdir %q: %v", p, err)
}
return p
}

func setupConfigFile(t *testing.T, content string) string {
cd := setupConfigDir(t)
p := filepath.Join(cd, "config.json")
if err := ioutil.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatalf("write %q: %v", p, err)
}
os.Setenv("PFLT_DOCKERCONFIG", p)
// return the config dir so we can clean up
return cd
}

func TestNoConfig(t *testing.T) {
cd := setupConfigDir(t)
defer os.RemoveAll(filepath.Dir(cd))

auth, err := PreflightKeychain.Resolve(testRegistry)
if err != nil {
t.Fatalf("Resolve() = %v", err)
}

if auth != craneauthn.Anonymous {
t.Errorf("expected Anonymous, got %v", auth)
}
}

func encode(user, pass string) string {
delimited := fmt.Sprintf("%s:%s", user, pass)
return base64.StdEncoding.EncodeToString([]byte(delimited))
}

func TestVariousPaths(t *testing.T) {
tests := []struct {
desc string
content string
wantErr bool
target craneauthn.Resource
cfg *craneauthn.AuthConfig
}{{
desc: "invalid config file",
target: testRegistry,
content: `}{`,
wantErr: true,
}, {
desc: "creds store does not exist",
target: testRegistry,
content: `{"credsStore":"#definitely-does-not-exist"}`,
wantErr: true,
}, {
desc: "valid config file",
target: testRegistry,
content: fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")),
cfg: &craneauthn.AuthConfig{
Username: "foo",
Password: "bar",
},
}, {
desc: "valid config file; default registry",
target: defaultRegistry,
content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, craneauthn.DefaultAuthKey, encode("foo", "bar")),
cfg: &craneauthn.AuthConfig{
Username: "foo",
Password: "bar",
},
}, {
desc: "valid config file; matches registry w/ v1",
target: testRegistry,
content: fmt.Sprintf(`{
"auths": {
"http://test.io/v1/": {"auth": %q}
}
}`, encode("baz", "quux")),
cfg: &craneauthn.AuthConfig{
Username: "baz",
Password: "quux",
},
}, {
desc: "valid config file; matches registry w/ v2",
target: testRegistry,
content: fmt.Sprintf(`{
"auths": {
"http://test.io/v2/": {"auth": %q}
}
}`, encode("baz", "quux")),
cfg: &craneauthn.AuthConfig{
Username: "baz",
Password: "quux",
},
}, {
desc: "valid config file; matches repo",
target: testRepo,
content: fmt.Sprintf(`{
"auths": {
"test.io/my-repo": {"auth": %q},
"test.io/another-repo": {"auth": %q},
"test.io": {"auth": %q}
}
}`, encode("foo", "bar"), encode("bar", "baz"), encode("baz", "quux")),
cfg: &craneauthn.AuthConfig{
Username: "foo",
Password: "bar",
},
}}

for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cd := setupConfigFile(t, test.content)
// For some reason, these tempdirs don't get cleaned up.
defer os.RemoveAll(filepath.Dir(cd))

auth, err := PreflightKeychain.Resolve(test.target)
if test.wantErr {
if err == nil {
t.Fatal("wanted err, got nil")
} else if err != nil {
// success
return
}
}
if err != nil {
t.Fatalf("wanted nil, got err: %v", err)
}
cfg, err := auth.Authorization()
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(cfg, test.cfg) {
t.Errorf("got %+v, want %+v", cfg, test.cfg)
}
})
}
}

type helper struct {
u, p string
err error
}

func (h helper) Get(serverURL string) (string, string, error) {
if serverURL != "example.com" {
return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL)
}
return h.u, h.p, h.err
}

func init() {
log.SetFormatter(&log.TextFormatter{})
log.SetLevel(log.TraceLevel)

viper.SetEnvPrefix("pflt")
viper.AutomaticEnv()
}
12 changes: 10 additions & 2 deletions certification/internal/engine/crane.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package engine

import (
"context"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/authn"
)

type craneEngine struct{}
Expand All @@ -10,6 +13,11 @@ func NewCraneEngine() *craneEngine {
return &craneEngine{}
}

func (c *craneEngine) ListTags(imageURI string) ([]string, error) {
return crane.ListTags(imageURI)
func (c *craneEngine) ListTags(ctx context.Context, imageURI string) ([]string, error) {
options := []crane.Option{
crane.WithContext(ctx),
crane.WithAuthFromKeychain(authn.PreflightKeychain),
}

return crane.ListTags(imageURI, options...)
}
6 changes: 5 additions & 1 deletion certification/internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/errors"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/authn"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/rpm"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/pyxis"
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime"
Expand Down Expand Up @@ -54,7 +55,10 @@ func (c *CraneEngine) ExecuteChecks(ctx context.Context) error {
log.Debug("target image: ", c.Image)

// prepare crane runtime options, if necessary
options := make([]crane.Option, 0)
options := []crane.Option{
crane.WithContext(ctx),
crane.WithAuthFromKeychain(authn.PreflightKeychain),
}

// pull the image and save to fs
log.Debug("pulling image from target registry")
Expand Down
Loading

0 comments on commit a4cd06d

Please sign in to comment.