mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +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