Skip to content

Commit

Permalink
Rework auth
Browse files Browse the repository at this point in the history
  • Loading branch information
blampe committed Feb 27, 2024
1 parent b5fb65e commit f62a475
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 153 deletions.
1 change: 0 additions & 1 deletion provider/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ require (
github.com/muesli/reflow v0.3.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc5
github.com/otiai10/copy v1.14.0
github.com/pkg/errors v0.9.1
github.com/pulumi/pulumi-go-provider v0.14.1-0.20240215003739-1759a7d2465b
github.com/pulumi/pulumi-go-provider/integration v0.10.0
Expand Down
4 changes: 0 additions & 4 deletions provider/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2248,10 +2248,6 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
Expand Down
2 changes: 1 addition & 1 deletion provider/internal/buildx.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (c *Config) Configure(ctx provider.Context) error {
c.client = client

for _, creds := range c.RegistryAuth {
if err := client.Auth(ctx, creds); err != nil {
if err := client.Auth(ctx, _baseAuth, creds); err != nil {
return err
}
}
Expand Down
139 changes: 76 additions & 63 deletions provider/internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ import (
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
controllerapi "github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/store"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/platformutil"
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
cfgtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/flags"
manifesttypes "github.com/docker/cli/cli/manifest/types"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
registryconst "github.com/docker/docker/registry"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/progress/progressui"
cp "github.com/otiai10/copy"

provider "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
Expand All @@ -42,25 +42,25 @@ import (

// Client handles all our Docker API calls.
type Client interface {
Auth(ctx context.Context, creds properties.RegistryAuth) error
Build(ctx provider.Context, opts controllerapi.BuildOptions) (*client.SolveResponse, error)
Close(ctx context.Context) error
Auth(ctx context.Context, name string, creds properties.RegistryAuth) error
Build(ctx provider.Context, name string, opts controllerapi.BuildOptions) (*client.SolveResponse, error)
BuildKitEnabled() (bool, error)
Inspect(ctx context.Context, id string) ([]manifesttypes.ImageManifest, error)
Inspect(ctx context.Context, name string, id string) ([]manifesttypes.ImageManifest, error)
Delete(ctx context.Context, id string) ([]image.DeleteResponse, error)
}

type docker struct {
mu sync.Mutex

cli *command.DockerCli
txn *store.Txn
dir string
auths map[string]map[string]cfgtypes.AuthConfig
builders map[string]*cachedBuilder
}

var _ Client = (*docker)(nil)

var _baseAuth = ""

func newDockerClient() (*docker, error) {
cli, err := command.NewDockerCli(
command.WithCombinedStreams(os.Stdout),
Expand All @@ -69,48 +69,54 @@ func newDockerClient() (*docker, error) {
return nil, err
}

// We create a temporary directory for our config to not disturb the host's
// existing settings.
dir, err := os.MkdirTemp("", "pulumi-docker-")
if err != nil {
return nil, err
}
// Attempt to copy the host's existing config, if it exists, over to our
// temporary config directory. This ensures we preserve things like
// credential helpers, builders, etc.
if _, serr := os.Stat(config.Dir()); serr == nil {
_ = cp.Copy(config.Dir(), dir)
}

opts := &flags.ClientOptions{
ConfigDir: dir,
// TODO(github.com/pulumi/pulumi-docker/issues/946): Support TLS options
}
err = cli.Initialize(opts)
if err != nil {
return nil, err
}

txn, _, err := storeutil.GetStore(cli)
d := &docker{
cli: cli,
auths: map[string]map[string]cfgtypes.AuthConfig{},
builders: map[string]*cachedBuilder{},
}

// Load existing credentials into memory.
creds, err := cli.ConfigFile().GetAllCredentials()
if err != nil {
return nil, err
}
for _, cred := range creds {
err := d.Auth(context.Background(), _baseAuth, properties.RegistryAuth{
Address: cred.ServerAddress,
Username: cred.Username,
Password: cred.Password,
})
if err != nil {
return nil, err
}
}

return &docker{cli: cli, txn: txn, dir: dir, builders: map[string]*cachedBuilder{}}, err
return d, nil
}

func (d *docker) Auth(ctx context.Context, creds properties.RegistryAuth) error {
func (d *docker) Auth(_ context.Context, name string, creds properties.RegistryAuth) error {
d.mu.Lock()
defer d.mu.Unlock()

cfg := d.cli.ConfigFile()
if _, ok := d.auths[name]; !ok {
d.auths[name] = map[string]cfgtypes.AuthConfig{}
}

// Special handling for legacy DockerHub domains. The OCI-compliant
// registry is registry-1.docker.io but this is stored in config under the
// legacy name.
// https://github.com/docker/cli/issues/3793#issuecomment-1269051403
if creds.Address == "docker.io" {
creds.Address = "https://index.docker.io/v1/"
key := credentials.ConvertToHostname(creds.Address)
if key == "registry-1.docker.io" || key == "index.docker.io" || key == "docker.io" {
key = "https://index.docker.io/v1/"
}

auth := cfgtypes.AuthConfig{
Expand All @@ -119,36 +125,19 @@ func (d *docker) Auth(ctx context.Context, creds properties.RegistryAuth) error
Password: creds.Password,
}

// Workaround for https://github.com/docker/docker-credential-helpers/issues/37.
if existing, err := cfg.GetAuthConfig(creds.Address); err == nil {
// Confirm the auth is still valid. Otherwise we'll set it to the
// provided config.
if existing.Username == creds.Username {
_, err = d.cli.Client().RegistryLogin(ctx, registrytypes.AuthConfig{
Auth: existing.Auth,
Email: existing.Email,
IdentityToken: existing.IdentityToken,
Password: existing.Password,
RegistryToken: existing.RegistryToken,
ServerAddress: creds.Address, // ServerAddress is sometimes empty?
Username: existing.Username,
})
if err == nil {
return nil // Creds still work, nothing to do.
}
}
if _, ok := d.auths[name][key]; ok {
return nil // Already saved these creds. Nothing to do.
}

err := cfg.GetCredentialsStore(creds.Address).Store(auth)
if err != nil {
return fmt.Errorf("%q: %w", creds.Address, err)
}
d.auths[name][key] = auth

return nil
}

// Build performs a buildkit build.
func (d *docker) Build(
pctx provider.Context,
name string,
opts controllerapi.BuildOptions,
) (*client.SolveResponse, error) {
// Use a separate context for the build. We don't want to kill our request's
Expand Down Expand Up @@ -212,6 +201,14 @@ func (d *docker) Build(
platforms, _ := platformutil.Parse(opts.Platforms)
platforms = platformutil.Dedupe(platforms)

auths := map[string]cfgtypes.AuthConfig{}
for host, cfg := range d.auths[_baseAuth] {
auths[host] = cfg
}
for host, cfg := range d.auths[name] {
auths[host] = cfg
}

// Perform the build.
targets, err := build.Build(
bctx,
Expand All @@ -233,12 +230,12 @@ func (d *docker) Build(
Target: opts.Target,

Session: []session.Attachable{
authprovider.NewDockerAuthProvider(d.cli.ConfigFile(), nil),
authprovider.NewDockerAuthProvider(&configfile.ConfigFile{AuthConfigs: auths}, nil),
},
},
},
dockerutil.NewClient(d.cli),
d.dir,
filepath.Dir(d.cli.ConfigFile().Filename),
printer,
)
if err != nil {
Expand Down Expand Up @@ -266,21 +263,38 @@ func (d *docker) BuildKitEnabled() (bool, error) {
return d.cli.BuildKitEnabled()
}

// Close cleans up temporary configs.
func (d *docker) Close(_ context.Context) error {
return os.RemoveAll(d.dir)
}

// Inspect inspects an image.
func (d *docker) Inspect(ctx context.Context, id string) ([]manifesttypes.ImageManifest, error) {
func (d *docker) Inspect(ctx context.Context, name string, id string) ([]manifesttypes.ImageManifest, error) {
ref, err := normalizeReference(id)
if err != nil {
return []manifesttypes.ImageManifest{}, err
}

rc := d.cli.RegistryClient(d.cli.DockerEndpoint().SkipTLSVerify)
// Constructed a RegistryClient which can use our in-memory auth.
insecure := d.cli.DockerEndpoint().SkipTLSVerify
resolver := func(_ context.Context, index *registry.IndexInfo) registry.AuthConfig {
configKey := index.Name
if index.Official {
configKey = registryconst.IndexServer
}

for _, scope := range []string{name, _baseAuth} {
auths, ok := d.auths[scope]
if !ok {
continue
}
if a, ok := auths[configKey]; ok {
return registry.AuthConfig(a)
}
}
return registry.AuthConfig{}
}
rc := registryclient.NewRegistryClient(resolver, command.UserAgent(), insecure)

manifests, err := rc.GetManifestList(ctx, ref)

// If the registry doesn't support manifest lists, attempt to fetch an
// individual one.
if err != nil && strings.Contains(err.Error(), "unsupported manifest format") {
manifest, err := rc.GetManifest(ctx, ref)
manifests = append(manifests, manifest)
Expand Down Expand Up @@ -328,7 +342,6 @@ func (d *docker) builder(
b, err := builder.New(d.cli,
builder.WithName(opts.Builder),
builder.WithContextPathHash(contextPathHash),
builder.WithStore(d.txn),
)
if err != nil {
return nil, err
Expand Down
16 changes: 12 additions & 4 deletions provider/internal/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ func TestAuth(t *testing.T) {
_ = d.cli.ConfigFile().GetCredentialsStore(host).Erase(host)
})

err = d.Auth(context.Background(), properties.RegistryAuth{
err = d.Auth(context.Background(), "test-resource", properties.RegistryAuth{
Address: host,
Username: user,
Password: password,
})
assert.NoError(t, err)

// Perform a second auth; it should be cached.
err = d.Auth(context.Background(), "test-resource", properties.RegistryAuth{
Address: host,
Username: user,
Password: password,
Expand All @@ -52,7 +60,7 @@ func TestBuild(t *testing.T) {
pctx.EXPECT().Err().Return(ctx.Err()).AnyTimes()
pctx.EXPECT().Deadline().Return(ctx.Deadline()).AnyTimes()

_, err = d.Build(pctx, pb.BuildOptions{
_, err = d.Build(pctx, "resource-name", pb.BuildOptions{
ContextPath: "../testdata/",
DockerfileName: "../testdata/Dockerfile",
})
Expand All @@ -71,11 +79,11 @@ func TestInspect(t *testing.T) {
d, err := newDockerClient()
require.NoError(t, err)

v2, err := d.Inspect(context.Background(), "blampe/myapp:buildx")
v2, err := d.Inspect(context.Background(), "test", "pulumibot/myapp:buildx")
assert.NoError(t, err)
assert.Equal(t, 2, v2[0].OCIManifest.SchemaVersion)

v1, err := d.Inspect(context.Background(), "pulumi/pulumi")
v1, err := d.Inspect(context.Background(), "test", "pulumi/pulumi")
assert.NoError(t, err)
assert.Nil(t, v1[0].OCIManifest)
}
Expand Down
17 changes: 5 additions & 12 deletions provider/internal/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (is *ImageState) Annotate(a infer.Annotator) {
// authenticated.
func (*Image) Check(
ctx provider.Context,
_ string,
name string,
_ resource.PropertyMap,
news resource.PropertyMap,
) (ImageArgs, []provider.CheckFailure, error) {
Expand All @@ -148,7 +148,7 @@ func (*Image) Check(
if reg.Address == "" {
continue
}
if err = cfg.client.Auth(ctx, reg); err != nil {
if err = cfg.client.Auth(ctx, name, reg); err != nil {
failures = append(failures,
provider.CheckFailure{Property: "registries", Reason: fmt.Sprintf("unable to authenticate: %s", err.Error())})
}
Expand Down Expand Up @@ -259,12 +259,11 @@ func (i *Image) Update(
return state, nil
}

_, err = cfg.client.Build(ctx, opts)
_, err = cfg.client.Build(ctx, name, opts)
if err != nil {
return state, err
}

// TODO: Handle case with no export.
_, _, state, err = i.Read(ctx, name, input, state)

return state, err
Expand Down Expand Up @@ -305,7 +304,7 @@ func (*Image) Read(
// Ensure we're authenticated.
cfg := infer.GetConfig[Config](ctx)
for _, reg := range input.Registries {
if err = cfg.client.Auth(ctx, reg); err != nil {
if err = cfg.client.Auth(ctx, name, reg); err != nil {
return name, input, state, err
}
}
Expand All @@ -319,7 +318,7 @@ func (*Image) Read(
continue
}
for _, tag := range input.Tags {
infos, err := cfg.client.Inspect(ctx, tag)
infos, err := cfg.client.Inspect(ctx, name, tag)
if err != nil {
continue
}
Expand Down Expand Up @@ -448,9 +447,3 @@ func (*Image) Diff(_ provider.Context, _ string, olds ImageState, news ImageArgs
DetailedDiff: diff,
}, nil
}

// Cancel cleans up temporary on-disk credentials.
func (*Image) Cancel(ctx provider.Context) error {
cfg := infer.GetConfig[Config](ctx)
return cfg.client.Close(ctx)
}
Loading

0 comments on commit f62a475

Please sign in to comment.