Skip to content

Commit

Permalink
Merge pull request #94196 from andrewsykim/registry-creds
Browse files Browse the repository at this point in the history
kubelet: add alpha credential provider plugins
  • Loading branch information
k8s-ci-robot authored Nov 12, 2020
2 parents a7ae99d + f3192c3 commit d233111
Show file tree
Hide file tree
Showing 51 changed files with 3,277 additions and 18 deletions.
4 changes: 4 additions & 0 deletions api/api-rules/violation_exceptions.list
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ API rule violation: list_type_missing,k8s.io/kube-scheduler/config/v1,Policy,Pri
API rule violation: list_type_missing,k8s.io/kube-scheduler/config/v1,RequestedToCapacityRatioArguments,Resources
API rule violation: list_type_missing,k8s.io/kube-scheduler/config/v1,RequestedToCapacityRatioArguments,Shape
API rule violation: list_type_missing,k8s.io/kube-scheduler/config/v1,ServiceAffinity,Labels
API rule violation: list_type_missing,k8s.io/kubelet/config/v1alpha1,CredentialProvider,Args
API rule violation: list_type_missing,k8s.io/kubelet/config/v1alpha1,CredentialProvider,Env
API rule violation: list_type_missing,k8s.io/kubelet/config/v1alpha1,CredentialProvider,MatchImages
API rule violation: list_type_missing,k8s.io/kubelet/config/v1alpha1,CredentialProviderConfig,Providers
API rule violation: list_type_missing,k8s.io/kubelet/config/v1beta1,KubeletConfiguration,AllowedUnsafeSysctls
API rule violation: list_type_missing,k8s.io/kubelet/config/v1beta1,KubeletConfiguration,ClusterDNS
API rule violation: list_type_missing,k8s.io/kubelet/config/v1beta1,KubeletConfiguration,EnforceNodeAllocatable
Expand Down
2 changes: 2 additions & 0 deletions build/kazel_generated.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ tags_values_pkgs = {"openapi-gen": {
"staging/src/k8s.io/kube-proxy/config/v1alpha1",
"staging/src/k8s.io/kube-scheduler/config/v1",
"staging/src/k8s.io/kube-scheduler/config/v1beta1",
"staging/src/k8s.io/kubelet/config/v1alpha1",
"staging/src/k8s.io/kubelet/config/v1beta1",
"staging/src/k8s.io/metrics/pkg/apis/custom_metrics/v1beta1",
"staging/src/k8s.io/metrics/pkg/apis/custom_metrics/v1beta2",
Expand Down Expand Up @@ -188,6 +189,7 @@ tags_pkgs_values = {"openapi-gen": {
"staging/src/k8s.io/kube-proxy/config/v1alpha1": ["true"],
"staging/src/k8s.io/kube-scheduler/config/v1": ["true"],
"staging/src/k8s.io/kube-scheduler/config/v1beta1": ["true"],
"staging/src/k8s.io/kubelet/config/v1alpha1": ["true"],
"staging/src/k8s.io/kubelet/config/v1beta1": ["true"],
"staging/src/k8s.io/metrics/pkg/apis/custom_metrics/v1beta1": ["true"],
"staging/src/k8s.io/metrics/pkg/apis/custom_metrics/v1beta2": ["true"],
Expand Down
6 changes: 6 additions & 0 deletions cmd/kubelet/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,8 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
kubeServer.CloudProvider,
kubeServer.CertDirectory,
kubeServer.RootDirectory,
kubeServer.ImageCredentialProviderConfigFile,
kubeServer.ImageCredentialProviderBinDir,
kubeServer.RegisterNode,
kubeServer.RegisterWithTaints,
kubeServer.AllowedUnsafeSysctls,
Expand Down Expand Up @@ -1204,6 +1206,8 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
cloudProvider string,
certDirectory string,
rootDirectory string,
imageCredentialProviderConfigFile string,
imageCredentialProviderBinDir string,
registerNode bool,
registerWithTaints []api.Taint,
allowedUnsafeSysctls []string,
Expand Down Expand Up @@ -1235,6 +1239,8 @@ func createAndInitKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
cloudProvider,
certDirectory,
rootDirectory,
imageCredentialProviderConfigFile,
imageCredentialProviderBinDir,
registerNode,
registerWithTaints,
allowedUnsafeSysctls,
Expand Down
3 changes: 3 additions & 0 deletions hack/.golint_failures
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pkg/controller/volume/persistentvolume
pkg/controller/volume/persistentvolume/config/v1alpha1
pkg/controlplane/controller/crdregistration
pkg/controlplane/tunneler
pkg/credentialprovider/plugin
pkg/features
pkg/kubeapiserver
pkg/kubectl/cmd/convert
Expand Down Expand Up @@ -455,6 +456,8 @@ staging/src/k8s.io/kubectl/pkg/polymorphichelpers
staging/src/k8s.io/kubectl/pkg/scale
staging/src/k8s.io/kubectl/pkg/util/templates
staging/src/k8s.io/kubelet/config/v1beta1
staging/src/k8s.io/kubelet/pkg/apis/credentialprovider
staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1
staging/src/k8s.io/legacy-cloud-providers/vsphere
staging/src/k8s.io/metrics/pkg/apis/custom_metrics
staging/src/k8s.io/metrics/pkg/apis/custom_metrics/v1beta1
Expand Down
1 change: 1 addition & 0 deletions pkg/credentialprovider/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ filegroup(
"//pkg/credentialprovider/aws:all-srcs",
"//pkg/credentialprovider/azure:all-srcs",
"//pkg/credentialprovider/gcp:all-srcs",
"//pkg/credentialprovider/plugin:all-srcs",
"//pkg/credentialprovider/secrets:all-srcs",
],
tags = ["automanaged"],
Expand Down
27 changes: 14 additions & 13 deletions pkg/credentialprovider/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,10 @@ func isDefaultRegistryMatch(image string) bool {
return !strings.ContainsAny(parts[0], ".:")
}

// 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) {
func ParseSchemelessURL(schemelessURL string) (*url.URL, error) {
parsed, err := url.Parse("https://" + schemelessURL)
if err != nil {
return nil, err
Expand All @@ -170,8 +171,8 @@ func parseSchemelessURL(schemelessURL string) (*url.URL, error) {
return parsed, nil
}

// split the host name into parts, as well as the port
func splitURL(url *url.URL) (parts []string, port string) {
// SplitURL splits the host name into parts, as well as the port
func SplitURL(url *url.URL) (parts []string, port string) {
host, port, err := net.SplitHostPort(url.Host)
if err != nil {
// could not parse port
Expand All @@ -180,30 +181,30 @@ func splitURL(url *url.URL) (parts []string, port string) {
return strings.Split(host, "."), port
}

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

// check whether the given target url matches the glob url, which may have
// 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 *url.URL, targetURL *url.URL) (bool, error) {
globURLParts, globPort := splitURL(globURL)
targetURLParts, targetPort := splitURL(targetURL)
func URLsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
globURLParts, globPort := SplitURL(globURL)
targetURLParts, targetPort := SplitURL(targetURL)
if globPort != targetPort {
// port doesn't match
return false, nil
Expand Down Expand Up @@ -240,7 +241,7 @@ func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
for _, k := range dk.index {
// both k and image are schemeless URLs because even though schemes are allowed
// in the credential configurations, we remove them in Add.
if matched, _ := urlsMatchStr(k, image); matched {
if matched, _ := URLsMatchStr(k, image); matched {
ret = append(ret, dk.creds[k]...)
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/credentialprovider/keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"testing"
)

func TestUrlsMatch(t *testing.T) {
func TestURLsMatch(t *testing.T) {
tests := []struct {
globURL string
targetURL string
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestUrlsMatch(t *testing.T) {
},
}
for _, test := range tests {
matched, _ := urlsMatchStr(test.globURL, test.targetURL)
matched, _ := URLsMatchStr(test.globURL, test.targetURL)
if matched != test.matchExpected {
t.Errorf("Expected match result of %s and %s to be %t, but was %t",
test.globURL, test.targetURL, test.matchExpected, matched)
Expand Down
58 changes: 58 additions & 0 deletions pkg/credentialprovider/plugin/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = [
"config.go",
"plugin.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/plugin",
visibility = ["//visibility:public"],
deps = [
"//pkg/credentialprovider:go_default_library",
"//pkg/kubelet/apis/config:go_default_library",
"//pkg/kubelet/apis/config/v1alpha1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider:go_default_library",
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/install:go_default_library",
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

go_test(
name = "go_default_test",
srcs = [
"config_test.go",
"plugin_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/credentialprovider:go_default_library",
"//pkg/kubelet/apis/config:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider:go_default_library",
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1:go_default_library",
],
)
128 changes: 128 additions & 0 deletions pkg/credentialprovider/plugin/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2020 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.
*/

package plugin

import (
"fmt"
"io/ioutil"
"strings"

"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/credentialprovider"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
)

// readCredentialProviderConfigFile receives a path to a config file and decodes it
// into the internal CredentialProviderConfig type.
func readCredentialProviderConfigFile(configPath string) (*kubeletconfig.CredentialProviderConfig, error) {
if configPath == "" {
return nil, fmt.Errorf("credential provider config path is empty")
}

data, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("unable to read external registry credential provider configuration from %q: %v", configPath, err)
}

config, err := decode(data)
if err != nil {
return nil, fmt.Errorf("error decoding config %s: %v", configPath, err)
}

return config, nil
}

// decode decodes data into the internal CredentialProviderConfig type.
func decode(data []byte) (*kubeletconfig.CredentialProviderConfig, error) {
obj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil)
if err != nil {
return nil, err
}

if gvk.Kind != "CredentialProviderConfig" {
return nil, fmt.Errorf("failed to decode %q (wrong Kind)", gvk.Kind)
}

if gvk.Group != kubeletconfig.GroupName {
return nil, fmt.Errorf("failed to decode CredentialProviderConfig, unexpected Group: %s", gvk.Group)
}

if internalConfig, ok := obj.(*kubeletconfig.CredentialProviderConfig); ok {
return internalConfig, nil
}

return nil, fmt.Errorf("unable to convert %T to *CredentialProviderConfig", obj)
}

// validateCredentialProviderConfig validates CredentialProviderConfig.
func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderConfig) field.ErrorList {
allErrs := field.ErrorList{}

if len(config.Providers) == 0 {
allErrs = append(allErrs, field.Required(field.NewPath("providers"), "at least 1 item in plugins is required"))
}

fieldPath := field.NewPath("providers")
for _, provider := range config.Providers {
if strings.Contains(provider.Name, "/") {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain '/'"))
}

if strings.Contains(provider.Name, " ") {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain spaces"))
}

if provider.Name == "." {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '.'"))
}

if provider.Name == ".." {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '..'"))
}

if provider.APIVersion == "" {
allErrs = append(allErrs, field.Required(fieldPath.Child("apiVersion"), "apiVersion is required"))
} else if _, ok := apiVersions[provider.APIVersion]; !ok {
validAPIVersions := []string{}
for apiVersion := range apiVersions {
validAPIVersions = append(validAPIVersions, apiVersion)
}

allErrs = append(allErrs, field.NotSupported(fieldPath.Child("apiVersion"), provider.APIVersion, validAPIVersions))
}

if len(provider.MatchImages) == 0 {
allErrs = append(allErrs, field.Required(fieldPath.Child("matchImages"), "at least 1 item in matchImages is required"))
}

for _, matchImage := range provider.MatchImages {
if _, err := credentialprovider.ParseSchemelessURL(matchImage); err != nil {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("matchImages"), matchImage, fmt.Sprintf("match image is invalid: %s", err.Error())))
}
}

if provider.DefaultCacheDuration == nil {
allErrs = append(allErrs, field.Required(fieldPath.Child("defaultCacheDuration"), "defaultCacheDuration is required"))
}

if provider.DefaultCacheDuration != nil && provider.DefaultCacheDuration.Duration < 0 {
allErrs = append(allErrs, field.Invalid(fieldPath.Child("defaultCacheDuration"), provider.DefaultCacheDuration.Duration, "defaultCacheDuration must be greater than or equal to 0"))
}
}

return allErrs
}
Loading

0 comments on commit d233111

Please sign in to comment.