mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 05:57:25 +00:00
kubelet: implement image pull policies
This commit is contained in:
parent
64c0164cec
commit
ad96b3aed5
171
pkg/kubelet/images/image_pull_policies.go
Normal file
171
pkg/kubelet/images/image_pull_policies.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 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 images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dockerref "github.com/distribution/reference"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImagePullPolicyEnforcer defines a class of functions implementing a credential
|
||||||
|
// verification policies for image pulls. These function determines whether the
|
||||||
|
// implemented policy requires credential verification based on image name, local
|
||||||
|
// image presence and existence of records about previous image pulls.
|
||||||
|
//
|
||||||
|
// `image` is an image name from a Pod's container "image" field.
|
||||||
|
// `imagePresent` informs whether the `image` is present on the node.
|
||||||
|
// `imagePulledByKubelet` marks that ImagePulledRecord or ImagePullingIntent records
|
||||||
|
// for the `image` exist on the node, meaning it was pulled by the kubelet somewhere
|
||||||
|
// in the past.
|
||||||
|
type ImagePullPolicyEnforcer interface {
|
||||||
|
RequireCredentialVerificationForImage(image string, imagePulledByKubelet bool) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagePullPolicyEnforcerFunc is a function type that implements the ImagePullPolicyEnforcer interface
|
||||||
|
type ImagePullPolicyEnforcerFunc func(image string, imagePulledByKubelet bool) bool
|
||||||
|
|
||||||
|
func (e ImagePullPolicyEnforcerFunc) RequireCredentialVerificationForImage(image string, imagePulledByKubelet bool) bool {
|
||||||
|
return e(image, imagePulledByKubelet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImagePullCredentialVerificationPolicy(policy kubeletconfiginternal.ImagePullCredentialsVerificationPolicy, imageAllowList []string) (ImagePullPolicyEnforcer, error) {
|
||||||
|
switch policy {
|
||||||
|
case kubeletconfiginternal.NeverVerify:
|
||||||
|
return NeverVerifyImagePullPolicy(), nil
|
||||||
|
case kubeletconfiginternal.NeverVerifyPreloadedImages:
|
||||||
|
return NeverVerifyPreloadedPullPolicy(), nil
|
||||||
|
case kubeletconfiginternal.NeverVerifyAllowlistedImages:
|
||||||
|
return NewNeverVerifyAllowListedPullPolicy(imageAllowList)
|
||||||
|
case kubeletconfiginternal.AlwaysVerify:
|
||||||
|
return AlwaysVerifyImagePullPolicy(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown image pull credential verification policy: %v", policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NeverVerifyImagePullPolicy() ImagePullPolicyEnforcerFunc {
|
||||||
|
return func(image string, imagePulledByKubelet bool) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NeverVerifyPreloadedPullPolicy() ImagePullPolicyEnforcerFunc {
|
||||||
|
return func(image string, imagePulledByKubelet bool) bool {
|
||||||
|
return imagePulledByKubelet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlwaysVerifyImagePullPolicy() ImagePullPolicyEnforcerFunc {
|
||||||
|
return func(image string, imagePulledByKubelet bool) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeverVerifyAllowlistedImages struct {
|
||||||
|
absoluteURLs sets.Set[string]
|
||||||
|
prefixes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNeverVerifyAllowListedPullPolicy(allowList []string) (*NeverVerifyAllowlistedImages, error) {
|
||||||
|
policy := &NeverVerifyAllowlistedImages{
|
||||||
|
absoluteURLs: sets.New[string](),
|
||||||
|
}
|
||||||
|
for _, pattern := range allowList {
|
||||||
|
normalizedPattern, isWildcard, err := getAllowlistImagePattern(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isWildcard {
|
||||||
|
policy.prefixes = append(policy.prefixes, normalizedPattern)
|
||||||
|
} else {
|
||||||
|
policy.absoluteURLs.Insert(normalizedPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NeverVerifyAllowlistedImages) RequireCredentialVerificationForImage(image string, imagePulledByKubelet bool) bool {
|
||||||
|
return !p.imageMatches(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NeverVerifyAllowlistedImages) imageMatches(image string) bool {
|
||||||
|
if p.absoluteURLs.Has(image) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, prefix := range p.prefixes {
|
||||||
|
if strings.HasPrefix(image, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateAllowlistImagesPatterns(patterns []string) error {
|
||||||
|
for _, p := range patterns {
|
||||||
|
if _, _, err := getAllowlistImagePattern(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllowlistImagePattern(pattern string) (string, bool, error) {
|
||||||
|
if pattern != strings.TrimSpace(pattern) {
|
||||||
|
return "", false, fmt.Errorf("leading/trailing spaces are not allowed: %s", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedPattern := pattern
|
||||||
|
isWildcard := false
|
||||||
|
if strings.HasSuffix(pattern, "/*") {
|
||||||
|
isWildcard = true
|
||||||
|
trimmedPattern = strings.TrimSuffix(trimmedPattern, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trimmedPattern) == 0 {
|
||||||
|
return "", false, fmt.Errorf("the supplied pattern is too short: %s", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ContainsRune(trimmedPattern, '*') {
|
||||||
|
return "", false, fmt.Errorf("not a valid wildcard pattern, only patterns ending with '/*' are allowed: %s", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isWildcard {
|
||||||
|
if len(trimmedPattern) == 1 {
|
||||||
|
return "", false, fmt.Errorf("at least registry hostname is required")
|
||||||
|
}
|
||||||
|
} else { // not a wildcard
|
||||||
|
image, err := dockerref.ParseNormalizedNamed(trimmedPattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to parse as an image name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmedPattern != image.Name() { // image.Name() returns the image name without tag/digest
|
||||||
|
return "", false, fmt.Errorf("neither tag nor digest is accepted in an image reference: %s", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedPattern, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedPattern, true, nil
|
||||||
|
}
|
188
pkg/kubelet/images/image_pull_policies_test.go
Normal file
188
pkg/kubelet/images/image_pull_policies_test.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 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 images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNeverVerifyPreloadedPullPolicy(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
imageRecordsExist bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are records about the image being pulled",
|
||||||
|
imageRecordsExist: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := NeverVerifyPreloadedPullPolicy()("test-image", tt.imageRecordsExist); got != tt.want {
|
||||||
|
t.Errorf("NeverVerifyPreloadedPullPolicy() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNeverVerifyAllowListedPullPolicy(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
imageRecordsExist bool
|
||||||
|
allowlist []string
|
||||||
|
expectedAbsolutes int
|
||||||
|
expectedWildcards int
|
||||||
|
want bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled, not in allowlist",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: true,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "test.io/test/image3"},
|
||||||
|
expectedAbsolutes: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are records about the image being pulled, not in allowlist",
|
||||||
|
imageRecordsExist: true,
|
||||||
|
want: true,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image3", "test.io/test/image2", "test.io/test/image3"},
|
||||||
|
expectedAbsolutes: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled, appears in allowlist",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: false,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "test.io/test/test-image", "test.io/test/image3"},
|
||||||
|
expectedAbsolutes: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are records about the image being pulled, appears in allowlist",
|
||||||
|
imageRecordsExist: true,
|
||||||
|
want: false,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "test.io/test/test-image", "test.io/test/image3"},
|
||||||
|
expectedAbsolutes: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - wildcard in the middle",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"image.repo/pokus*/imagename"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - trailing non-segment wildcard middle",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"image.repo/pokus*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - wildcard path segment in the middle",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"image.repo/*/imagename"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - only wildcard segment",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"/*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - ends with a '/'",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"image.repo/"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - empty",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - asterisk",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - image with a tag",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"test.io/test/image1:tagged"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - image with a digest",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"test.io/test/image1@sha256:38a8906435c4dd5f4258899d46621bfd8eea3ad6ff494ee3c2f17ef0321625bd"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid allowlist pattern - trailing whitespace",
|
||||||
|
wantErr: true,
|
||||||
|
allowlist: []string{"test.io/test/image1 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled, not in allowlist - different repo wildcard",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: true,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "different.repo/test/*"},
|
||||||
|
expectedAbsolutes: 2,
|
||||||
|
expectedWildcards: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled, not in allowlist - matches org wildcard",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: false,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "test.io/test/*"},
|
||||||
|
expectedAbsolutes: 2,
|
||||||
|
expectedWildcards: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "there are no records about the image being pulled, not in allowlist - matches repo wildcard",
|
||||||
|
imageRecordsExist: false,
|
||||||
|
want: false,
|
||||||
|
allowlist: []string{"test.io/test/image1", "test.io/test/image2", "test.io/*"},
|
||||||
|
expectedAbsolutes: 2,
|
||||||
|
expectedWildcards: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
policyEnforcer, err := NewNeverVerifyAllowListedPullPolicy(tt.allowlist)
|
||||||
|
if tt.wantErr != (err != nil) {
|
||||||
|
t.Fatalf("wanted error: %t, got: %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policyEnforcer.absoluteURLs) != tt.expectedAbsolutes {
|
||||||
|
t.Errorf("expected %d of absolute image URLs in the allowlist policy, got %d: %v", tt.expectedAbsolutes, len(policyEnforcer.absoluteURLs), policyEnforcer.absoluteURLs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(policyEnforcer.prefixes) != tt.expectedWildcards {
|
||||||
|
t.Errorf("expected %d of wildcard image URLs in the allowlist policy, got %d: %v", tt.expectedWildcards, len(policyEnforcer.prefixes), policyEnforcer.prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := policyEnforcer.RequireCredentialVerificationForImage("test.io/test/test-image", tt.imageRecordsExist)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("NewNeverVerifyAllowListedPullPolicy() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user