Skip to content

Commit

Permalink
refactor: Move url globber to own package
Browse files Browse the repository at this point in the history
Copied some functionality from upstream kubernetes, and add unit tests to globber.
  • Loading branch information
jimmidyson committed Nov 16, 2022
1 parent ec9b20c commit b64783a
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 34 deletions.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module github.com/mesosphere/kubelet-image-credential-provider-shim
go 1.19

require (
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279
github.com/kelseyhightower/envconfig v1.4.0
github.com/otiai10/copy v1.9.0
github.com/sirupsen/logrus v1.9.0
Expand Down Expand Up @@ -36,13 +37,14 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279 h1:+lFUfSfK1/rMGIUUAwu6O+t4WGRwBU1EpaQTcN8KaeM=
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279/go.mod h1:4x0IxAMsdeCSTr9UopCvp6MnryD2nyRLycsOrgvveAs=
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -215,6 +217,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
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/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
Expand Down Expand Up @@ -354,8 +358,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down
33 changes: 2 additions & 31 deletions pkg/credentialprovider/shim/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import (
"context"
"errors"
"fmt"
"net"
"os"
"regexp"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -19,14 +16,13 @@ import (

"github.com/mesosphere/kubelet-image-credential-provider-shim/apis/config/v1alpha1"
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/plugin"
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/urlglobber"
)

var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)

domainSegmentRE = regexp.MustCompile(`[^.]+`)

ErrUnsupportedMirrorCredentialStrategy = errors.New("unsupported mirror credential strategy")
)

Expand Down Expand Up @@ -61,19 +57,12 @@ func NewProviderFromConfigFile(fName string) (plugin.CredentialProvider, error)
return &shimProvider{cfg: config}, nil
}

var (
ErrUnsupportedConfiguration = errors.New(
"the shim provider currently only supports hard-coded mirror credentials",
)
ErrInvalidImageReference = errors.New("invalid: image reference")
)

func (p shimProvider) GetCredentials(
_ context.Context,
img string,
_ []string,
) (*credentialproviderv1beta1.CredentialProviderResponse, error) {
globbedDomain, err := globbedDomainForImage(img)
globbedDomain, err := urlglobber.GlobbedDomainForImage(img)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -169,21 +158,3 @@ func isRegistryCredentialsOnly(cfg *v1alpha1.MirrorConfig) bool {
return cfg != nil &&
cfg.MirrorCredentialsStrategy == v1alpha1.MirrorCredentialsOnly
}

func globbedDomainForImage(img string) (string, error) {
splitImg := strings.Split(img, "/")
if len(splitImg) < 2 {
return "", fmt.Errorf("%w: missing domain", ErrInvalidImageReference)
}
domain := splitImg[0]
domainWithoutPort, port, errSplitPort := net.SplitHostPort(domain)
if errSplitPort == nil {
domain = domainWithoutPort
}
globbedDomain := domainSegmentRE.ReplaceAllLiteralString(domain, "*")
if errSplitPort == nil {
globbedDomain = net.JoinHostPort(domain, port)
}

return globbedDomain, nil
}
39 changes: 39 additions & 0 deletions pkg/credentialprovider/urlglobber/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2022 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// This package provides utilities to work with URL globs for credential providers.
//
// Duplicating documentation from
// https://github.com/kubernetes/kubelet/blob/v0.25.4/pkg/apis/credentialprovider/v1beta1/types.go#L73-L101
// for visibility:
//
// auth is a map containing authentication information passed into the kubelet.
// Each key is a match image string (more on this below). The corresponding authConfig value
// should be valid for all images that match against this key. A plugin should set
// this field to null if no valid credentials can be returned for the requested image.
//
// Each key in the map is a pattern which can optionally contain a port and a path.
// Globs can be used in the domain, but not in the port or the path. Globs are supported
// as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'.
// Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match
// a single subdomain segment, so *.io does not match *.k8s.io.
//
// The kubelet will match images against the key when all of the below are true:
// - Both contain the same number of domain parts and each part matches.
// - The URL path of an imageMatch must be a prefix of the target image URL path.
// - If the imageMatch contains a port, then the port must match in the image as well.
//
// When multiple keys are returned, the kubelet will traverse all keys in reverse order so that:
// - longer keys come before shorter keys with the same prefix
// - non-wildcard keys come before wildcard keys with the same prefix.
//
// For any given match, the kubelet will attempt an image pull with the provided credentials,
// stopping after the first successfully authenticated pull.
//
// Example keys:
// - 123456789.dkr.ecr.us-east-1.amazonaws.com
// - *.azurecr.io
// - gcr.io
// - *.*.registry.io
// - registry.io:8080/path
package urlglobber
39 changes: 39 additions & 0 deletions pkg/credentialprovider/urlglobber/globber.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2022 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package urlglobber

import (
"errors"
"fmt"
"net"
"regexp"

"github.com/distribution/distribution/v3/reference"
)

var (
domainSegmentRE = regexp.MustCompile(`[^.]+`)

ErrInvalidImageReference = errors.New("invalid image reference")
)

func GlobbedDomainForImage(img string) (string, error) {
namedImg, err := reference.ParseNormalizedNamed(img)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidImageReference, err)
}

domain := reference.Domain(namedImg)

domainWithoutPort, port, errSplitPort := net.SplitHostPort(domain)
if errSplitPort == nil {
domain = domainWithoutPort
}
globbedDomain := domainSegmentRE.ReplaceAllLiteralString(domain, "*")
if errSplitPort == nil {
globbedDomain = net.JoinHostPort(globbedDomain, port)
}

return globbedDomain, nil
}
52 changes: 52 additions & 0 deletions pkg/credentialprovider/urlglobber/globber_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2022 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package urlglobber_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/urlglobber"
)

func TestGlobbedDomainForImage(t *testing.T) {
t.Parallel()

tests := []struct {
name string
img string
wantGlobbed string
wantErr error
}{{
name: "Empty URL",
wantErr: urlglobber.ErrInvalidImageReference,
}, {
name: "Simple image",
img: prefixKubernetesIO + "/foo/bar:v1.2.3",
wantGlobbed: "*.*.*",
}, {
name: "Simple image with port",
img: prefixKubernetesIO + ":1111/foo/bar:v1.2.3",
wantGlobbed: "*.*.*:1111",
}, {
name: "Image from docker hub with no domain",
img: "foo/bar:v1.2.3",
wantGlobbed: "*.*", // To match docker.io.
}, {
name: "Image from docker hub with no domain or path",
img: "bar:v1.2.3",
wantGlobbed: "*.*", // To match docker.io.
}}
for _, tt := range tests {
tt := tt // Capture range variable.
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
globbed, err := urlglobber.GlobbedDomainForImage(tt.img)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantGlobbed, globbed)
})
}
}
104 changes: 104 additions & 0 deletions pkg/credentialprovider/urlglobber/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2022 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/*
Copyright 2014 The Kubernetes 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.
*/

// The content of this file is copied from
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/pkg/credentialprovider/keyring.go#L160-L233.
// Copied rather than imported as go mod because it lives in k8s.io/kubernetes which would pull in
// lots of unnecessary dependencies.

package urlglobber

import (
"net"
"net/url"
"path/filepath"
"strings"
)

// ParseSchemelessURL parses a schemeless url and returns a url.URL
// url.Parse require a scheme, but ours don't have schemes. Adding a
// scheme to make url.Parse happy, then clear out the resulting scheme.
func ParseSchemelessURL(schemelessURL string) (*url.URL, error) {
parsed, err := url.Parse("https://" + schemelessURL)
if err != nil {
return nil, err
}
// clear out the resulting scheme
parsed.Scheme = ""
return parsed, nil
}

// SplitURL splits the host name into parts, as well as the port.
func SplitURL(u *url.URL) (parts []string, port string) {
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
// could not parse port
host, port = u.Host, ""
}
return strings.Split(host, "."), port
}

// URLsMatchStr is wrapper for URLsMatch, operating on strings instead of URLs.
func URLsMatchStr(glob, target string) (bool, error) {
globURL, err := ParseSchemelessURL(glob)
if err != nil {
return false, err
}
targetURL, err := ParseSchemelessURL(target)
if err != nil {
return false, err
}
return URLsMatch(globURL, targetURL)
}

// URLsMatch checks whether the given target url matches the glob url, which may have
// glob wild cards in the host name.
//
// Examples:
//
// globURL=*.docker.io, targetURL=blah.docker.io => match
// globURL=*.docker.io, targetURL=not.right.io => no match
//
// Note that we don't support wildcards in ports and paths yet.
func URLsMatch(globURL, targetURL *url.URL) (bool, error) {
globURLParts, globPort := SplitURL(globURL)
targetURLParts, targetPort := SplitURL(targetURL)
if globPort != targetPort {
// port doesn't match
return false, nil
}
if len(globURLParts) != len(targetURLParts) {
// host name does not have the same number of parts
return false, nil
}
if !strings.HasPrefix(targetURL.Path, globURL.Path) {
// the path of the credential must be a prefix
return false, nil
}
for k, globURLPart := range globURLParts {
targetURLPart := targetURLParts[k]
matched, err := filepath.Match(globURLPart, targetURLPart)
if err != nil {
return false, err
}
if !matched {
// glob mismatch for some part
return false, nil
}
}
// everything matches
return true, nil
}
Loading

0 comments on commit b64783a

Please sign in to comment.