mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-08 16:30:57 -04:00
Merge pull request #128152 from stlaz/ensure-secret-images
Multi-tenancy in accessing node images via Pod API
This commit is contained in:
commit
fcb2418f7b
57 changed files with 4467 additions and 512 deletions
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
package credentialprovider
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
|
@ -24,7 +27,9 @@ import (
|
|||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// DockerKeyring tracks a set of docker registry credentials, maintaining a
|
||||
|
|
@ -35,13 +40,13 @@ import (
|
|||
// most specific match for a given image
|
||||
// - iterating a map does not yield predictable results
|
||||
type DockerKeyring interface {
|
||||
Lookup(image string) ([]AuthConfig, bool)
|
||||
Lookup(image string) ([]TrackedAuthConfig, bool)
|
||||
}
|
||||
|
||||
// BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
|
||||
type BasicDockerKeyring struct {
|
||||
index []string
|
||||
creds map[string][]AuthConfig
|
||||
creds map[string][]TrackedAuthConfig
|
||||
}
|
||||
|
||||
// providersDockerKeyring is an implementation of DockerKeyring that
|
||||
|
|
@ -50,6 +55,47 @@ type providersDockerKeyring struct {
|
|||
Providers []DockerConfigProvider
|
||||
}
|
||||
|
||||
// TrackedAuthConfig wraps the AuthConfig and adds information about the source
|
||||
// of the credentials.
|
||||
type TrackedAuthConfig struct {
|
||||
AuthConfig
|
||||
AuthConfigHash string
|
||||
|
||||
Source *CredentialSource
|
||||
}
|
||||
|
||||
// NewTrackedAuthConfig initializes the TrackedAuthConfig structure by adding
|
||||
// the source information to the supplied AuthConfig. It also counts a hash of the
|
||||
// AuthConfig and keeps it in the returned structure.
|
||||
//
|
||||
// The supplied CredentialSource is only used when the "KubeletEnsureSecretPulledImages"
|
||||
// is enabled, the same applies for counting the hash.
|
||||
func NewTrackedAuthConfig(c *AuthConfig, src *CredentialSource) *TrackedAuthConfig {
|
||||
if c == nil {
|
||||
panic("cannot construct TrackedAuthConfig with a nil AuthConfig")
|
||||
}
|
||||
|
||||
authConfig := &TrackedAuthConfig{
|
||||
AuthConfig: *c,
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
authConfig.Source = src
|
||||
authConfig.AuthConfigHash = hashAuthConfig(c)
|
||||
}
|
||||
return authConfig
|
||||
}
|
||||
|
||||
type CredentialSource struct {
|
||||
Secret SecretCoordinates
|
||||
}
|
||||
|
||||
type SecretCoordinates struct {
|
||||
UID string
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
// AuthConfig contains authorization information for connecting to a Registry
|
||||
// This type mirrors "github.com/docker/docker/api/types.AuthConfig"
|
||||
type AuthConfig struct {
|
||||
|
|
@ -72,11 +118,13 @@ type AuthConfig struct {
|
|||
RegistryToken string `json:"registrytoken,omitempty"`
|
||||
}
|
||||
|
||||
// Add add some docker config in basic docker keyring
|
||||
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
|
||||
// Add inserts the docker config `cfg` into the basic docker keyring. It attaches
|
||||
// the `src` information that describes where the docker config `cfg` comes from.
|
||||
// `src` is nil if the docker config is globally available on the node.
|
||||
func (dk *BasicDockerKeyring) Add(src *CredentialSource, cfg DockerConfig) {
|
||||
if dk.index == nil {
|
||||
dk.index = make([]string, 0)
|
||||
dk.creds = make(map[string][]AuthConfig)
|
||||
dk.creds = make(map[string][]TrackedAuthConfig)
|
||||
}
|
||||
for loc, ident := range cfg {
|
||||
creds := AuthConfig{
|
||||
|
|
@ -111,7 +159,9 @@ func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
|
|||
} else {
|
||||
key = parsed.Host
|
||||
}
|
||||
dk.creds[key] = append(dk.creds[key], creds)
|
||||
trackedCreds := NewTrackedAuthConfig(&creds, src)
|
||||
|
||||
dk.creds[key] = append(dk.creds[key], *trackedCreds)
|
||||
dk.index = append(dk.index, key)
|
||||
}
|
||||
|
||||
|
|
@ -235,9 +285,9 @@ func URLsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
|
|||
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
|
||||
// Multiple credentials may be returned if there are multiple potentially valid credentials
|
||||
// available. This allows for rotation.
|
||||
func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
||||
func (dk *BasicDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
|
||||
// range over the index as iterating over a map does not provide a predictable ordering
|
||||
ret := []AuthConfig{}
|
||||
ret := []TrackedAuthConfig{}
|
||||
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.
|
||||
|
|
@ -257,16 +307,18 @@ func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
return []AuthConfig{}, false
|
||||
return []TrackedAuthConfig{}, false
|
||||
}
|
||||
|
||||
// Lookup implements the DockerKeyring method for fetching credentials
|
||||
// based on image name.
|
||||
func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
||||
func (dk *providersDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
|
||||
keyring := &BasicDockerKeyring{}
|
||||
|
||||
for _, p := range dk.Providers {
|
||||
keyring.Add(p.Provide(image))
|
||||
// TODO: the source should probably change once we depend on service accounts (KEP-4412).
|
||||
// Perhaps `Provide()` should return the source modified to accommodate this?
|
||||
keyring.Add(nil, p.Provide(image))
|
||||
}
|
||||
|
||||
return keyring.Lookup(image)
|
||||
|
|
@ -274,13 +326,13 @@ func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
|||
|
||||
// FakeKeyring a fake config credentials
|
||||
type FakeKeyring struct {
|
||||
auth []AuthConfig
|
||||
auth []TrackedAuthConfig
|
||||
ok bool
|
||||
}
|
||||
|
||||
// Lookup implements the DockerKeyring method for fetching credentials based on image name
|
||||
// return fake auth and ok
|
||||
func (f *FakeKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
||||
func (f *FakeKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
|
||||
return f.auth, f.ok
|
||||
}
|
||||
|
||||
|
|
@ -289,8 +341,8 @@ type UnionDockerKeyring []DockerKeyring
|
|||
|
||||
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
|
||||
// return each credentials
|
||||
func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
||||
authConfigs := []AuthConfig{}
|
||||
func (k UnionDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
|
||||
authConfigs := []TrackedAuthConfig{}
|
||||
for _, subKeyring := range k {
|
||||
if subKeyring == nil {
|
||||
continue
|
||||
|
|
@ -302,3 +354,14 @@ func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
|
|||
|
||||
return authConfigs, (len(authConfigs) > 0)
|
||||
}
|
||||
|
||||
func hashAuthConfig(creds *AuthConfig) string {
|
||||
credBytes, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(credBytes))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import (
|
|||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func TestURLsMatch(t *testing.T) {
|
||||
|
|
@ -222,7 +226,7 @@ func TestDockerKeyringForGlob(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
creds, ok := keyring.Lookup(test.targetURL + "/foo/bar")
|
||||
|
|
@ -290,7 +294,7 @@ func TestKeyringMiss(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
_, ok := keyring.Lookup(test.lookupURL + "/foo/bar")
|
||||
|
|
@ -318,7 +322,7 @@ func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
val, ok := keyring.Lookup("world.mesos.org/foo/bar")
|
||||
|
|
@ -344,7 +348,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
creds, ok := keyring.Lookup("google/docker-registry")
|
||||
|
|
@ -353,7 +357,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if len(creds) > 1 {
|
||||
t.Errorf("Got more hits than expected: %s", creds)
|
||||
t.Errorf("Got more hits than expected: %v", creds)
|
||||
}
|
||||
val := creds[0]
|
||||
|
||||
|
|
@ -385,7 +389,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
creds, ok := keyring.Lookup("jenkins")
|
||||
|
|
@ -394,7 +398,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if len(creds) > 1 {
|
||||
t.Errorf("Got more hits than expected: %s", creds)
|
||||
t.Errorf("Got more hits than expected: %v", creds)
|
||||
}
|
||||
val := creds[0]
|
||||
|
||||
|
|
@ -426,7 +430,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
|
|||
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
|
||||
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
|
||||
} else {
|
||||
keyring.Add(cfg)
|
||||
keyring.Add(nil, cfg)
|
||||
}
|
||||
|
||||
creds, ok := keyring.Lookup(url + "/google/docker-registry")
|
||||
|
|
@ -435,7 +439,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if len(creds) > 2 {
|
||||
t.Errorf("Got more hits than expected: %s", creds)
|
||||
t.Errorf("Got more hits than expected: %v", creds)
|
||||
}
|
||||
val := creds[0]
|
||||
|
||||
|
|
@ -498,20 +502,24 @@ func TestProvidersDockerKeyring(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDockerKeyringLookup(t *testing.T) {
|
||||
// turn on the ensure secret pulled images feature to get the hashes with the creds
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
|
||||
ada := AuthConfig{
|
||||
Username: "ada",
|
||||
Password: "smash", // Fake value for testing.
|
||||
Email: "ada@example.com",
|
||||
}
|
||||
adaHash := "353258b53f5e9a57b059eab3f05312fc35bbeb874f08ce101e7bf0bf46977423"
|
||||
|
||||
grace := AuthConfig{
|
||||
Username: "grace",
|
||||
Password: "squash", // Fake value for testing.
|
||||
Email: "grace@example.com",
|
||||
}
|
||||
graceHash := "f949b3837a1eb733a951b6aeda0b3327c09ec50c917de9ca35818e8fbf567e29"
|
||||
|
||||
dk := &BasicDockerKeyring{}
|
||||
dk.Add(DockerConfig{
|
||||
dk.Add(nil, DockerConfig{
|
||||
"bar.example.com/pong": DockerConfigEntry{
|
||||
Username: grace.Username,
|
||||
Password: grace.Password,
|
||||
|
|
@ -526,27 +534,27 @@ func TestDockerKeyringLookup(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
image string
|
||||
match []AuthConfig
|
||||
match []TrackedAuthConfig
|
||||
ok bool
|
||||
}{
|
||||
// direct match
|
||||
{"bar.example.com", []AuthConfig{ada}, true},
|
||||
{"bar.example.com", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
|
||||
|
||||
// direct match deeper than other possible matches
|
||||
{"bar.example.com/pong", []AuthConfig{grace, ada}, true},
|
||||
{"bar.example.com/pong", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
|
||||
|
||||
// no direct match, deeper path ignored
|
||||
{"bar.example.com/ping", []AuthConfig{ada}, true},
|
||||
{"bar.example.com/ping", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
|
||||
|
||||
// match first part of path token
|
||||
{"bar.example.com/pongz", []AuthConfig{grace, ada}, true},
|
||||
{"bar.example.com/pongz", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
|
||||
|
||||
// match regardless of sub-path
|
||||
{"bar.example.com/pong/pang", []AuthConfig{grace, ada}, true},
|
||||
{"bar.example.com/pong/pang", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
|
||||
|
||||
// no host match
|
||||
{"example.com", []AuthConfig{}, false},
|
||||
{"foo.example.com", []AuthConfig{}, false},
|
||||
{"example.com", []TrackedAuthConfig{}, false},
|
||||
{"foo.example.com", []TrackedAuthConfig{}, false},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
|
|
@ -565,14 +573,17 @@ func TestDockerKeyringLookup(t *testing.T) {
|
|||
// by images that only match the hostname.
|
||||
// NOTE: the above covers the case of a more specific match trumping just hostname.
|
||||
func TestIssue3797(t *testing.T) {
|
||||
// turn on the ensure secret pulled images feature to get the hashes with the creds
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
|
||||
rex := AuthConfig{
|
||||
Username: "rex",
|
||||
Password: "tiny arms", // Fake value for testing.
|
||||
Email: "rex@example.com",
|
||||
}
|
||||
rexHash := "899748fec74c8dd761845fca727f4249b05be275ff24026676fcd4351f656363"
|
||||
|
||||
dk := &BasicDockerKeyring{}
|
||||
dk.Add(DockerConfig{
|
||||
dk.Add(nil, DockerConfig{
|
||||
"https://quay.io/v1/": DockerConfigEntry{
|
||||
Username: rex.Username,
|
||||
Password: rex.Password,
|
||||
|
|
@ -582,15 +593,15 @@ func TestIssue3797(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
image string
|
||||
match []AuthConfig
|
||||
match []TrackedAuthConfig
|
||||
ok bool
|
||||
}{
|
||||
// direct match
|
||||
{"quay.io", []AuthConfig{rex}, true},
|
||||
{"quay.io", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
|
||||
|
||||
// partial matches
|
||||
{"quay.io/foo", []AuthConfig{rex}, true},
|
||||
{"quay.io/foo/bar", []AuthConfig{rex}, true},
|
||||
{"quay.io/foo", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
|
||||
{"quay.io/foo/bar", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -87,11 +87,12 @@ func NewExternalCredentialProviderDockerKeyring(podNamespace, podName, podUID, s
|
|||
return keyring
|
||||
}
|
||||
|
||||
func (k *externalCredentialProviderKeyring) Lookup(image string) ([]credentialprovider.AuthConfig, bool) {
|
||||
func (k *externalCredentialProviderKeyring) Lookup(image string) ([]credentialprovider.TrackedAuthConfig, bool) {
|
||||
keyring := &credentialprovider.BasicDockerKeyring{}
|
||||
|
||||
for _, p := range k.providers {
|
||||
keyring.Add(p.Provide(image))
|
||||
// TODO: modify the credentialprovider.CredentialSource to contain the SA/pod information
|
||||
keyring.Add(nil, p.Provide(image))
|
||||
}
|
||||
|
||||
return keyring.Lookup(image)
|
||||
|
|
|
|||
|
|
@ -27,32 +27,52 @@ import (
|
|||
// then a DockerKeyring is built based on every hit and unioned with the defaultKeyring.
|
||||
// If they do not, then the default keyring is returned
|
||||
func MakeDockerKeyring(passedSecrets []v1.Secret, defaultKeyring credentialprovider.DockerKeyring) (credentialprovider.DockerKeyring, error) {
|
||||
passedCredentials := []credentialprovider.DockerConfig{}
|
||||
for _, passedSecret := range passedSecrets {
|
||||
providerFromSecrets, err := secretsToTrackedDockerConfigs(passedSecrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if providerFromSecrets == nil {
|
||||
return defaultKeyring, nil
|
||||
}
|
||||
|
||||
return credentialprovider.UnionDockerKeyring{providerFromSecrets, defaultKeyring}, nil
|
||||
}
|
||||
|
||||
func secretsToTrackedDockerConfigs(secrets []v1.Secret) (credentialprovider.DockerKeyring, error) {
|
||||
provider := &credentialprovider.BasicDockerKeyring{}
|
||||
validSecretsFound := 0
|
||||
for _, passedSecret := range secrets {
|
||||
if dockerConfigJSONBytes, dockerConfigJSONExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJSONExists && (len(dockerConfigJSONBytes) > 0) {
|
||||
dockerConfigJSON := credentialprovider.DockerConfigJSON{}
|
||||
if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passedCredentials = append(passedCredentials, dockerConfigJSON.Auths)
|
||||
coords := credentialprovider.SecretCoordinates{
|
||||
UID: string(passedSecret.UID),
|
||||
Namespace: passedSecret.Namespace,
|
||||
Name: passedSecret.Name}
|
||||
|
||||
provider.Add(&credentialprovider.CredentialSource{Secret: coords}, dockerConfigJSON.Auths)
|
||||
validSecretsFound++
|
||||
} else if dockercfgBytes, dockercfgExists := passedSecret.Data[v1.DockerConfigKey]; (passedSecret.Type == v1.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) {
|
||||
dockercfg := credentialprovider.DockerConfig{}
|
||||
if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passedCredentials = append(passedCredentials, dockercfg)
|
||||
coords := credentialprovider.SecretCoordinates{
|
||||
UID: string(passedSecret.UID),
|
||||
Namespace: passedSecret.Namespace,
|
||||
Name: passedSecret.Name}
|
||||
provider.Add(&credentialprovider.CredentialSource{Secret: coords}, dockercfg)
|
||||
validSecretsFound++
|
||||
}
|
||||
}
|
||||
|
||||
if len(passedCredentials) > 0 {
|
||||
basicKeyring := &credentialprovider.BasicDockerKeyring{}
|
||||
for _, currCredentials := range passedCredentials {
|
||||
basicKeyring.Add(currCredentials)
|
||||
}
|
||||
return credentialprovider.UnionDockerKeyring{basicKeyring, defaultKeyring}, nil
|
||||
if validSecretsFound == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return defaultKeyring, nil
|
||||
return provider, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,107 +20,145 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// fakeKeyring is a fake docker auth config keyring
|
||||
type fakeKeyring struct {
|
||||
auth []credentialprovider.AuthConfig
|
||||
// FakeKeyring a fake config credentials
|
||||
type FakeKeyring struct {
|
||||
auth []credentialprovider.TrackedAuthConfig
|
||||
ok bool
|
||||
}
|
||||
|
||||
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
|
||||
// Returns fake results based on the auth and ok fields in fakeKeyring
|
||||
func (f *fakeKeyring) Lookup(image string) ([]credentialprovider.AuthConfig, bool) {
|
||||
// Lookup implements the DockerKeyring method for fetching credentials based on image name
|
||||
// return fake auth and ok
|
||||
func (f *FakeKeyring) Lookup(image string) ([]credentialprovider.TrackedAuthConfig, bool) {
|
||||
return f.auth, f.ok
|
||||
}
|
||||
|
||||
func Test_MakeDockerKeyring(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
image string
|
||||
defaultKeyring credentialprovider.DockerKeyring
|
||||
pullSecrets []v1.Secret
|
||||
authConfigs []credentialprovider.AuthConfig
|
||||
found bool
|
||||
name string
|
||||
image string
|
||||
defaultKeyring credentialprovider.DockerKeyring
|
||||
pullSecrets []v1.Secret
|
||||
expectedAuthConfigs []credentialprovider.TrackedAuthConfig
|
||||
found bool
|
||||
}{
|
||||
{
|
||||
name: "with .dockerconfigjson and auth field",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
name: "with .dockerconfigjson and auth field",
|
||||
image: "test.registry.io",
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Source: &credentialprovider.CredentialSource{
|
||||
Secret: credentialprovider.SecretCoordinates{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
},
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "with .dockerconfig and auth field",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
name: "with .dockerconfig and auth field",
|
||||
image: "test.registry.io",
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockercfg,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Source: &credentialprovider.CredentialSource{
|
||||
Secret: credentialprovider.SecretCoordinates{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
},
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "with .dockerconfigjson and username/password fields",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
name: "with .dockerconfigjson and username/password fields",
|
||||
image: "test.registry.io",
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"username": "user", "password": "password"}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Source: &credentialprovider.CredentialSource{
|
||||
Secret: credentialprovider.SecretCoordinates{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
},
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "with .dockerconfig and username/password fields",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
name: "with .dockerconfig and username/password fields",
|
||||
image: "test.registry.io",
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockercfg,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigKey: []byte(`{"test.registry.io": {"username": "user", "password": "password"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Source: &credentialprovider.CredentialSource{
|
||||
Secret: credentialprovider.SecretCoordinates{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
},
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
|
|
@ -128,56 +166,65 @@ func Test_MakeDockerKeyring(t *testing.T) {
|
|||
{
|
||||
name: "with .dockerconfigjson but with wrong Secret Type",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
defaultKeyring: &FakeKeyring{},
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockercfg,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: nil,
|
||||
found: false,
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "with .dockerconfig but with wrong Secret Type",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{},
|
||||
defaultKeyring: &FakeKeyring{},
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: nil,
|
||||
found: false,
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
name: "with not matcing .dockerconfigjson and default keyring",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{
|
||||
auth: []credentialprovider.AuthConfig{
|
||||
defaultKeyring: &FakeKeyring{
|
||||
auth: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
AuthConfigHash: "",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
|
|
@ -185,26 +232,33 @@ func Test_MakeDockerKeyring(t *testing.T) {
|
|||
{
|
||||
name: "with not matching .dockerconfig and default keyring",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{
|
||||
auth: []credentialprovider.AuthConfig{
|
||||
defaultKeyring: &FakeKeyring{
|
||||
auth: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockercfg,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigKey: []byte(`{"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
AuthConfigHash: "",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
|
|
@ -212,19 +266,24 @@ func Test_MakeDockerKeyring(t *testing.T) {
|
|||
{
|
||||
name: "with no pull secrets but has default keyring",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{
|
||||
auth: []credentialprovider.AuthConfig{
|
||||
defaultKeyring: &FakeKeyring{
|
||||
auth: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pullSecrets: []v1.Secret{},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
AuthConfigHash: "",
|
||||
},
|
||||
},
|
||||
found: false,
|
||||
|
|
@ -232,30 +291,44 @@ func Test_MakeDockerKeyring(t *testing.T) {
|
|||
{
|
||||
name: "with pull secrets and has default keyring",
|
||||
image: "test.registry.io",
|
||||
defaultKeyring: &fakeKeyring{
|
||||
auth: []credentialprovider.AuthConfig{
|
||||
defaultKeyring: &FakeKeyring{
|
||||
auth: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pullSecrets: []v1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
authConfigs: []credentialprovider.AuthConfig{
|
||||
expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
|
||||
{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
Source: &credentialprovider.CredentialSource{
|
||||
Secret: credentialprovider.SecretCoordinates{
|
||||
Name: "s1", Namespace: "ns1", UID: "uid1"},
|
||||
},
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
|
||||
},
|
||||
{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
AuthConfig: credentialprovider.AuthConfig{
|
||||
Username: "default-user",
|
||||
Password: "default-password",
|
||||
},
|
||||
AuthConfigHash: "",
|
||||
},
|
||||
},
|
||||
found: true,
|
||||
|
|
@ -276,9 +349,9 @@ func Test_MakeDockerKeyring(t *testing.T) {
|
|||
t.Errorf("unexpected lookup for image: %s", testcase.image)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(authConfigs, testcase.authConfigs) {
|
||||
if !reflect.DeepEqual(authConfigs, testcase.expectedAuthConfigs) { // TODO: we may need better comparison as the result is unordered
|
||||
t.Logf("actual auth configs: %#v", authConfigs)
|
||||
t.Logf("expected auth configs: %#v", testcase.authConfigs)
|
||||
t.Logf("expected auth configs: %#v", testcase.expectedAuthConfigs)
|
||||
t.Error("auth configs did not match")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -352,6 +352,13 @@ const (
|
|||
// fallback to using it's cgroupDriver option.
|
||||
KubeletCgroupDriverFromCRI featuregate.Feature = "KubeletCgroupDriverFromCRI"
|
||||
|
||||
// owner: @stlaz
|
||||
// kep: https://kep.k8s.io/2535
|
||||
//
|
||||
// Enables tracking credentials for image pulls in order to authorize image
|
||||
// access for different tenants.
|
||||
KubeletEnsureSecretPulledImages featuregate.Feature = "KubeletEnsureSecretPulledImages"
|
||||
|
||||
// owner: @vinayakankugoyal
|
||||
// kep: http://kep.k8s.io/2862
|
||||
//
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
|||
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
|
||||
},
|
||||
|
||||
KubeletEnsureSecretPulledImages: {
|
||||
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
|
||||
},
|
||||
|
||||
KubeletFineGrainedAuthz: {
|
||||
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
|
||||
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},
|
||||
|
|
|
|||
210
pkg/generated/openapi/zz_generated.openapi.go
generated
210
pkg/generated/openapi/zz_generated.openapi.go
generated
|
|
@ -1260,6 +1260,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
"k8s.io/kubelet/config/v1alpha1.CredentialProvider": schema_k8sio_kubelet_config_v1alpha1_CredentialProvider(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.CredentialProviderConfig": schema_k8sio_kubelet_config_v1alpha1_CredentialProviderConfig(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ExecEnvVar": schema_k8sio_kubelet_config_v1alpha1_ExecEnvVar(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ImagePullCredentials": schema_k8sio_kubelet_config_v1alpha1_ImagePullCredentials(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ImagePullIntent": schema_k8sio_kubelet_config_v1alpha1_ImagePullIntent(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ImagePullSecret": schema_k8sio_kubelet_config_v1alpha1_ImagePullSecret(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ImagePulledRecord": schema_k8sio_kubelet_config_v1alpha1_ImagePulledRecord(ref),
|
||||
"k8s.io/kubelet/config/v1beta1.CrashLoopBackOffConfig": schema_k8sio_kubelet_config_v1beta1_CrashLoopBackOffConfig(ref),
|
||||
"k8s.io/kubelet/config/v1beta1.CredentialProvider": schema_k8sio_kubelet_config_v1beta1_CredentialProvider(ref),
|
||||
"k8s.io/kubelet/config/v1beta1.CredentialProviderConfig": schema_k8sio_kubelet_config_v1beta1_CredentialProviderConfig(ref),
|
||||
|
|
@ -64667,6 +64671,185 @@ func schema_k8sio_kubelet_config_v1alpha1_ExecEnvVar(ref common.ReferenceCallbac
|
|||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1alpha1_ImagePullCredentials(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ImagePullCredentials describe credentials that can be used to pull an image.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kubernetesSecrets": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "KuberneteSecretCoordinates is an index of coordinates of all the kubernetes secrets that were used to pull the image.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/kubelet/config/v1alpha1.ImagePullSecret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"nodePodsAccessible": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "NodePodsAccessible is a flag denoting the pull credentials are accessible by all the pods on the node, or that no credentials are needed for the pull.\n\nIf true, it is mutually exclusive with the `kubernetesSecrets` field.",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/kubelet/config/v1alpha1.ImagePullSecret"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1alpha1_ImagePullIntent(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ImagePullIntent is a record of the kubelet attempting to pull an image.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"image": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Image is the image spec from a Container's `image` field. The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe characters like ':' and '/'.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"image"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1alpha1_ImagePullSecret(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ImagePullSecret is a representation of a Kubernetes secret object coordinates along with a credential hash of the pull secret credentials this object contains.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"uid": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"namespace": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"credentialHash": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "CredentialHash is a SHA-256 retrieved by hashing the image pull credentials content of the secret specified by the UID/Namespace/Name coordinates.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"uid", "namespace", "name", "credentialHash"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1alpha1_ImagePulledRecord(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ImagePullRecord is a record of an image that was pulled by the kubelet.\n\nIf there are no records in the `kubernetesSecrets` field and both `nodeWideCredentials` and `anonymous` are `false`, credentials must be re-checked the next time an image represented by this record is being requested.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"lastUpdatedTime": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "LastUpdatedTime is the time of the last update to this record",
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
||||
},
|
||||
},
|
||||
"imageRef": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ImageRef is a reference to the image represented by this file as received from the CRI. The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe characters like ':' and '/'.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"credentialMapping": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "CredentialMapping maps `image` to the set of credentials that it was previously pulled with. `image` in this case is the content of a pod's container `image` field that's got its tag/digest removed.\n\nExample:\n Container requests the `hello-world:latest@sha256:91fb4b041da273d5a3273b6d587d62d518300a6ad268b28628f74997b93171b2` image:\n \"credentialMapping\": {\n \"hello-world\": { \"nodePodsAccessible\": true }\n }",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/kubelet/config/v1alpha1.ImagePullCredentials"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"lastUpdatedTime", "imageRef"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Time", "k8s.io/kubelet/config/v1alpha1.ImagePullCredentials"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1beta1_CrashLoopBackOffConfig(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
@ -65112,6 +65295,33 @@ func schema_k8sio_kubelet_config_v1beta1_KubeletConfiguration(ref common.Referen
|
|||
Format: "int32",
|
||||
},
|
||||
},
|
||||
"imagePullCredentialsVerificationPolicy": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "imagePullCredentialsVerificationPolicy determines how credentials should be verified when pod requests an image that is already present on the node:\n - NeverVerify\n - anyone on a node can use any image present on the node\n - NeverVerifyPreloadedImages\n - images that were pulled to the node by something else than the kubelet\n can be used without reverifying pull credentials\n - NeverVerifyAllowlistedImages\n - like \"NeverVerifyPreloadedImages\" but only node images from\n `preloadedImagesVerificationAllowlist` don't require reverification\n - AlwaysVerify\n - all images require credential reverification",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"preloadedImagesVerificationAllowlist": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "preloadedImagesVerificationAllowlist specifies a list of images that are exempted from credential reverification for the \"NeverVerifyAllowlistedImages\" `imagePullCredentialsVerificationPolicy`. The list accepts a full path segment wildcard suffix \"/*\". Only use image specs without an image tag or digest.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"eventRecordQPS": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "eventRecordQPS is the maximum event creations per second. If 0, there is no limit enforced. The value cannot be a negative number. Default: 50",
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ var (
|
|||
"ImageGCLowThresholdPercent",
|
||||
"ImageMinimumGCAge.Duration",
|
||||
"ImageMaximumGCAge.Duration",
|
||||
"ImagePullCredentialsVerificationPolicy",
|
||||
"KernelMemcgNotification",
|
||||
"KubeAPIBurst",
|
||||
"KubeAPIQPS",
|
||||
|
|
@ -268,6 +269,7 @@ var (
|
|||
"PodPidsLimit",
|
||||
"PodsPerCore",
|
||||
"Port",
|
||||
"PreloadedImagesVerificationAllowlist[*]",
|
||||
"ProtectKernelDefaults",
|
||||
"ProviderID",
|
||||
"ReadOnlyPort",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||
&KubeletConfiguration{},
|
||||
&SerializedNodeConfigSource{},
|
||||
&CredentialProviderConfig{},
|
||||
&ImagePullIntent{},
|
||||
&ImagePulledRecord{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func TestComponentConfigSetup(t *testing.T) {
|
|||
reflect.TypeOf(logsapi.LoggingConfiguration{}): true,
|
||||
reflect.TypeOf(tracingapi.TracingConfiguration{}): true,
|
||||
reflect.TypeOf(metav1.Duration{}): true,
|
||||
reflect.TypeOf(metav1.Time{}): true,
|
||||
reflect.TypeOf(metav1.TypeMeta{}): true,
|
||||
reflect.TypeOf(v1.NodeConfigSource{}): true,
|
||||
reflect.TypeOf(v1.Taint{}): true,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,25 @@ type KubeletConfiguration struct {
|
|||
// pulls to burst to this number, while still not exceeding registryPullQPS.
|
||||
// Only used if registryPullQPS > 0.
|
||||
RegistryBurst int32
|
||||
// imagePullCredentialsVerificationPolicy determines how credentials should be
|
||||
// verified when pod requests an image that is already present on the node:
|
||||
// - NeverVerify
|
||||
// - anyone on a node can use any image present on the node
|
||||
// - NeverVerifyPreloadedImages
|
||||
// - images that were pulled to the node by something else than the kubelet
|
||||
// can be used without reverifying pull credentials
|
||||
// - NeverVerifyAllowlistedImages
|
||||
// - like "NeverVerifyPreloadedImages" but only node images from
|
||||
// `preloadedImagesVerificationAllowlist` don't require reverification
|
||||
// - AlwaysVerify
|
||||
// - all images require credential reverification
|
||||
ImagePullCredentialsVerificationPolicy string
|
||||
// preloadedImagesVerificationAllowlist specifies a list of images that are
|
||||
// exempted from credential reverification for the "NeverVerifyAllowlistedImages"
|
||||
// `imagePullCredentialsVerificationPolicy`.
|
||||
// The list accepts a full path segment wildcard suffix "/*".
|
||||
// Only use image specs without an image tag or digest.
|
||||
PreloadedImagesVerificationAllowlist []string
|
||||
// eventRecordQPS is the maximum event creations per second. If 0, there
|
||||
// is no limit enforced.
|
||||
EventRecordQPS int32
|
||||
|
|
@ -769,3 +788,93 @@ type CrashLoopBackOffConfig struct {
|
|||
// +optional
|
||||
MaxContainerRestartPeriod *metav1.Duration
|
||||
}
|
||||
|
||||
// ImagePullCredentialsVerificationPolicy is an enum for the policy that is enforced
|
||||
// when pod is requesting an image that appears on the system
|
||||
type ImagePullCredentialsVerificationPolicy string
|
||||
|
||||
const (
|
||||
// NeverVerify will never require credential verification for images that
|
||||
// already exist on the node
|
||||
NeverVerify ImagePullCredentialsVerificationPolicy = "NeverVerify"
|
||||
// NeverVerifyPreloadedImages does not require credential verification for images
|
||||
// pulled outside the kubelet process
|
||||
NeverVerifyPreloadedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyPreloadedImages"
|
||||
// NeverVerifyAllowlistedImages does not require credential verification for
|
||||
// a list of images that were pulled outside the kubelet process
|
||||
NeverVerifyAllowlistedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyAllowlistedImages"
|
||||
// AlwaysVerify requires credential verification for accessing any image on the
|
||||
// node irregardless how it was pulled
|
||||
AlwaysVerify ImagePullCredentialsVerificationPolicy = "AlwaysVerify"
|
||||
)
|
||||
|
||||
// ImagePullIntent is a record of the kubelet attempting to pull an image.
|
||||
//
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ImagePullIntent struct {
|
||||
metav1.TypeMeta
|
||||
|
||||
// Image is the image spec from a Container's `image` field.
|
||||
// The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe
|
||||
// characters like ':' and '/'.
|
||||
Image string
|
||||
}
|
||||
|
||||
// ImagePullRecord is a record of an image that was pulled by the kubelet.
|
||||
//
|
||||
// If there are no records in the `kubernetesSecrets` field and both `nodeWideCredentials`
|
||||
// and `anonymous` are `false`, credentials must be re-checked the next time an
|
||||
// image represented by this record is being requested.
|
||||
//
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ImagePulledRecord struct {
|
||||
metav1.TypeMeta
|
||||
|
||||
// LastUpdatedTime is the time of the last update to this record
|
||||
LastUpdatedTime metav1.Time
|
||||
|
||||
// ImageRef is a reference to the image represented by this file as received
|
||||
// from the CRI.
|
||||
// The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe
|
||||
// characters like ':' and '/'.
|
||||
ImageRef string
|
||||
|
||||
// CredentialMapping maps `image` to the set of credentials that it was
|
||||
// previously pulled with.
|
||||
// `image` in this case is the content of a pod's container `image` field that's
|
||||
// got its tag/digest removed.
|
||||
//
|
||||
// Example:
|
||||
// Container requests the `hello-world:latest@sha256:91fb4b041da273d5a3273b6d587d62d518300a6ad268b28628f74997b93171b2` image:
|
||||
// "credentialMapping": {
|
||||
// "hello-world": { "nodePodsAccessible": true }
|
||||
// }
|
||||
CredentialMapping map[string]ImagePullCredentials
|
||||
}
|
||||
|
||||
// ImagePullCredentials describe credentials that can be used to pull an image.
|
||||
type ImagePullCredentials struct {
|
||||
// KuberneteSecretCoordinates is an index of coordinates of all the kubernetes
|
||||
// secrets that were used to pull the image.
|
||||
// +optional
|
||||
KubernetesSecrets []ImagePullSecret
|
||||
|
||||
// NodePodsAccessible is a flag denoting the pull credentials are accessible
|
||||
// by all the pods on the node, or that no credentials are needed for the pull.
|
||||
//
|
||||
// If true, it is mutually exclusive with the `kubernetesSecrets` field.
|
||||
// +optional
|
||||
NodePodsAccessible bool
|
||||
}
|
||||
|
||||
// ImagePullSecret is a representation of a Kubernetes secret object coordinates along
|
||||
// with a credential hash of the pull secret credentials this object contains.
|
||||
type ImagePullSecret struct {
|
||||
UID string
|
||||
Namespace string
|
||||
Name string
|
||||
|
||||
// CredentialHash is a SHA-256 retrieved by hashing the image pull credentials
|
||||
// content of the secret specified by the UID/Namespace/Name coordinates.
|
||||
CredentialHash string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,46 @@ func RegisterConversions(s *runtime.Scheme) error {
|
|||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.ImagePullCredentials)(nil), (*config.ImagePullCredentials)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ImagePullCredentials_To_config_ImagePullCredentials(a.(*configv1alpha1.ImagePullCredentials), b.(*config.ImagePullCredentials), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.ImagePullCredentials)(nil), (*configv1alpha1.ImagePullCredentials)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_ImagePullCredentials_To_v1alpha1_ImagePullCredentials(a.(*config.ImagePullCredentials), b.(*configv1alpha1.ImagePullCredentials), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.ImagePullIntent)(nil), (*config.ImagePullIntent)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ImagePullIntent_To_config_ImagePullIntent(a.(*configv1alpha1.ImagePullIntent), b.(*config.ImagePullIntent), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.ImagePullIntent)(nil), (*configv1alpha1.ImagePullIntent)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_ImagePullIntent_To_v1alpha1_ImagePullIntent(a.(*config.ImagePullIntent), b.(*configv1alpha1.ImagePullIntent), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.ImagePullSecret)(nil), (*config.ImagePullSecret)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ImagePullSecret_To_config_ImagePullSecret(a.(*configv1alpha1.ImagePullSecret), b.(*config.ImagePullSecret), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.ImagePullSecret)(nil), (*configv1alpha1.ImagePullSecret)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_ImagePullSecret_To_v1alpha1_ImagePullSecret(a.(*config.ImagePullSecret), b.(*configv1alpha1.ImagePullSecret), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.ImagePulledRecord)(nil), (*config.ImagePulledRecord)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_ImagePulledRecord_To_config_ImagePulledRecord(a.(*configv1alpha1.ImagePulledRecord), b.(*config.ImagePulledRecord), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.ImagePulledRecord)(nil), (*configv1alpha1.ImagePulledRecord)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_ImagePulledRecord_To_v1alpha1_ImagePulledRecord(a.(*config.ImagePulledRecord), b.(*configv1alpha1.ImagePulledRecord), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.CredentialProvider)(nil), (*configv1alpha1.CredentialProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(a.(*config.CredentialProvider), b.(*configv1alpha1.CredentialProvider), scope)
|
||||
}); err != nil {
|
||||
|
|
@ -158,3 +198,95 @@ func autoConvert_config_ExecEnvVar_To_v1alpha1_ExecEnvVar(in *config.ExecEnvVar,
|
|||
func Convert_config_ExecEnvVar_To_v1alpha1_ExecEnvVar(in *config.ExecEnvVar, out *configv1alpha1.ExecEnvVar, s conversion.Scope) error {
|
||||
return autoConvert_config_ExecEnvVar_To_v1alpha1_ExecEnvVar(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ImagePullCredentials_To_config_ImagePullCredentials(in *configv1alpha1.ImagePullCredentials, out *config.ImagePullCredentials, s conversion.Scope) error {
|
||||
out.KubernetesSecrets = *(*[]config.ImagePullSecret)(unsafe.Pointer(&in.KubernetesSecrets))
|
||||
out.NodePodsAccessible = in.NodePodsAccessible
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ImagePullCredentials_To_config_ImagePullCredentials is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ImagePullCredentials_To_config_ImagePullCredentials(in *configv1alpha1.ImagePullCredentials, out *config.ImagePullCredentials, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ImagePullCredentials_To_config_ImagePullCredentials(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_config_ImagePullCredentials_To_v1alpha1_ImagePullCredentials(in *config.ImagePullCredentials, out *configv1alpha1.ImagePullCredentials, s conversion.Scope) error {
|
||||
out.KubernetesSecrets = *(*[]configv1alpha1.ImagePullSecret)(unsafe.Pointer(&in.KubernetesSecrets))
|
||||
out.NodePodsAccessible = in.NodePodsAccessible
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_ImagePullCredentials_To_v1alpha1_ImagePullCredentials is an autogenerated conversion function.
|
||||
func Convert_config_ImagePullCredentials_To_v1alpha1_ImagePullCredentials(in *config.ImagePullCredentials, out *configv1alpha1.ImagePullCredentials, s conversion.Scope) error {
|
||||
return autoConvert_config_ImagePullCredentials_To_v1alpha1_ImagePullCredentials(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ImagePullIntent_To_config_ImagePullIntent(in *configv1alpha1.ImagePullIntent, out *config.ImagePullIntent, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ImagePullIntent_To_config_ImagePullIntent is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ImagePullIntent_To_config_ImagePullIntent(in *configv1alpha1.ImagePullIntent, out *config.ImagePullIntent, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ImagePullIntent_To_config_ImagePullIntent(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_config_ImagePullIntent_To_v1alpha1_ImagePullIntent(in *config.ImagePullIntent, out *configv1alpha1.ImagePullIntent, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_ImagePullIntent_To_v1alpha1_ImagePullIntent is an autogenerated conversion function.
|
||||
func Convert_config_ImagePullIntent_To_v1alpha1_ImagePullIntent(in *config.ImagePullIntent, out *configv1alpha1.ImagePullIntent, s conversion.Scope) error {
|
||||
return autoConvert_config_ImagePullIntent_To_v1alpha1_ImagePullIntent(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ImagePullSecret_To_config_ImagePullSecret(in *configv1alpha1.ImagePullSecret, out *config.ImagePullSecret, s conversion.Scope) error {
|
||||
out.UID = in.UID
|
||||
out.Namespace = in.Namespace
|
||||
out.Name = in.Name
|
||||
out.CredentialHash = in.CredentialHash
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ImagePullSecret_To_config_ImagePullSecret is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ImagePullSecret_To_config_ImagePullSecret(in *configv1alpha1.ImagePullSecret, out *config.ImagePullSecret, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ImagePullSecret_To_config_ImagePullSecret(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_config_ImagePullSecret_To_v1alpha1_ImagePullSecret(in *config.ImagePullSecret, out *configv1alpha1.ImagePullSecret, s conversion.Scope) error {
|
||||
out.UID = in.UID
|
||||
out.Namespace = in.Namespace
|
||||
out.Name = in.Name
|
||||
out.CredentialHash = in.CredentialHash
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_ImagePullSecret_To_v1alpha1_ImagePullSecret is an autogenerated conversion function.
|
||||
func Convert_config_ImagePullSecret_To_v1alpha1_ImagePullSecret(in *config.ImagePullSecret, out *configv1alpha1.ImagePullSecret, s conversion.Scope) error {
|
||||
return autoConvert_config_ImagePullSecret_To_v1alpha1_ImagePullSecret(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_ImagePulledRecord_To_config_ImagePulledRecord(in *configv1alpha1.ImagePulledRecord, out *config.ImagePulledRecord, s conversion.Scope) error {
|
||||
out.LastUpdatedTime = in.LastUpdatedTime
|
||||
out.ImageRef = in.ImageRef
|
||||
out.CredentialMapping = *(*map[string]config.ImagePullCredentials)(unsafe.Pointer(&in.CredentialMapping))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_ImagePulledRecord_To_config_ImagePulledRecord is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_ImagePulledRecord_To_config_ImagePulledRecord(in *configv1alpha1.ImagePulledRecord, out *config.ImagePulledRecord, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_ImagePulledRecord_To_config_ImagePulledRecord(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_config_ImagePulledRecord_To_v1alpha1_ImagePulledRecord(in *config.ImagePulledRecord, out *configv1alpha1.ImagePulledRecord, s conversion.Scope) error {
|
||||
out.LastUpdatedTime = in.LastUpdatedTime
|
||||
out.ImageRef = in.ImageRef
|
||||
out.CredentialMapping = *(*map[string]configv1alpha1.ImagePullCredentials)(unsafe.Pointer(&in.CredentialMapping))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_ImagePulledRecord_To_v1alpha1_ImagePulledRecord is an autogenerated conversion function.
|
||||
func Convert_config_ImagePulledRecord_To_v1alpha1_ImagePulledRecord(in *config.ImagePulledRecord, out *configv1alpha1.ImagePulledRecord, s conversion.Scope) error {
|
||||
return autoConvert_config_ImagePulledRecord_To_v1alpha1_ImagePulledRecord(in, out, s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,4 +313,10 @@ func SetDefaults_KubeletConfiguration(obj *kubeletconfigv1beta1.KubeletConfigura
|
|||
obj.CrashLoopBackOff.MaxContainerRestartPeriod = &metav1.Duration{Duration: MaxContainerBackOff}
|
||||
}
|
||||
}
|
||||
|
||||
if localFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
if obj.ImagePullCredentialsVerificationPolicy == "" {
|
||||
obj.ImagePullCredentialsVerificationPolicy = kubeletconfigv1beta1.NeverVerifyPreloadedImages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) {
|
|||
NodeLeaseDurationSeconds: 40,
|
||||
ImageMinimumGCAge: metav1.Duration{Duration: 2 * time.Minute},
|
||||
ImageMaximumGCAge: metav1.Duration{},
|
||||
ImagePullCredentialsVerificationPolicy: "",
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](85),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](80),
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
|
|
@ -168,74 +169,75 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) {
|
|||
CacheUnauthorizedTTL: zeroDuration,
|
||||
},
|
||||
},
|
||||
RegistryPullQPS: ptr.To[int32](0),
|
||||
RegistryBurst: 0,
|
||||
EventRecordQPS: ptr.To[int32](0),
|
||||
EventBurst: 0,
|
||||
EnableDebuggingHandlers: ptr.To(false),
|
||||
EnableContentionProfiling: false,
|
||||
HealthzPort: ptr.To[int32](0),
|
||||
HealthzBindAddress: "",
|
||||
OOMScoreAdj: ptr.To[int32](0),
|
||||
ClusterDomain: "",
|
||||
ClusterDNS: []string{},
|
||||
StreamingConnectionIdleTimeout: zeroDuration,
|
||||
NodeStatusUpdateFrequency: zeroDuration,
|
||||
NodeStatusReportFrequency: zeroDuration,
|
||||
NodeLeaseDurationSeconds: 0,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: zeroDuration,
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](0),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](0),
|
||||
VolumeStatsAggPeriod: zeroDuration,
|
||||
KubeletCgroups: "",
|
||||
SystemCgroups: "",
|
||||
CgroupRoot: "",
|
||||
CgroupsPerQOS: ptr.To(false),
|
||||
CgroupDriver: "",
|
||||
CPUManagerPolicy: "",
|
||||
CPUManagerPolicyOptions: map[string]string{},
|
||||
CPUManagerReconcilePeriod: zeroDuration,
|
||||
MemoryManagerPolicy: "",
|
||||
TopologyManagerPolicy: "",
|
||||
TopologyManagerScope: "",
|
||||
QOSReserved: map[string]string{},
|
||||
RuntimeRequestTimeout: zeroDuration,
|
||||
HairpinMode: "",
|
||||
MaxPods: 0,
|
||||
PodCIDR: "",
|
||||
PodPidsLimit: ptr.To[int64](0),
|
||||
ResolverConfig: ptr.To(""),
|
||||
RunOnce: false,
|
||||
CPUCFSQuota: ptr.To(false),
|
||||
CPUCFSQuotaPeriod: &zeroDuration,
|
||||
NodeStatusMaxImages: ptr.To[int32](0),
|
||||
MaxOpenFiles: 0,
|
||||
ContentType: "",
|
||||
KubeAPIQPS: ptr.To[int32](0),
|
||||
KubeAPIBurst: 0,
|
||||
SerializeImagePulls: ptr.To(false),
|
||||
MaxParallelImagePulls: nil,
|
||||
EvictionHard: map[string]string{},
|
||||
EvictionSoft: map[string]string{},
|
||||
EvictionSoftGracePeriod: map[string]string{},
|
||||
EvictionPressureTransitionPeriod: zeroDuration,
|
||||
EvictionMaxPodGracePeriod: 0,
|
||||
EvictionMinimumReclaim: map[string]string{},
|
||||
MergeDefaultEvictionSettings: ptr.To(false),
|
||||
PodsPerCore: 0,
|
||||
EnableControllerAttachDetach: ptr.To(false),
|
||||
ProtectKernelDefaults: false,
|
||||
MakeIPTablesUtilChains: ptr.To(false),
|
||||
IPTablesMasqueradeBit: ptr.To[int32](0),
|
||||
IPTablesDropBit: ptr.To[int32](0),
|
||||
FeatureGates: map[string]bool{},
|
||||
FailSwapOn: ptr.To(false),
|
||||
MemorySwap: v1beta1.MemorySwapConfiguration{SwapBehavior: ""},
|
||||
ContainerLogMaxSize: "",
|
||||
ContainerLogMaxFiles: ptr.To[int32](0),
|
||||
ContainerLogMaxWorkers: ptr.To[int32](1),
|
||||
ContainerLogMonitorInterval: &metav1.Duration{Duration: 10 * time.Second},
|
||||
RegistryPullQPS: ptr.To[int32](0),
|
||||
RegistryBurst: 0,
|
||||
EventRecordQPS: ptr.To[int32](0),
|
||||
EventBurst: 0,
|
||||
EnableDebuggingHandlers: ptr.To(false),
|
||||
EnableContentionProfiling: false,
|
||||
HealthzPort: ptr.To[int32](0),
|
||||
HealthzBindAddress: "",
|
||||
OOMScoreAdj: ptr.To[int32](0),
|
||||
ClusterDomain: "",
|
||||
ClusterDNS: []string{},
|
||||
StreamingConnectionIdleTimeout: zeroDuration,
|
||||
NodeStatusUpdateFrequency: zeroDuration,
|
||||
NodeStatusReportFrequency: zeroDuration,
|
||||
NodeLeaseDurationSeconds: 0,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: zeroDuration,
|
||||
ImagePullCredentialsVerificationPolicy: "",
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](0),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](0),
|
||||
VolumeStatsAggPeriod: zeroDuration,
|
||||
KubeletCgroups: "",
|
||||
SystemCgroups: "",
|
||||
CgroupRoot: "",
|
||||
CgroupsPerQOS: ptr.To(false),
|
||||
CgroupDriver: "",
|
||||
CPUManagerPolicy: "",
|
||||
CPUManagerPolicyOptions: map[string]string{},
|
||||
CPUManagerReconcilePeriod: zeroDuration,
|
||||
MemoryManagerPolicy: "",
|
||||
TopologyManagerPolicy: "",
|
||||
TopologyManagerScope: "",
|
||||
QOSReserved: map[string]string{},
|
||||
RuntimeRequestTimeout: zeroDuration,
|
||||
HairpinMode: "",
|
||||
MaxPods: 0,
|
||||
PodCIDR: "",
|
||||
PodPidsLimit: ptr.To[int64](0),
|
||||
ResolverConfig: ptr.To(""),
|
||||
RunOnce: false,
|
||||
CPUCFSQuota: ptr.To(false),
|
||||
CPUCFSQuotaPeriod: &zeroDuration,
|
||||
NodeStatusMaxImages: ptr.To[int32](0),
|
||||
MaxOpenFiles: 0,
|
||||
ContentType: "",
|
||||
KubeAPIQPS: ptr.To[int32](0),
|
||||
KubeAPIBurst: 0,
|
||||
SerializeImagePulls: ptr.To(false),
|
||||
MaxParallelImagePulls: nil,
|
||||
EvictionHard: map[string]string{},
|
||||
EvictionSoft: map[string]string{},
|
||||
EvictionSoftGracePeriod: map[string]string{},
|
||||
EvictionPressureTransitionPeriod: zeroDuration,
|
||||
EvictionMaxPodGracePeriod: 0,
|
||||
EvictionMinimumReclaim: map[string]string{},
|
||||
MergeDefaultEvictionSettings: ptr.To(false),
|
||||
PodsPerCore: 0,
|
||||
EnableControllerAttachDetach: ptr.To(false),
|
||||
ProtectKernelDefaults: false,
|
||||
MakeIPTablesUtilChains: ptr.To(false),
|
||||
IPTablesMasqueradeBit: ptr.To[int32](0),
|
||||
IPTablesDropBit: ptr.To[int32](0),
|
||||
FeatureGates: map[string]bool{},
|
||||
FailSwapOn: ptr.To(false),
|
||||
MemorySwap: v1beta1.MemorySwapConfiguration{SwapBehavior: ""},
|
||||
ContainerLogMaxSize: "",
|
||||
ContainerLogMaxFiles: ptr.To[int32](0),
|
||||
ContainerLogMaxWorkers: ptr.To[int32](1),
|
||||
ContainerLogMonitorInterval: &metav1.Duration{Duration: 10 * time.Second},
|
||||
ConfigMapAndSecretChangeDetectionStrategy: v1beta1.WatchChangeDetectionStrategy,
|
||||
SystemReserved: map[string]string{},
|
||||
KubeReserved: map[string]string{},
|
||||
|
|
@ -307,6 +309,7 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) {
|
|||
ImageMinimumGCAge: metav1.Duration{Duration: 2 * time.Minute},
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](0),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](0),
|
||||
ImagePullCredentialsVerificationPolicy: "",
|
||||
VolumeStatsAggPeriod: metav1.Duration{Duration: time.Minute},
|
||||
CgroupsPerQOS: ptr.To(false),
|
||||
CgroupDriver: "cgroupfs",
|
||||
|
|
@ -407,54 +410,55 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) {
|
|||
CacheUnauthorizedTTL: metav1.Duration{Duration: 60 * time.Second},
|
||||
},
|
||||
},
|
||||
RegistryPullQPS: ptr.To[int32](1),
|
||||
RegistryBurst: 1,
|
||||
EventRecordQPS: ptr.To[int32](1),
|
||||
EventBurst: 1,
|
||||
EnableDebuggingHandlers: ptr.To(true),
|
||||
EnableContentionProfiling: true,
|
||||
HealthzPort: ptr.To[int32](1),
|
||||
HealthzBindAddress: "127.0.0.2",
|
||||
OOMScoreAdj: ptr.To[int32](1),
|
||||
ClusterDomain: "cluster-domain",
|
||||
ClusterDNS: []string{"192.168.1.3"},
|
||||
StreamingConnectionIdleTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusUpdateFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusReportFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeLeaseDurationSeconds: 1,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: metav1.Duration{Duration: 60 * time.Second},
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](1),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](1),
|
||||
VolumeStatsAggPeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
KubeletCgroups: "kubelet-cgroup",
|
||||
SystemCgroups: "system-cgroup",
|
||||
CgroupRoot: "root-cgroup",
|
||||
CgroupsPerQOS: ptr.To(true),
|
||||
CgroupDriver: "systemd",
|
||||
CPUManagerPolicy: "cpu-manager-policy",
|
||||
CPUManagerPolicyOptions: map[string]string{"key": "value"},
|
||||
CPUManagerReconcilePeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
MemoryManagerPolicy: v1beta1.StaticMemoryManagerPolicy,
|
||||
TopologyManagerPolicy: v1beta1.RestrictedTopologyManagerPolicy,
|
||||
TopologyManagerScope: v1beta1.PodTopologyManagerScope,
|
||||
QOSReserved: map[string]string{"memory": "10%"},
|
||||
RuntimeRequestTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
HairpinMode: v1beta1.HairpinVeth,
|
||||
MaxPods: 1,
|
||||
PodCIDR: "192.168.1.0/24",
|
||||
PodPidsLimit: ptr.To[int64](1),
|
||||
ResolverConfig: ptr.To("resolver-config"),
|
||||
RunOnce: true,
|
||||
CPUCFSQuota: ptr.To(true),
|
||||
CPUCFSQuotaPeriod: &metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusMaxImages: ptr.To[int32](1),
|
||||
MaxOpenFiles: 1,
|
||||
ContentType: "application/protobuf",
|
||||
KubeAPIQPS: ptr.To[int32](1),
|
||||
KubeAPIBurst: 1,
|
||||
SerializeImagePulls: ptr.To(true),
|
||||
MaxParallelImagePulls: ptr.To[int32](5),
|
||||
RegistryPullQPS: ptr.To[int32](1),
|
||||
RegistryBurst: 1,
|
||||
EventRecordQPS: ptr.To[int32](1),
|
||||
EventBurst: 1,
|
||||
EnableDebuggingHandlers: ptr.To(true),
|
||||
EnableContentionProfiling: true,
|
||||
HealthzPort: ptr.To[int32](1),
|
||||
HealthzBindAddress: "127.0.0.2",
|
||||
OOMScoreAdj: ptr.To[int32](1),
|
||||
ClusterDomain: "cluster-domain",
|
||||
ClusterDNS: []string{"192.168.1.3"},
|
||||
StreamingConnectionIdleTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusUpdateFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusReportFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeLeaseDurationSeconds: 1,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: metav1.Duration{Duration: 60 * time.Second},
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](1),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](1),
|
||||
PreloadedImagesVerificationAllowlist: []string{"test.test/repo/image"},
|
||||
VolumeStatsAggPeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
KubeletCgroups: "kubelet-cgroup",
|
||||
SystemCgroups: "system-cgroup",
|
||||
CgroupRoot: "root-cgroup",
|
||||
CgroupsPerQOS: ptr.To(true),
|
||||
CgroupDriver: "systemd",
|
||||
CPUManagerPolicy: "cpu-manager-policy",
|
||||
CPUManagerPolicyOptions: map[string]string{"key": "value"},
|
||||
CPUManagerReconcilePeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
MemoryManagerPolicy: v1beta1.StaticMemoryManagerPolicy,
|
||||
TopologyManagerPolicy: v1beta1.RestrictedTopologyManagerPolicy,
|
||||
TopologyManagerScope: v1beta1.PodTopologyManagerScope,
|
||||
QOSReserved: map[string]string{"memory": "10%"},
|
||||
RuntimeRequestTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
HairpinMode: v1beta1.HairpinVeth,
|
||||
MaxPods: 1,
|
||||
PodCIDR: "192.168.1.0/24",
|
||||
PodPidsLimit: ptr.To[int64](1),
|
||||
ResolverConfig: ptr.To("resolver-config"),
|
||||
RunOnce: true,
|
||||
CPUCFSQuota: ptr.To(true),
|
||||
CPUCFSQuotaPeriod: &metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusMaxImages: ptr.To[int32](1),
|
||||
MaxOpenFiles: 1,
|
||||
ContentType: "application/protobuf",
|
||||
KubeAPIQPS: ptr.To[int32](1),
|
||||
KubeAPIBurst: 1,
|
||||
SerializeImagePulls: ptr.To(true),
|
||||
MaxParallelImagePulls: ptr.To[int32](5),
|
||||
EvictionHard: map[string]string{
|
||||
"memory.available": "1Mi",
|
||||
"nodefs.available": "1%",
|
||||
|
|
@ -563,54 +567,55 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) {
|
|||
CacheUnauthorizedTTL: metav1.Duration{Duration: 60 * time.Second},
|
||||
},
|
||||
},
|
||||
RegistryPullQPS: ptr.To[int32](1),
|
||||
RegistryBurst: 1,
|
||||
EventRecordQPS: ptr.To[int32](1),
|
||||
EventBurst: 1,
|
||||
EnableDebuggingHandlers: ptr.To(true),
|
||||
EnableContentionProfiling: true,
|
||||
HealthzPort: ptr.To[int32](1),
|
||||
HealthzBindAddress: "127.0.0.2",
|
||||
OOMScoreAdj: ptr.To[int32](1),
|
||||
ClusterDomain: "cluster-domain",
|
||||
ClusterDNS: []string{"192.168.1.3"},
|
||||
StreamingConnectionIdleTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusUpdateFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusReportFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeLeaseDurationSeconds: 1,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: metav1.Duration{Duration: 60 * time.Second},
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](1),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](1),
|
||||
VolumeStatsAggPeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
KubeletCgroups: "kubelet-cgroup",
|
||||
SystemCgroups: "system-cgroup",
|
||||
CgroupRoot: "root-cgroup",
|
||||
CgroupsPerQOS: ptr.To(true),
|
||||
CgroupDriver: "systemd",
|
||||
CPUManagerPolicy: "cpu-manager-policy",
|
||||
CPUManagerPolicyOptions: map[string]string{"key": "value"},
|
||||
CPUManagerReconcilePeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
MemoryManagerPolicy: v1beta1.StaticMemoryManagerPolicy,
|
||||
TopologyManagerPolicy: v1beta1.RestrictedTopologyManagerPolicy,
|
||||
TopologyManagerScope: v1beta1.PodTopologyManagerScope,
|
||||
QOSReserved: map[string]string{"memory": "10%"},
|
||||
RuntimeRequestTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
HairpinMode: v1beta1.HairpinVeth,
|
||||
MaxPods: 1,
|
||||
PodCIDR: "192.168.1.0/24",
|
||||
PodPidsLimit: ptr.To[int64](1),
|
||||
ResolverConfig: ptr.To("resolver-config"),
|
||||
RunOnce: true,
|
||||
CPUCFSQuota: ptr.To(true),
|
||||
CPUCFSQuotaPeriod: &metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusMaxImages: ptr.To[int32](1),
|
||||
MaxOpenFiles: 1,
|
||||
ContentType: "application/protobuf",
|
||||
KubeAPIQPS: ptr.To[int32](1),
|
||||
KubeAPIBurst: 1,
|
||||
SerializeImagePulls: ptr.To(true),
|
||||
MaxParallelImagePulls: ptr.To[int32](5),
|
||||
RegistryPullQPS: ptr.To[int32](1),
|
||||
RegistryBurst: 1,
|
||||
EventRecordQPS: ptr.To[int32](1),
|
||||
EventBurst: 1,
|
||||
EnableDebuggingHandlers: ptr.To(true),
|
||||
EnableContentionProfiling: true,
|
||||
HealthzPort: ptr.To[int32](1),
|
||||
HealthzBindAddress: "127.0.0.2",
|
||||
OOMScoreAdj: ptr.To[int32](1),
|
||||
ClusterDomain: "cluster-domain",
|
||||
ClusterDNS: []string{"192.168.1.3"},
|
||||
StreamingConnectionIdleTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusUpdateFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusReportFrequency: metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeLeaseDurationSeconds: 1,
|
||||
ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock",
|
||||
ImageMinimumGCAge: metav1.Duration{Duration: 60 * time.Second},
|
||||
ImageGCHighThresholdPercent: ptr.To[int32](1),
|
||||
ImageGCLowThresholdPercent: ptr.To[int32](1),
|
||||
PreloadedImagesVerificationAllowlist: []string{"test.test/repo/image"},
|
||||
VolumeStatsAggPeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
KubeletCgroups: "kubelet-cgroup",
|
||||
SystemCgroups: "system-cgroup",
|
||||
CgroupRoot: "root-cgroup",
|
||||
CgroupsPerQOS: ptr.To(true),
|
||||
CgroupDriver: "systemd",
|
||||
CPUManagerPolicy: "cpu-manager-policy",
|
||||
CPUManagerPolicyOptions: map[string]string{"key": "value"},
|
||||
CPUManagerReconcilePeriod: metav1.Duration{Duration: 60 * time.Second},
|
||||
MemoryManagerPolicy: v1beta1.StaticMemoryManagerPolicy,
|
||||
TopologyManagerPolicy: v1beta1.RestrictedTopologyManagerPolicy,
|
||||
TopologyManagerScope: v1beta1.PodTopologyManagerScope,
|
||||
QOSReserved: map[string]string{"memory": "10%"},
|
||||
RuntimeRequestTimeout: metav1.Duration{Duration: 60 * time.Second},
|
||||
HairpinMode: v1beta1.HairpinVeth,
|
||||
MaxPods: 1,
|
||||
PodCIDR: "192.168.1.0/24",
|
||||
PodPidsLimit: ptr.To[int64](1),
|
||||
ResolverConfig: ptr.To("resolver-config"),
|
||||
RunOnce: true,
|
||||
CPUCFSQuota: ptr.To(true),
|
||||
CPUCFSQuotaPeriod: &metav1.Duration{Duration: 60 * time.Second},
|
||||
NodeStatusMaxImages: ptr.To[int32](1),
|
||||
MaxOpenFiles: 1,
|
||||
ContentType: "application/protobuf",
|
||||
KubeAPIQPS: ptr.To[int32](1),
|
||||
KubeAPIBurst: 1,
|
||||
SerializeImagePulls: ptr.To(true),
|
||||
MaxParallelImagePulls: ptr.To[int32](5),
|
||||
EvictionHard: map[string]string{
|
||||
"memory.available": "1Mi",
|
||||
"nodefs.available": "1%",
|
||||
|
|
|
|||
|
|
@ -417,6 +417,8 @@ func autoConvert_v1beta1_KubeletConfiguration_To_config_KubeletConfiguration(in
|
|||
return err
|
||||
}
|
||||
out.RegistryBurst = in.RegistryBurst
|
||||
out.ImagePullCredentialsVerificationPolicy = string(in.ImagePullCredentialsVerificationPolicy)
|
||||
out.PreloadedImagesVerificationAllowlist = *(*[]string)(unsafe.Pointer(&in.PreloadedImagesVerificationAllowlist))
|
||||
if err := v1.Convert_Pointer_int32_To_int32(&in.EventRecordQPS, &out.EventRecordQPS, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -622,6 +624,8 @@ func autoConvert_config_KubeletConfiguration_To_v1beta1_KubeletConfiguration(in
|
|||
return err
|
||||
}
|
||||
out.RegistryBurst = in.RegistryBurst
|
||||
out.ImagePullCredentialsVerificationPolicy = configv1beta1.ImagePullCredentialsVerificationPolicy(in.ImagePullCredentialsVerificationPolicy)
|
||||
out.PreloadedImagesVerificationAllowlist = *(*[]string)(unsafe.Pointer(&in.PreloadedImagesVerificationAllowlist))
|
||||
if err := v1.Convert_int32_To_Pointer_int32(&in.EventRecordQPS, &out.EventRecordQPS, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
tracingapi "k8s.io/component-base/tracing/api/v1"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
imagepullmanager "k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
|
||||
utilfs "k8s.io/kubernetes/pkg/util/filesystem"
|
||||
utiltaints "k8s.io/kubernetes/pkg/util/taints"
|
||||
|
|
@ -286,6 +287,32 @@ func ValidateKubeletConfiguration(kc *kubeletconfig.KubeletConfiguration, featur
|
|||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: option %q specified for hairpinMode (--hairpin-mode). Valid options are %q, %q or %q",
|
||||
kc.HairpinMode, kubeletconfig.HairpinNone, kubeletconfig.HairpinVeth, kubeletconfig.PromiscuousBridge))
|
||||
}
|
||||
|
||||
if localFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
switch kc.ImagePullCredentialsVerificationPolicy {
|
||||
case string(kubeletconfig.NeverVerify),
|
||||
string(kubeletconfig.NeverVerifyPreloadedImages),
|
||||
string(kubeletconfig.NeverVerifyAllowlistedImages),
|
||||
string(kubeletconfig.AlwaysVerify):
|
||||
default:
|
||||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: option %q specified for imagePullCredentialsVerificationPolicy. Valid options are %q, %q, %q or %q",
|
||||
kc.ImagePullCredentialsVerificationPolicy, kubeletconfig.NeverVerify, kubeletconfig.NeverVerifyPreloadedImages, kubeletconfig.NeverVerifyAllowlistedImages, kubeletconfig.AlwaysVerify))
|
||||
}
|
||||
|
||||
if len(kc.PreloadedImagesVerificationAllowlist) > 0 && kc.ImagePullCredentialsVerificationPolicy != string(kubeletconfig.NeverVerifyAllowlistedImages) {
|
||||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: can't set `preloadedImagesVerificationAllowlist` if `imagePullCredentialsVertificationPolicy` is not \"NeverVerifyAllowlistedImages\""))
|
||||
} else if err := imagepullmanager.ValidateAllowlistImagesPatterns(kc.PreloadedImagesVerificationAllowlist); err != nil {
|
||||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: invalid image pattern in `preloadedImagesVerificationAllowlist`: %w", err))
|
||||
}
|
||||
} else {
|
||||
if len(kc.ImagePullCredentialsVerificationPolicy) > 0 {
|
||||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: `imagePullCredentialsVerificationPolicy` must not be set if KubeletEnsureSecretPulledImages feature gate is not enabled"))
|
||||
}
|
||||
if len(kc.PreloadedImagesVerificationAllowlist) > 0 {
|
||||
allErrors = append(allErrors, fmt.Errorf("invalid configuration: `preloadedImagesVerificationAllowlist` must not be set if KubeletEnsureSecretPulledImages feature gate is not enabled"))
|
||||
}
|
||||
}
|
||||
|
||||
if kc.ReservedSystemCPUs != "" {
|
||||
// --reserved-cpus does not support --system-reserved-cgroup or --kube-reserved-cgroup
|
||||
if kc.SystemReservedCgroup != "" || kc.KubeReservedCgroup != "" {
|
||||
|
|
|
|||
|
|
@ -728,6 +728,39 @@ func TestValidateKubeletConfiguration(t *testing.T) {
|
|||
return conf
|
||||
},
|
||||
errMsg: "logging.format: Invalid value: \"invalid\": Unsupported log format",
|
||||
}, {
|
||||
name: "invalid imagePullCredentialsVerificationPolicy configuration",
|
||||
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
|
||||
conf.FeatureGates = map[string]bool{"KubeletEnsureSecretPulledImages": true}
|
||||
conf.ImagePullCredentialsVerificationPolicy = "invalid"
|
||||
return conf
|
||||
},
|
||||
errMsg: `option "invalid" specified for imagePullCredentialsVerificationPolicy. Valid options are "NeverVerify", "NeverVerifyPreloadedImages", "NeverVerifyAllowlistedImages" or "AlwaysVerify"]`,
|
||||
}, {
|
||||
name: "invalid PreloadedImagesVerificationAllowlist configuration - featuregate enabled",
|
||||
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
|
||||
conf.FeatureGates = map[string]bool{"KubeletEnsureSecretPulledImages": true}
|
||||
conf.ImagePullCredentialsVerificationPolicy = string(kubeletconfig.NeverVerify)
|
||||
conf.PreloadedImagesVerificationAllowlist = []string{"test.test/repo"}
|
||||
return conf
|
||||
},
|
||||
errMsg: "can't set `preloadedImagesVerificationAllowlist` if `imagePullCredentialsVertificationPolicy` is not \"NeverVerifyAllowlistedImages\"]",
|
||||
}, {
|
||||
name: "invalid PreloadedImagesVerificationAllowlist configuration - featuregate disabled",
|
||||
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
|
||||
conf.FeatureGates = map[string]bool{"KubeletEnsureSecretPulledImages": false}
|
||||
conf.ImagePullCredentialsVerificationPolicy = string(kubeletconfig.NeverVerify)
|
||||
return conf
|
||||
},
|
||||
errMsg: "invalid configuration: `imagePullCredentialsVerificationPolicy` must not be set if KubeletEnsureSecretPulledImages feature gate is not enabled",
|
||||
}, {
|
||||
name: "invalid PreloadedImagesVerificationAllowlist configuration",
|
||||
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
|
||||
conf.FeatureGates = map[string]bool{"KubeletEnsureSecretPulledImages": false}
|
||||
conf.PreloadedImagesVerificationAllowlist = []string{"test.test/repo"}
|
||||
return conf
|
||||
},
|
||||
errMsg: "invalid configuration: `preloadedImagesVerificationAllowlist` must not be set if KubeletEnsureSecretPulledImages feature gate is not enabled",
|
||||
}, {
|
||||
name: "invalid FeatureGate",
|
||||
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
|
||||
|
|
|
|||
100
pkg/kubelet/apis/config/zz_generated.deepcopy.go
generated
100
pkg/kubelet/apis/config/zz_generated.deepcopy.go
generated
|
|
@ -138,6 +138,101 @@ func (in *ExecEnvVar) DeepCopy() *ExecEnvVar {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullCredentials) DeepCopyInto(out *ImagePullCredentials) {
|
||||
*out = *in
|
||||
if in.KubernetesSecrets != nil {
|
||||
in, out := &in.KubernetesSecrets, &out.KubernetesSecrets
|
||||
*out = make([]ImagePullSecret, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullCredentials.
|
||||
func (in *ImagePullCredentials) DeepCopy() *ImagePullCredentials {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullCredentials)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullIntent) DeepCopyInto(out *ImagePullIntent) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullIntent.
|
||||
func (in *ImagePullIntent) DeepCopy() *ImagePullIntent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullIntent)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImagePullIntent) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecret.
|
||||
func (in *ImagePullSecret) DeepCopy() *ImagePullSecret {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullSecret)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePulledRecord) DeepCopyInto(out *ImagePulledRecord) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.LastUpdatedTime.DeepCopyInto(&out.LastUpdatedTime)
|
||||
if in.CredentialMapping != nil {
|
||||
in, out := &in.CredentialMapping, &out.CredentialMapping
|
||||
*out = make(map[string]ImagePullCredentials, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePulledRecord.
|
||||
func (in *ImagePulledRecord) DeepCopy() *ImagePulledRecord {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePulledRecord)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImagePulledRecord) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubeletAnonymousAuthentication) DeepCopyInto(out *KubeletAnonymousAuthentication) {
|
||||
*out = *in
|
||||
|
|
@ -219,6 +314,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) {
|
|||
}
|
||||
out.Authentication = in.Authentication
|
||||
out.Authorization = in.Authorization
|
||||
if in.PreloadedImagesVerificationAllowlist != nil {
|
||||
in, out := &in.PreloadedImagesVerificationAllowlist, &out.PreloadedImagesVerificationAllowlist
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.ClusterDNS != nil {
|
||||
in, out := &in.ClusterDNS, &out.ClusterDNS
|
||||
*out = make([]string, len(*in))
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import (
|
|||
"k8s.io/client-go/util/flowcontrol"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
||||
|
|
@ -151,8 +152,11 @@ type StreamingRuntime interface {
|
|||
// ImageService interfaces allows to work with image service.
|
||||
type ImageService interface {
|
||||
// PullImage pulls an image from the network to local storage using the supplied
|
||||
// secrets if necessary. It returns a reference (digest or ID) to the pulled image.
|
||||
PullImage(ctx context.Context, image ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error)
|
||||
// secrets if necessary.
|
||||
// It returns a reference (digest or ID) to the pulled image and the credentials
|
||||
// that were used to pull the image. If the returned credentials are nil, the
|
||||
// pull was anonymous.
|
||||
PullImage(ctx context.Context, image ImageSpec, credentials []credentialprovider.TrackedAuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error)
|
||||
// GetImageRef gets the reference (digest or ID) of the image which has already been in
|
||||
// the local storage. It returns ("", nil) if the image isn't in the local storage.
|
||||
GetImageRef(ctx context.Context, image ImageSpec) (string, error)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/volume"
|
||||
)
|
||||
|
|
@ -308,7 +309,7 @@ func (f *FakeRuntime) GetContainerLogs(_ context.Context, pod *v1.Pod, container
|
|||
return f.Err
|
||||
}
|
||||
|
||||
func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSpec, creds []credentialprovider.TrackedAuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error) {
|
||||
f.Lock()
|
||||
f.CalledFunctions = append(f.CalledFunctions, "PullImage")
|
||||
if f.Err == nil {
|
||||
|
|
@ -319,9 +320,15 @@ func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSp
|
|||
f.ImageList = append(f.ImageList, i)
|
||||
}
|
||||
|
||||
// if credentials were supplied for the pull at least return the first in the list
|
||||
var retCreds *credentialprovider.TrackedAuthConfig = nil
|
||||
if len(creds) > 0 {
|
||||
retCreds = &creds[0]
|
||||
}
|
||||
|
||||
if !f.BlockImagePulls {
|
||||
f.Unlock()
|
||||
return image.Image, f.Err
|
||||
return image.Image, retCreds, f.Err
|
||||
}
|
||||
|
||||
retErr := f.Err
|
||||
|
|
@ -334,7 +341,8 @@ func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSp
|
|||
case <-ctx.Done():
|
||||
case <-f.imagePullTokenBucket:
|
||||
}
|
||||
return image.Image, retErr
|
||||
|
||||
return image.Image, retCreds, retErr
|
||||
}
|
||||
|
||||
// UnblockImagePulls unblocks a certain number of image pulls, if BlockImagePulls is true.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import (
|
|||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
credentialprovider "k8s.io/kubernetes/pkg/credentialprovider"
|
||||
|
||||
flowcontrol "k8s.io/client-go/util/flowcontrol"
|
||||
|
||||
io "io"
|
||||
|
|
@ -990,32 +992,41 @@ func (_c *MockRuntime_ListPodSandboxMetrics_Call) RunAndReturn(run func(context.
|
|||
return _c
|
||||
}
|
||||
|
||||
// PullImage provides a mock function with given fields: ctx, image, pullSecrets, podSandboxConfig, serviceAccountName
|
||||
func (_m *MockRuntime) PullImage(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
ret := _m.Called(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
// PullImage provides a mock function with given fields: ctx, image, credentials, podSandboxConfig
|
||||
func (_m *MockRuntime) PullImage(ctx context.Context, image container.ImageSpec, credentials []credentialprovider.TrackedAuthConfig, podSandboxConfig *v1.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error) {
|
||||
ret := _m.Called(ctx, image, credentials, podSandboxConfig)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullImage")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) (string, error)); ok {
|
||||
return rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
var r1 *credentialprovider.TrackedAuthConfig
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []credentialprovider.TrackedAuthConfig, *v1.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error)); ok {
|
||||
return rf(ctx, image, credentials, podSandboxConfig)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) string); ok {
|
||||
r0 = rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []credentialprovider.TrackedAuthConfig, *v1.PodSandboxConfig) string); ok {
|
||||
r0 = rf(ctx, image, credentials, podSandboxConfig)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) error); ok {
|
||||
r1 = rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
if rf, ok := ret.Get(1).(func(context.Context, container.ImageSpec, []credentialprovider.TrackedAuthConfig, *v1.PodSandboxConfig) *credentialprovider.TrackedAuthConfig); ok {
|
||||
r1 = rf(ctx, image, credentials, podSandboxConfig)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*credentialprovider.TrackedAuthConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
if rf, ok := ret.Get(2).(func(context.Context, container.ImageSpec, []credentialprovider.TrackedAuthConfig, *v1.PodSandboxConfig) error); ok {
|
||||
r2 = rf(ctx, image, credentials, podSandboxConfig)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// MockRuntime_PullImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PullImage'
|
||||
|
|
@ -1026,26 +1037,25 @@ type MockRuntime_PullImage_Call struct {
|
|||
// PullImage is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - image container.ImageSpec
|
||||
// - pullSecrets []corev1.Secret
|
||||
// - credentials []credentialprovider.TrackedAuthConfig
|
||||
// - podSandboxConfig *v1.PodSandboxConfig
|
||||
// - serviceAccountName string
|
||||
func (_e *MockRuntime_Expecter) PullImage(ctx interface{}, image interface{}, pullSecrets interface{}, podSandboxConfig interface{}, serviceAccountName interface{}) *MockRuntime_PullImage_Call {
|
||||
return &MockRuntime_PullImage_Call{Call: _e.mock.On("PullImage", ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)}
|
||||
func (_e *MockRuntime_Expecter) PullImage(ctx interface{}, image interface{}, credentials interface{}, podSandboxConfig interface{}) *MockRuntime_PullImage_Call {
|
||||
return &MockRuntime_PullImage_Call{Call: _e.mock.On("PullImage", ctx, image, credentials, podSandboxConfig)}
|
||||
}
|
||||
|
||||
func (_c *MockRuntime_PullImage_Call) Run(run func(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig, serviceAccountName string)) *MockRuntime_PullImage_Call {
|
||||
func (_c *MockRuntime_PullImage_Call) Run(run func(ctx context.Context, image container.ImageSpec, credentials []credentialprovider.TrackedAuthConfig, podSandboxConfig *v1.PodSandboxConfig)) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(container.ImageSpec), args[2].([]corev1.Secret), args[3].(*v1.PodSandboxConfig), args[4].(string))
|
||||
run(args[0].(context.Context), args[1].(container.ImageSpec), args[2].([]credentialprovider.TrackedAuthConfig), args[3].(*v1.PodSandboxConfig))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRuntime_PullImage_Call) Return(_a0 string, _a1 error) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
func (_c *MockRuntime_PullImage_Call) Return(_a0 string, _a1 *credentialprovider.TrackedAuthConfig, _a2 error) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Return(_a0, _a1, _a2)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRuntime_PullImage_Call) RunAndReturn(run func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) (string, error)) *MockRuntime_PullImage_Call {
|
||||
func (_c *MockRuntime_PullImage_Call) RunAndReturn(run func(context.Context, container.ImageSpec, []credentialprovider.TrackedAuthConfig, *v1.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error)) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
)
|
||||
|
||||
|
|
@ -44,9 +44,9 @@ type throttledImageService struct {
|
|||
limiter flowcontrol.RateLimiter
|
||||
}
|
||||
|
||||
func (ts throttledImageService) PullImage(ctx context.Context, image kubecontainer.ImageSpec, secrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
func (ts throttledImageService) PullImage(ctx context.Context, image kubecontainer.ImageSpec, credentials []credentialprovider.TrackedAuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, *credentialprovider.TrackedAuthConfig, error) {
|
||||
if ts.limiter.TryAccept() {
|
||||
return ts.ImageService.PullImage(ctx, image, secrets, podSandboxConfig, serviceAccountName)
|
||||
return ts.ImageService.PullImage(ctx, image, credentials, podSandboxConfig)
|
||||
}
|
||||
return "", fmt.Errorf("pull QPS exceeded")
|
||||
return "", nil, fmt.Errorf("pull QPS exceeded")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ const (
|
|||
ImageGarbageCollectedTotalReasonSpace = "space"
|
||||
)
|
||||
|
||||
// PostImageGCHook allows external sources to react to GC collect events.
|
||||
// `remainingImages` is a list of images that were left on the system after garbage
|
||||
// collection finished.
|
||||
type PostImageGCHook func(remainingImages []string, gcStart time.Time)
|
||||
|
||||
// StatsProvider is an interface for fetching stats used during image garbage
|
||||
// collection.
|
||||
type StatsProvider interface {
|
||||
|
|
@ -128,6 +133,8 @@ type realImageGCManager struct {
|
|||
// imageCache is the cache of latest image list.
|
||||
imageCache imageCache
|
||||
|
||||
postGCHooks []PostImageGCHook
|
||||
|
||||
// tracer for recording spans
|
||||
tracer trace.Tracer
|
||||
}
|
||||
|
|
@ -181,7 +188,7 @@ type imageRecord struct {
|
|||
}
|
||||
|
||||
// NewImageGCManager instantiates a new ImageGCManager object.
|
||||
func NewImageGCManager(runtime container.Runtime, statsProvider StatsProvider, recorder record.EventRecorder, nodeRef *v1.ObjectReference, policy ImageGCPolicy, tracerProvider trace.TracerProvider) (ImageGCManager, error) {
|
||||
func NewImageGCManager(runtime container.Runtime, statsProvider StatsProvider, postGCHooks []PostImageGCHook, recorder record.EventRecorder, nodeRef *v1.ObjectReference, policy ImageGCPolicy, tracerProvider trace.TracerProvider) (ImageGCManager, error) {
|
||||
// Validate policy.
|
||||
if policy.HighThresholdPercent < 0 || policy.HighThresholdPercent > 100 {
|
||||
return nil, fmt.Errorf("invalid HighThresholdPercent %d, must be in range [0-100]", policy.HighThresholdPercent)
|
||||
|
|
@ -200,6 +207,7 @@ func NewImageGCManager(runtime container.Runtime, statsProvider StatsProvider, r
|
|||
statsProvider: statsProvider,
|
||||
recorder: recorder,
|
||||
nodeRef: nodeRef,
|
||||
postGCHooks: postGCHooks,
|
||||
tracer: tracer,
|
||||
}
|
||||
|
||||
|
|
@ -381,11 +389,13 @@ func (im *realImageGCManager) GarbageCollect(ctx context.Context, beganGC time.T
|
|||
if usagePercent >= im.policy.HighThresholdPercent {
|
||||
amountToFree := capacity*int64(100-im.policy.LowThresholdPercent)/100 - available
|
||||
klog.InfoS("Disk usage on image filesystem is over the high threshold, trying to free bytes down to the low threshold", "usage", usagePercent, "highThreshold", im.policy.HighThresholdPercent, "amountToFree", amountToFree, "lowThreshold", im.policy.LowThresholdPercent)
|
||||
freed, err := im.freeSpace(ctx, amountToFree, freeTime, images)
|
||||
remainingImages, freed, err := im.freeSpace(ctx, amountToFree, freeTime, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
im.runPostGCHooks(remainingImages, freeTime)
|
||||
|
||||
if freed < amountToFree {
|
||||
err := fmt.Errorf("Failed to garbage collect required amount of images. Attempted to free %d bytes, but only found %d bytes eligible to free.", amountToFree, freed)
|
||||
im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.FreeDiskSpaceFailed, err.Error())
|
||||
|
|
@ -396,6 +406,12 @@ func (im *realImageGCManager) GarbageCollect(ctx context.Context, beganGC time.T
|
|||
return nil
|
||||
}
|
||||
|
||||
func (im *realImageGCManager) runPostGCHooks(remainingImages []string, gcStartTime time.Time) {
|
||||
for _, h := range im.postGCHooks {
|
||||
h(remainingImages, gcStartTime)
|
||||
}
|
||||
}
|
||||
|
||||
func (im *realImageGCManager) freeOldImages(ctx context.Context, images []evictionInfo, freeTime, beganGC time.Time) ([]evictionInfo, error) {
|
||||
if im.policy.MaxAge == 0 {
|
||||
return images, nil
|
||||
|
|
@ -430,29 +446,38 @@ func (im *realImageGCManager) freeOldImages(ctx context.Context, images []evicti
|
|||
func (im *realImageGCManager) DeleteUnusedImages(ctx context.Context) error {
|
||||
klog.InfoS("Attempting to delete unused images")
|
||||
freeTime := time.Now()
|
||||
|
||||
images, err := im.imagesInEvictionOrder(ctx, freeTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = im.freeSpace(ctx, math.MaxInt64, freeTime, images)
|
||||
return err
|
||||
|
||||
remainingImages, _, err := im.freeSpace(ctx, math.MaxInt64, freeTime, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
im.runPostGCHooks(remainingImages, freeTime)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tries to free bytesToFree worth of images on the disk.
|
||||
//
|
||||
// Returns the number of bytes free and an error if any occurred. The number of
|
||||
// bytes freed is always returned.
|
||||
// Returns the images that are still available after the cleanup, the number of bytes freed
|
||||
// and an error if any occurred. The number of bytes freed is always returned.
|
||||
// Note that error may be nil and the number of bytes free may be less
|
||||
// than bytesToFree.
|
||||
func (im *realImageGCManager) freeSpace(ctx context.Context, bytesToFree int64, freeTime time.Time, images []evictionInfo) (int64, error) {
|
||||
func (im *realImageGCManager) freeSpace(ctx context.Context, bytesToFree int64, freeTime time.Time, images []evictionInfo) ([]string, int64, error) {
|
||||
// Delete unused images until we've freed up enough space.
|
||||
var deletionErrors []error
|
||||
spaceFreed := int64(0)
|
||||
var imagesLeft []string
|
||||
for _, image := range images {
|
||||
klog.V(5).InfoS("Evaluating image ID for possible garbage collection based on disk usage", "imageID", image.id, "runtimeHandler", image.imageRecord.runtimeHandlerUsedToPullImage)
|
||||
// Images that are currently in used were given a newer lastUsed.
|
||||
if image.lastUsed.Equal(freeTime) || image.lastUsed.After(freeTime) {
|
||||
klog.V(5).InfoS("Image ID was used too recently, not eligible for garbage collection", "imageID", image.id, "lastUsed", image.lastUsed, "freeTime", freeTime)
|
||||
imagesLeft = append(imagesLeft, image.id)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -460,11 +485,13 @@ func (im *realImageGCManager) freeSpace(ctx context.Context, bytesToFree int64,
|
|||
// In such a case, the image may have just been pulled down, and will be used by a container right away.
|
||||
if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
|
||||
klog.V(5).InfoS("Image ID's age is less than the policy's minAge, not eligible for garbage collection", "imageID", image.id, "age", freeTime.Sub(image.firstDetected), "minAge", im.policy.MinAge)
|
||||
imagesLeft = append(imagesLeft, image.id)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := im.freeImage(ctx, image, ImageGarbageCollectedTotalReasonSpace); err != nil {
|
||||
deletionErrors = append(deletionErrors, err)
|
||||
imagesLeft = append(imagesLeft, image.id)
|
||||
continue
|
||||
}
|
||||
spaceFreed += image.size
|
||||
|
|
@ -475,9 +502,9 @@ func (im *realImageGCManager) freeSpace(ctx context.Context, bytesToFree int64,
|
|||
}
|
||||
|
||||
if len(deletionErrors) > 0 {
|
||||
return spaceFreed, fmt.Errorf("wanted to free %d bytes, but freed %d bytes space with errors in image deletion: %v", bytesToFree, spaceFreed, errors.NewAggregate(deletionErrors))
|
||||
return nil, spaceFreed, fmt.Errorf("wanted to free %d bytes, but freed %d bytes space with errors in image deletion: %w", bytesToFree, spaceFreed, errors.NewAggregate(deletionErrors))
|
||||
}
|
||||
return spaceFreed, nil
|
||||
return imagesLeft, spaceFreed, nil
|
||||
}
|
||||
|
||||
func (im *realImageGCManager) freeImage(ctx context.Context, image evictionInfo, reason string) error {
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@ func TestGarbageCollectImageNotOldEnough(t *testing.T) {
|
|||
func getImagesAndFreeSpace(ctx context.Context, t *testing.T, assert *assert.Assertions, im *realImageGCManager, fakeRuntime *containertest.FakeRuntime, spaceToFree, expectedSpaceFreed int64, imagesLen int, freeTime time.Time) {
|
||||
images, err := im.imagesInEvictionOrder(ctx, freeTime)
|
||||
require.NoError(t, err)
|
||||
spaceFreed, err := im.freeSpace(ctx, spaceToFree, freeTime, images)
|
||||
_, spaceFreed, err := im.freeSpace(ctx, spaceToFree, freeTime, images)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(expectedSpaceFreed, spaceFreed)
|
||||
assert.Len(fakeRuntime.ImageList, imagesLen)
|
||||
|
|
@ -910,7 +910,7 @@ func TestValidateImageGCPolicy(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if _, err := NewImageGCManager(nil, nil, nil, nil, tc.imageGCPolicy, noopoteltrace.NewTracerProvider()); err != nil {
|
||||
if _, err := NewImageGCManager(nil, nil, nil, nil, nil, tc.imageGCPolicy, noopoteltrace.NewTracerProvider()); err != nil {
|
||||
if err.Error() != tc.expectErr {
|
||||
t.Errorf("[%s:]Expected err:%v, but got:%v", tc.name, tc.expectErr, err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,21 @@ import (
|
|||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
crierrors "k8s.io/cri-api/pkg/errors"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
credentialproviderplugin "k8s.io/kubernetes/pkg/credentialprovider/plugin"
|
||||
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/events"
|
||||
"k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
"k8s.io/kubernetes/pkg/util/parsers"
|
||||
)
|
||||
|
|
@ -44,13 +51,15 @@ type ImagePodPullingTimeRecorder interface {
|
|||
|
||||
// imageManager provides the functionalities for image pulling.
|
||||
type imageManager struct {
|
||||
recorder record.EventRecorder
|
||||
imageService kubecontainer.ImageService
|
||||
backOff *flowcontrol.Backoff
|
||||
prevPullErrMsg sync.Map
|
||||
recorder record.EventRecorder
|
||||
imageService kubecontainer.ImageService
|
||||
imagePullManager pullmanager.ImagePullManager
|
||||
backOff *flowcontrol.Backoff
|
||||
prevPullErrMsg sync.Map
|
||||
|
||||
// It will check the presence of the image, and report the 'image pulling', image pulled' events correspondingly.
|
||||
puller imagePuller
|
||||
puller imagePuller
|
||||
nodeKeyring credentialprovider.DockerKeyring
|
||||
|
||||
podPullingTimeRecorder ImagePodPullingTimeRecorder
|
||||
}
|
||||
|
|
@ -58,7 +67,19 @@ type imageManager struct {
|
|||
var _ ImageManager = &imageManager{}
|
||||
|
||||
// NewImageManager instantiates a new ImageManager object.
|
||||
func NewImageManager(recorder record.EventRecorder, imageService kubecontainer.ImageService, imageBackOff *flowcontrol.Backoff, serialized bool, maxParallelImagePulls *int32, qps float32, burst int, podPullingTimeRecorder ImagePodPullingTimeRecorder) ImageManager {
|
||||
func NewImageManager(
|
||||
recorder record.EventRecorder,
|
||||
nodeKeyring credentialprovider.DockerKeyring,
|
||||
imageService kubecontainer.ImageService,
|
||||
imagePullManager pullmanager.ImagePullManager,
|
||||
imageBackOff *flowcontrol.Backoff,
|
||||
serialized bool,
|
||||
maxParallelImagePulls *int32,
|
||||
qps float32,
|
||||
burst int,
|
||||
podPullingTimeRecorder ImagePodPullingTimeRecorder,
|
||||
) ImageManager {
|
||||
|
||||
imageService = throttleImagePulling(imageService, qps, burst)
|
||||
|
||||
var puller imagePuller
|
||||
|
|
@ -70,6 +91,8 @@ func NewImageManager(recorder record.EventRecorder, imageService kubecontainer.I
|
|||
return &imageManager{
|
||||
recorder: recorder,
|
||||
imageService: imageService,
|
||||
imagePullManager: imagePullManager,
|
||||
nodeKeyring: nodeKeyring,
|
||||
backOff: imageBackOff,
|
||||
puller: puller,
|
||||
podPullingTimeRecorder: podPullingTimeRecorder,
|
||||
|
|
@ -78,33 +101,25 @@ func NewImageManager(recorder record.EventRecorder, imageService kubecontainer.I
|
|||
|
||||
// imagePullPrecheck inspects the pull policy and checks for image presence accordingly,
|
||||
// returning (imageRef, error msg, err) and logging any errors.
|
||||
func (m *imageManager) imagePullPrecheck(ctx context.Context, objRef *v1.ObjectReference, logPrefix string, pullPolicy v1.PullPolicy, spec *kubecontainer.ImageSpec, imgRef string) (imageRef string, msg string, err error) {
|
||||
func (m *imageManager) imagePullPrecheck(ctx context.Context, objRef *v1.ObjectReference, logPrefix string, pullPolicy v1.PullPolicy, spec *kubecontainer.ImageSpec, requestedImage string) (imageRef string, msg string, err error) {
|
||||
switch pullPolicy {
|
||||
case v1.PullAlways:
|
||||
return "", msg, nil
|
||||
case v1.PullIfNotPresent:
|
||||
case v1.PullIfNotPresent, v1.PullNever:
|
||||
imageRef, err = m.imageService.GetImageRef(ctx, *spec)
|
||||
if err != nil {
|
||||
msg = fmt.Sprintf("Failed to inspect image %q: %v", imageRef, err)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToInspectImage, logPrefix, msg, klog.Warning)
|
||||
return "", msg, ErrImageInspect
|
||||
}
|
||||
return imageRef, msg, nil
|
||||
case v1.PullNever:
|
||||
imageRef, err = m.imageService.GetImageRef(ctx, *spec)
|
||||
if err != nil {
|
||||
msg = fmt.Sprintf("Failed to inspect image %q: %v", imageRef, err)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToInspectImage, logPrefix, msg, klog.Warning)
|
||||
return "", msg, ErrImageInspect
|
||||
}
|
||||
if imageRef == "" {
|
||||
msg = fmt.Sprintf("Container image %q is not present with pull policy of Never", imgRef)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.ErrImageNeverPullPolicy, logPrefix, msg, klog.Warning)
|
||||
return "", msg, ErrImageNeverPull
|
||||
}
|
||||
return imageRef, msg, nil
|
||||
}
|
||||
return
|
||||
|
||||
if len(imageRef) == 0 && pullPolicy == v1.PullNever {
|
||||
msg, err = m.imageNotPresentOnNeverPolicyError(logPrefix, objRef, requestedImage)
|
||||
return "", msg, err
|
||||
}
|
||||
|
||||
return imageRef, msg, nil
|
||||
}
|
||||
|
||||
// records an event using ref, event msg. log to glog using prefix, msg, logFn
|
||||
|
|
@ -116,15 +131,30 @@ func (m *imageManager) logIt(objRef *v1.ObjectReference, eventtype, event, prefi
|
|||
}
|
||||
}
|
||||
|
||||
// EnsureImageExists pulls the image for the specified pod and imgRef, and returns
|
||||
// imageNotPresentOnNeverPolicy error is a utility function that emits an event about
|
||||
// an image not being present and returns the appropriate error to be passed on.
|
||||
//
|
||||
// Called in 2 scenarios:
|
||||
// 1. image is not present with `imagePullPolicy: Never“
|
||||
// 2. image is present but cannot be accessed with the presented set of credentials
|
||||
//
|
||||
// We don't want to reveal the presence of an image if it cannot be accessed, hence we
|
||||
// want the same behavior in both the above scenarios.
|
||||
func (m *imageManager) imageNotPresentOnNeverPolicyError(logPrefix string, objRef *v1.ObjectReference, requestedImage string) (string, error) {
|
||||
msg := fmt.Sprintf("Container image %q is not present with pull policy of Never", requestedImage)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.ErrImageNeverPullPolicy, logPrefix, msg, klog.Warning)
|
||||
return msg, ErrImageNeverPull
|
||||
}
|
||||
|
||||
// EnsureImageExists pulls the image for the specified pod and requestedImage, and returns
|
||||
// (imageRef, error message, error).
|
||||
func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectReference, pod *v1.Pod, imgRef string, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, podRuntimeHandler string, pullPolicy v1.PullPolicy) (imageRef, message string, err error) {
|
||||
logPrefix := fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, imgRef)
|
||||
func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectReference, pod *v1.Pod, requestedImage string, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, podRuntimeHandler string, pullPolicy v1.PullPolicy) (imageRef, message string, err error) {
|
||||
logPrefix := fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, requestedImage)
|
||||
|
||||
// If the image contains no tag or digest, a default tag should be applied.
|
||||
image, err := applyDefaultImageTag(imgRef)
|
||||
image, err := applyDefaultImageTag(requestedImage)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Failed to apply default image tag %q: %v", imgRef, err)
|
||||
msg := fmt.Sprintf("Failed to apply default image tag %q: %v", requestedImage, err)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToInspectImage, logPrefix, msg, klog.Warning)
|
||||
return "", msg, ErrInvalidImageName
|
||||
}
|
||||
|
|
@ -143,19 +173,99 @@ func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectR
|
|||
RuntimeHandler: podRuntimeHandler,
|
||||
}
|
||||
|
||||
imageRef, message, err = m.imagePullPrecheck(ctx, objRef, logPrefix, pullPolicy, &spec, imgRef)
|
||||
imageRef, message, err = m.imagePullPrecheck(ctx, objRef, logPrefix, pullPolicy, &spec, requestedImage)
|
||||
if err != nil {
|
||||
return "", message, err
|
||||
}
|
||||
if imageRef != "" {
|
||||
msg := fmt.Sprintf("Container image %q already present on machine", imgRef)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PulledImage, logPrefix, msg, klog.Info)
|
||||
return imageRef, msg, nil
|
||||
|
||||
repoToPull, _, _, err := parsers.ParseImageName(spec.Image)
|
||||
if err != nil {
|
||||
return "", err.Error(), err
|
||||
}
|
||||
|
||||
backOffKey := fmt.Sprintf("%s_%s", pod.UID, imgRef)
|
||||
// construct the dynamic keyring using the providers we have in the kubelet
|
||||
var podName, podNamespace, podUID string
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
sandboxMetadata := podSandboxConfig.GetMetadata()
|
||||
|
||||
podName = sandboxMetadata.Name
|
||||
podNamespace = sandboxMetadata.Namespace
|
||||
podUID = sandboxMetadata.Uid
|
||||
}
|
||||
|
||||
externalCredentialProviderKeyring := credentialproviderplugin.NewExternalCredentialProviderDockerKeyring(
|
||||
podNamespace,
|
||||
podName,
|
||||
podUID,
|
||||
pod.Spec.ServiceAccountName)
|
||||
|
||||
keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, credentialprovider.UnionDockerKeyring{m.nodeKeyring, externalCredentialProviderKeyring})
|
||||
if err != nil {
|
||||
return "", err.Error(), err
|
||||
}
|
||||
|
||||
pullCredentials, _ := keyring.Lookup(repoToPull)
|
||||
|
||||
if imageRef != "" {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
msg := fmt.Sprintf("Container image %q already present on machine", requestedImage)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PulledImage, logPrefix, msg, klog.Info)
|
||||
return imageRef, msg, nil
|
||||
}
|
||||
|
||||
var imagePullSecrets []kubeletconfiginternal.ImagePullSecret
|
||||
for _, s := range pullCredentials {
|
||||
if s.Source == nil {
|
||||
// we're only interested in creds that are not node accessible
|
||||
continue
|
||||
}
|
||||
imagePullSecrets = append(imagePullSecrets, kubeletconfiginternal.ImagePullSecret{
|
||||
UID: string(s.Source.Secret.UID),
|
||||
Name: s.Source.Secret.Name,
|
||||
Namespace: s.Source.Secret.Namespace,
|
||||
CredentialHash: s.AuthConfigHash,
|
||||
})
|
||||
}
|
||||
|
||||
pullRequired := m.imagePullManager.MustAttemptImagePull(requestedImage, imageRef, imagePullSecrets)
|
||||
if !pullRequired {
|
||||
msg := fmt.Sprintf("Container image %q already present on machine and can be accessed by the pod", requestedImage)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PulledImage, logPrefix, msg, klog.Info)
|
||||
return imageRef, msg, nil
|
||||
}
|
||||
}
|
||||
|
||||
if pullPolicy == v1.PullNever {
|
||||
// The image is present as confirmed by imagePullPrecheck but it apparently
|
||||
// wasn't accessible given the credentials check by the imagePullManager.
|
||||
msg, err := m.imageNotPresentOnNeverPolicyError(logPrefix, objRef, requestedImage)
|
||||
return "", msg, err
|
||||
}
|
||||
|
||||
return m.pullImage(ctx, logPrefix, objRef, pod.UID, requestedImage, spec, pullCredentials, podSandboxConfig)
|
||||
}
|
||||
|
||||
func (m *imageManager) pullImage(ctx context.Context, logPrefix string, objRef *v1.ObjectReference, podUID types.UID, image string, imgSpec kubecontainer.ImageSpec, pullCredentials []credentialprovider.TrackedAuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (imageRef, message string, err error) {
|
||||
var pullSucceeded bool
|
||||
var finalPullCredentials *credentialprovider.TrackedAuthConfig
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
if err := m.imagePullManager.RecordPullIntent(image); err != nil {
|
||||
return "", fmt.Sprintf("Failed to record image pull intent for container image %q: %v", image, err), err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if pullSucceeded {
|
||||
m.imagePullManager.RecordImagePulled(image, imageRef, trackedToImagePullCreds(finalPullCredentials))
|
||||
} else {
|
||||
m.imagePullManager.RecordImagePullFailed(image)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
backOffKey := fmt.Sprintf("%s_%s", podUID, image)
|
||||
if m.backOff.IsInBackOffSinceUpdate(backOffKey, m.backOff.Clock.Now()) {
|
||||
msg := fmt.Sprintf("Back-off pulling image %q", imgRef)
|
||||
msg := fmt.Sprintf("Back-off pulling image %q", image)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.BackOffPullImage, logPrefix, msg, klog.Info)
|
||||
|
||||
// Wrap the error from the actual pull if available.
|
||||
|
|
@ -171,17 +281,17 @@ func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectR
|
|||
// Ensure that the map cannot grow indefinitely.
|
||||
m.prevPullErrMsg.Delete(backOffKey)
|
||||
|
||||
m.podPullingTimeRecorder.RecordImageStartedPulling(pod.UID)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PullingImage, logPrefix, fmt.Sprintf("Pulling image %q", imgRef), klog.Info)
|
||||
m.podPullingTimeRecorder.RecordImageStartedPulling(podUID)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PullingImage, logPrefix, fmt.Sprintf("Pulling image %q", image), klog.Info)
|
||||
startTime := time.Now()
|
||||
|
||||
pullChan := make(chan pullResult)
|
||||
m.puller.pullImage(ctx, spec, pullSecrets, pullChan, podSandboxConfig, pod.Spec.ServiceAccountName)
|
||||
m.puller.pullImage(ctx, imgSpec, pullCredentials, pullChan, podSandboxConfig)
|
||||
imagePullResult := <-pullChan
|
||||
if imagePullResult.err != nil {
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToPullImage, logPrefix, fmt.Sprintf("Failed to pull image %q: %v", imgRef, imagePullResult.err), klog.Warning)
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToPullImage, logPrefix, fmt.Sprintf("Failed to pull image %q: %v", image, imagePullResult.err), klog.Warning)
|
||||
m.backOff.Next(backOffKey, m.backOff.Clock.Now())
|
||||
|
||||
msg, err := evalCRIPullErr(imgRef, imagePullResult.err)
|
||||
msg, err := evalCRIPullErr(image, imagePullResult.err)
|
||||
|
||||
// Store the actual pull error for providing that information during
|
||||
// the image pull back-off.
|
||||
|
|
@ -189,12 +299,15 @@ func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectR
|
|||
|
||||
return "", msg, err
|
||||
}
|
||||
m.podPullingTimeRecorder.RecordImageFinishedPulling(pod.UID)
|
||||
m.podPullingTimeRecorder.RecordImageFinishedPulling(podUID)
|
||||
imagePullDuration := time.Since(startTime).Truncate(time.Millisecond)
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PulledImage, logPrefix, fmt.Sprintf("Successfully pulled image %q in %v (%v including waiting). Image size: %v bytes.",
|
||||
imgRef, imagePullResult.pullDuration.Truncate(time.Millisecond), imagePullDuration, imagePullResult.imageSize), klog.Info)
|
||||
image, imagePullResult.pullDuration.Truncate(time.Millisecond), imagePullDuration, imagePullResult.imageSize), klog.Info)
|
||||
metrics.ImagePullDuration.WithLabelValues(metrics.GetImageSizeBucket(imagePullResult.imageSize)).Observe(imagePullDuration.Seconds())
|
||||
m.backOff.GC()
|
||||
finalPullCredentials = imagePullResult.credentialsUsed
|
||||
pullSucceeded = true
|
||||
|
||||
return imagePullResult.imageRef, "", nil
|
||||
}
|
||||
|
||||
|
|
@ -247,3 +360,23 @@ func applyDefaultImageTag(image string) (string, error) {
|
|||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func trackedToImagePullCreds(trackedCreds *credentialprovider.TrackedAuthConfig) *kubeletconfiginternal.ImagePullCredentials {
|
||||
ret := &kubeletconfiginternal.ImagePullCredentials{}
|
||||
switch {
|
||||
case trackedCreds == nil, trackedCreds.Source == nil:
|
||||
ret.NodePodsAccessible = true
|
||||
default:
|
||||
sourceSecret := trackedCreds.Source.Secret
|
||||
ret.KubernetesSecrets = []kubeletconfiginternal.ImagePullSecret{
|
||||
{
|
||||
UID: sourceSecret.UID,
|
||||
Name: sourceSecret.Name,
|
||||
Namespace: sourceSecret.Namespace,
|
||||
CredentialHash: trackedCreds.AuthConfigHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,19 @@ import (
|
|||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/component-base/featuregate"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
crierrors "k8s.io/cri-api/pkg/errors"
|
||||
"k8s.io/kubernetes/pkg/controller/testutil"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
. "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
ctest "k8s.io/kubernetes/pkg/kubelet/container/testing"
|
||||
"k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
"k8s.io/utils/ptr"
|
||||
|
|
@ -53,17 +58,29 @@ type pullerExpects struct {
|
|||
}
|
||||
|
||||
type pullerTestCase struct {
|
||||
testName string
|
||||
containerImage string
|
||||
policy v1.PullPolicy
|
||||
inspectErr error
|
||||
pullerErr error
|
||||
qps float32
|
||||
burst int
|
||||
expected []pullerExpects
|
||||
testName string
|
||||
containerImage string
|
||||
policy v1.PullPolicy
|
||||
pullSecrets []v1.Secret
|
||||
allowedCredentials map[string][]kubeletconfiginternal.ImagePullSecret // image -> allowedCredentials; nil means allow all
|
||||
inspectErr error
|
||||
pullerErr error
|
||||
qps float32
|
||||
burst int
|
||||
expected []pullerExpects
|
||||
enableFeatures []featuregate.Feature
|
||||
}
|
||||
|
||||
func pullerTestCases() []pullerTestCase {
|
||||
return append(
|
||||
noFGPullerTestCases(),
|
||||
ensureSecretImagesTestCases()...,
|
||||
)
|
||||
}
|
||||
|
||||
// noFGPullerTestCases returns all test cases that test the default behavior without any
|
||||
// feature gate required
|
||||
func noFGPullerTestCases() []pullerTestCase {
|
||||
return []pullerTestCase{
|
||||
{ // pull missing image
|
||||
testName: "image missing, pull",
|
||||
|
|
@ -82,7 +99,7 @@ func pullerTestCases() []pullerTestCase {
|
|||
}},
|
||||
|
||||
{ // image present, don't pull
|
||||
testName: "image present, don't pull ",
|
||||
testName: "image present, allow all, don't pull ",
|
||||
containerImage: "present_image",
|
||||
policy: v1.PullIfNotPresent,
|
||||
inspectErr: nil,
|
||||
|
|
@ -335,6 +352,142 @@ func pullerTestCases() []pullerTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
// ensureSecretImages returns test cases specific for the KubeletEnsureSecretPulledImages
|
||||
// featuregate plus a copy of all non-featuregated tests, but it requests the featuregate
|
||||
// to be enabled there, too
|
||||
func ensureSecretImagesTestCases() []pullerTestCase {
|
||||
testCases := []pullerTestCase{
|
||||
{
|
||||
testName: "[KubeletEnsureSecretPulledImages] image present, unknown to image pull manager, pull",
|
||||
containerImage: "present_image",
|
||||
policy: v1.PullIfNotPresent,
|
||||
allowedCredentials: map[string][]kubeletconfiginternal.ImagePullSecret{
|
||||
"another_image": {{Namespace: "testns", Name: "testname", UID: "testuid"}},
|
||||
},
|
||||
pullSecrets: []v1.Secret{makeDockercfgSecretForRepo(metav1.ObjectMeta{Namespace: "testns", Name: "testname", UID: "testuid"}, "docker.io/library/present_image")},
|
||||
inspectErr: nil,
|
||||
pullerErr: nil,
|
||||
qps: 0.0,
|
||||
burst: 0,
|
||||
enableFeatures: []featuregate.Feature{features.KubeletEnsureSecretPulledImages},
|
||||
expected: []pullerExpects{
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
}},
|
||||
{
|
||||
testName: "[KubeletEnsureSecretPulledImages] image present, unknown secret to image pull manager, pull",
|
||||
containerImage: "present_image",
|
||||
policy: v1.PullIfNotPresent,
|
||||
allowedCredentials: map[string][]kubeletconfiginternal.ImagePullSecret{
|
||||
"present_image": {{Namespace: "testns", Name: "testname", UID: "testuid"}},
|
||||
},
|
||||
pullSecrets: []v1.Secret{makeDockercfgSecretForRepo(metav1.ObjectMeta{Namespace: "testns", Name: "testname", UID: "someothertestuid"}, "docker.io/library/present_image")},
|
||||
inspectErr: nil,
|
||||
pullerErr: nil,
|
||||
qps: 0.0,
|
||||
burst: 0,
|
||||
enableFeatures: []featuregate.Feature{features.KubeletEnsureSecretPulledImages},
|
||||
expected: []pullerExpects{
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef", "PullImage", "GetImageSize"}, nil, true, true,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulling"},
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "[KubeletEnsureSecretPulledImages] image present, unknown secret to image pull manager, never pull policy -> fail",
|
||||
containerImage: "present_image",
|
||||
policy: v1.PullNever,
|
||||
allowedCredentials: map[string][]kubeletconfiginternal.ImagePullSecret{
|
||||
"present_image": {{Namespace: "testns", Name: "testname", UID: "testuid"}},
|
||||
},
|
||||
pullSecrets: []v1.Secret{makeDockercfgSecretForRepo(metav1.ObjectMeta{Namespace: "testns", Name: "testname", UID: "someothertestuid"}, "docker.io/library/present_image")},
|
||||
inspectErr: nil,
|
||||
pullerErr: nil,
|
||||
qps: 0.0,
|
||||
burst: 0,
|
||||
enableFeatures: []featuregate.Feature{features.KubeletEnsureSecretPulledImages},
|
||||
expected: []pullerExpects{
|
||||
{[]string{"GetImageRef"}, ErrImageNeverPull, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "ErrImageNeverPull"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef"}, ErrImageNeverPull, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "ErrImageNeverPull"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef"}, ErrImageNeverPull, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "ErrImageNeverPull"},
|
||||
}, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "[KubeletEnsureSecretPulledImages] image present, a secret matches one of known to the image pull manager, don't pull",
|
||||
containerImage: "present_image",
|
||||
policy: v1.PullIfNotPresent,
|
||||
allowedCredentials: map[string][]kubeletconfiginternal.ImagePullSecret{
|
||||
"present_image": {{Namespace: "testns", Name: "testname", UID: "testuid"}},
|
||||
},
|
||||
pullSecrets: []v1.Secret{
|
||||
makeDockercfgSecretForRepo(metav1.ObjectMeta{Namespace: "testns", Name: "testname", UID: "someothertestuid"}, "docker.io/library/present_image"),
|
||||
makeDockercfgSecretForRepo(metav1.ObjectMeta{Namespace: "testns", Name: "testname", UID: "testuid"}, "docker.io/library/present_image"),
|
||||
},
|
||||
inspectErr: nil,
|
||||
pullerErr: nil,
|
||||
qps: 0.0,
|
||||
burst: 0,
|
||||
enableFeatures: []featuregate.Feature{features.KubeletEnsureSecretPulledImages},
|
||||
expected: []pullerExpects{
|
||||
{[]string{"GetImageRef"}, nil, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef"}, nil, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
{[]string{"GetImageRef"}, nil, false, false,
|
||||
[]v1.Event{
|
||||
{Reason: "Pulled"},
|
||||
}, ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range noFGPullerTestCases() {
|
||||
tc.testName = "[KubeletEnsureSecretPulledImages] " + tc.testName
|
||||
tc.enableFeatures = append(tc.enableFeatures, features.KubeletEnsureSecretPulledImages)
|
||||
testCases = append(testCases, tc)
|
||||
}
|
||||
|
||||
return testCases
|
||||
}
|
||||
|
||||
type mockPodPullingTimeRecorder struct {
|
||||
sync.Mutex
|
||||
startedPullingRecorded bool
|
||||
|
|
@ -360,7 +513,46 @@ func (m *mockPodPullingTimeRecorder) reset() {
|
|||
m.finishedPullingRecorded = false
|
||||
}
|
||||
|
||||
func pullerTestEnv(t *testing.T, c pullerTestCase, serialized bool, maxParallelImagePulls *int32) (puller ImageManager, fakeClock *testingclock.FakeClock, fakeRuntime *ctest.FakeRuntime, container *v1.Container, fakePodPullingTimeRecorder *mockPodPullingTimeRecorder, fakeRecorder *testutil.FakeRecorder) {
|
||||
type mockImagePullManager struct {
|
||||
pullmanager.NoopImagePullManager
|
||||
|
||||
imageAllowlist map[string]sets.Set[kubeletconfiginternal.ImagePullSecret]
|
||||
allowAll bool
|
||||
}
|
||||
|
||||
func (m *mockImagePullManager) MustAttemptImagePull(image, _ string, podSecrets []kubeletconfiginternal.ImagePullSecret) bool {
|
||||
if m.allowAll == true {
|
||||
return false
|
||||
}
|
||||
|
||||
cachedSecrets, ok := m.imageAllowlist[image]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// cut off all the hashes and only determine the match based on the secret coords to simplify testing
|
||||
for _, s := range podSecrets {
|
||||
if cachedSecrets.Has(kubeletconfiginternal.ImagePullSecret{Namespace: s.Namespace, Name: s.Name, UID: s.UID}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func pullerTestEnv(
|
||||
t *testing.T,
|
||||
c pullerTestCase,
|
||||
serialized bool,
|
||||
maxParallelImagePulls *int32,
|
||||
) (
|
||||
puller ImageManager,
|
||||
fakeClock *testingclock.FakeClock,
|
||||
fakeRuntime *ctest.FakeRuntime,
|
||||
container *v1.Container,
|
||||
fakePodPullingTimeRecorder *mockPodPullingTimeRecorder,
|
||||
fakeRecorder *testutil.FakeRecorder,
|
||||
) {
|
||||
container = &v1.Container{
|
||||
Name: "container_name",
|
||||
Image: c.containerImage,
|
||||
|
|
@ -380,7 +572,19 @@ func pullerTestEnv(t *testing.T, c pullerTestCase, serialized bool, maxParallelI
|
|||
|
||||
fakePodPullingTimeRecorder = &mockPodPullingTimeRecorder{}
|
||||
|
||||
puller = NewImageManager(fakeRecorder, fakeRuntime, backOff, serialized, maxParallelImagePulls, c.qps, c.burst, fakePodPullingTimeRecorder)
|
||||
pullManager := &mockImagePullManager{allowAll: true}
|
||||
if c.allowedCredentials != nil {
|
||||
pullManager.allowAll = false
|
||||
pullManager.imageAllowlist = make(map[string]sets.Set[kubeletconfiginternal.ImagePullSecret])
|
||||
for image, secrets := range c.allowedCredentials {
|
||||
pullManager.imageAllowlist[image] = sets.New(secrets...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fg := range c.enableFeatures {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, fg, true)
|
||||
}
|
||||
puller = NewImageManager(fakeRecorder, &credentialprovider.BasicDockerKeyring{}, fakeRuntime, pullManager, backOff, serialized, maxParallelImagePulls, c.qps, c.burst, fakePodPullingTimeRecorder)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +609,7 @@ func TestParallelPuller(t *testing.T) {
|
|||
fakeRuntime.CalledFunctions = nil
|
||||
fakeClock.Step(time.Second)
|
||||
|
||||
_, msg, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, msg, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, c.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
fakeRuntime.AssertCalls(expected.calls)
|
||||
assert.Equal(t, expected.err, err)
|
||||
assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
|
||||
|
|
@ -438,7 +642,7 @@ func TestSerializedPuller(t *testing.T) {
|
|||
fakeRuntime.CalledFunctions = nil
|
||||
fakeClock.Step(time.Second)
|
||||
|
||||
_, msg, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, msg, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, c.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
fakeRuntime.AssertCalls(expected.calls)
|
||||
assert.Equal(t, expected.err, err)
|
||||
assert.Equal(t, expected.shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
|
||||
|
|
@ -502,7 +706,7 @@ func TestPullAndListImageWithPodAnnotations(t *testing.T) {
|
|||
fakeRuntime.ImageList = []Image{}
|
||||
fakeClock.Step(time.Second)
|
||||
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, c.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
fakeRuntime.AssertCalls(c.expected[0].calls)
|
||||
assert.Equal(t, c.expected[0].err, err, "tick=%d", 0)
|
||||
assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
|
||||
|
|
@ -559,7 +763,7 @@ func TestPullAndListImageWithRuntimeHandlerInImageCriAPIFeatureGate(t *testing.T
|
|||
fakeRuntime.ImageList = []Image{}
|
||||
fakeClock.Step(time.Second)
|
||||
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, runtimeHandler, container.ImagePullPolicy)
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, c.pullSecrets, nil, runtimeHandler, container.ImagePullPolicy)
|
||||
fakeRuntime.AssertCalls(c.expected[0].calls)
|
||||
assert.Equal(t, c.expected[0].err, err, "tick=%d", 0)
|
||||
assert.Equal(t, c.expected[0].shouldRecordStartedPullingTime, fakePodPullingTimeRecorder.startedPullingRecorded)
|
||||
|
|
@ -618,7 +822,7 @@ func TestMaxParallelImagePullsLimit(t *testing.T) {
|
|||
for i := 0; i < maxParallelImagePulls; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, testCase.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
assert.NoError(t, err)
|
||||
wg.Done()
|
||||
}()
|
||||
|
|
@ -630,7 +834,7 @@ func TestMaxParallelImagePullsLimit(t *testing.T) {
|
|||
for i := 0; i < 2; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, _, err := puller.EnsureImageExists(ctx, nil, pod, container.Image, testCase.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
assert.NoError(t, err)
|
||||
wg.Done()
|
||||
}()
|
||||
|
|
@ -731,7 +935,7 @@ func TestImagePullPrecheck(t *testing.T) {
|
|||
fakeRecorder.Events = []*v1.Event{}
|
||||
fakeClock.Step(time.Second)
|
||||
|
||||
_, _, err := puller.EnsureImageExists(ctx, &v1.ObjectReference{}, pod, container.Image, nil, nil, "", container.ImagePullPolicy)
|
||||
_, _, err := puller.EnsureImageExists(ctx, &v1.ObjectReference{}, pod, container.Image, c.pullSecrets, nil, "", container.ImagePullPolicy)
|
||||
fakeRuntime.AssertCalls(expected.calls)
|
||||
var recorderEvents []v1.Event
|
||||
for _, event := range fakeRecorder.Events {
|
||||
|
|
@ -747,3 +951,13 @@ func TestImagePullPrecheck(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeDockercfgSecretForRepo(sMeta metav1.ObjectMeta, repo string) v1.Secret {
|
||||
return v1.Secret{
|
||||
ObjectMeta: sMeta,
|
||||
Type: v1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
v1.DockerConfigJsonKey: []byte(`{"auths": {"` + repo + `": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,21 +20,22 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
)
|
||||
|
||||
type pullResult struct {
|
||||
imageRef string
|
||||
imageSize uint64
|
||||
err error
|
||||
pullDuration time.Duration
|
||||
imageRef string
|
||||
imageSize uint64
|
||||
err error
|
||||
pullDuration time.Duration
|
||||
credentialsUsed *credentialprovider.TrackedAuthConfig
|
||||
}
|
||||
|
||||
type imagePuller interface {
|
||||
pullImage(context.Context, kubecontainer.ImageSpec, []v1.Secret, chan<- pullResult, *runtimeapi.PodSandboxConfig, string)
|
||||
pullImage(context.Context, kubecontainer.ImageSpec, []credentialprovider.TrackedAuthConfig, chan<- pullResult, *runtimeapi.PodSandboxConfig)
|
||||
}
|
||||
|
||||
var _, _ imagePuller = ¶llelImagePuller{}, &serialImagePuller{}
|
||||
|
|
@ -51,24 +52,25 @@ func newParallelImagePuller(imageService kubecontainer.ImageService, maxParallel
|
|||
return ¶llelImagePuller{imageService, make(chan struct{}, *maxParallelImagePulls)}
|
||||
}
|
||||
|
||||
func (pip *parallelImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) {
|
||||
func (pip *parallelImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, credentials []credentialprovider.TrackedAuthConfig, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
|
||||
go func() {
|
||||
if pip.tokens != nil {
|
||||
pip.tokens <- struct{}{}
|
||||
defer func() { <-pip.tokens }()
|
||||
}
|
||||
startTime := time.Now()
|
||||
imageRef, err := pip.imageService.PullImage(ctx, spec, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
imageRef, creds, err := pip.imageService.PullImage(ctx, spec, credentials, podSandboxConfig)
|
||||
var size uint64
|
||||
if err == nil && imageRef != "" {
|
||||
// Getting the image size with best effort, ignoring the error.
|
||||
size, _ = pip.imageService.GetImageSize(ctx, spec)
|
||||
}
|
||||
pullChan <- pullResult{
|
||||
imageRef: imageRef,
|
||||
imageSize: size,
|
||||
err: err,
|
||||
pullDuration: time.Since(startTime),
|
||||
imageRef: imageRef,
|
||||
imageSize: size,
|
||||
err: err,
|
||||
pullDuration: time.Since(startTime),
|
||||
credentialsUsed: creds,
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -88,29 +90,27 @@ func newSerialImagePuller(imageService kubecontainer.ImageService) imagePuller {
|
|||
}
|
||||
|
||||
type imagePullRequest struct {
|
||||
ctx context.Context
|
||||
spec kubecontainer.ImageSpec
|
||||
pullSecrets []v1.Secret
|
||||
pullChan chan<- pullResult
|
||||
podSandboxConfig *runtimeapi.PodSandboxConfig
|
||||
serviceAccountName string
|
||||
ctx context.Context
|
||||
spec kubecontainer.ImageSpec
|
||||
credentials []credentialprovider.TrackedAuthConfig
|
||||
pullChan chan<- pullResult
|
||||
podSandboxConfig *runtimeapi.PodSandboxConfig
|
||||
}
|
||||
|
||||
func (sip *serialImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) {
|
||||
func (sip *serialImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, credentials []credentialprovider.TrackedAuthConfig, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
|
||||
sip.pullRequests <- &imagePullRequest{
|
||||
ctx: ctx,
|
||||
spec: spec,
|
||||
pullSecrets: pullSecrets,
|
||||
pullChan: pullChan,
|
||||
podSandboxConfig: podSandboxConfig,
|
||||
serviceAccountName: serviceAccountName,
|
||||
ctx: ctx,
|
||||
spec: spec,
|
||||
credentials: credentials,
|
||||
pullChan: pullChan,
|
||||
podSandboxConfig: podSandboxConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (sip *serialImagePuller) processImagePullRequests() {
|
||||
for pullRequest := range sip.pullRequests {
|
||||
startTime := time.Now()
|
||||
imageRef, err := sip.imageService.PullImage(pullRequest.ctx, pullRequest.spec, pullRequest.pullSecrets, pullRequest.podSandboxConfig, pullRequest.serviceAccountName)
|
||||
imageRef, creds, err := sip.imageService.PullImage(pullRequest.ctx, pullRequest.spec, pullRequest.credentials, pullRequest.podSandboxConfig)
|
||||
var size uint64
|
||||
if err == nil && imageRef != "" {
|
||||
// Getting the image size with best effort, ignoring the error.
|
||||
|
|
@ -120,8 +120,9 @@ func (sip *serialImagePuller) processImagePullRequests() {
|
|||
imageRef: imageRef,
|
||||
imageSize: size,
|
||||
err: err,
|
||||
// Note: pullDuration includes credential resolution and getting the image size.
|
||||
pullDuration: time.Since(startTime),
|
||||
// Note: pullDuration includes getting the image size.
|
||||
pullDuration: time.Since(startTime),
|
||||
credentialsUsed: creds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
pkg/kubelet/images/pullmanager/doc.go
Normal file
19
pkg/kubelet/images/pullmanager/doc.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// pullmanager package keeps the implementation of the image pull manager and
|
||||
// image credential verification policies
|
||||
package pullmanager
|
||||
302
pkg/kubelet/images/pullmanager/fs_pullrecords.go
Normal file
302
pkg/kubelet/images/pullmanager/fs_pullrecords.go
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
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 pullmanager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
kubeletconfigv1alpha1 "k8s.io/kubelet/config/v1alpha1"
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
kubeletconfigvint1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheFilesSHA256Prefix = "sha256-"
|
||||
tmpFilesSuffix = ".tmp"
|
||||
)
|
||||
|
||||
var _ PullRecordsAccessor = &fsPullRecordsAccessor{}
|
||||
|
||||
// fsPullRecordsAccessor uses the filesystem to read/write ImagePullIntent/ImagePulledRecord
|
||||
// records.
|
||||
type fsPullRecordsAccessor struct {
|
||||
pullingDir string
|
||||
pulledDir string
|
||||
|
||||
encoder runtime.Encoder
|
||||
decoder runtime.Decoder
|
||||
}
|
||||
|
||||
// NewFSPullRecordsAccessor returns an accessor for the ImagePullIntent/ImagePulledRecord
|
||||
// records with a filesystem as the backing database.
|
||||
func NewFSPullRecordsAccessor(kubeletDir string) (*fsPullRecordsAccessor, error) {
|
||||
kubeletConfigEncoder, kubeletConfigDecoder, err := createKubeletConfigSchemeEncoderDecoder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessor := &fsPullRecordsAccessor{
|
||||
pullingDir: filepath.Join(kubeletDir, "image_manager", "pulling"),
|
||||
pulledDir: filepath.Join(kubeletDir, "image_manager", "pulled"),
|
||||
|
||||
encoder: kubeletConfigEncoder,
|
||||
decoder: kubeletConfigDecoder,
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(accessor.pullingDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(accessor.pulledDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return accessor, nil
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) WriteImagePullIntent(image string) error {
|
||||
intent := kubeletconfiginternal.ImagePullIntent{
|
||||
Image: image,
|
||||
}
|
||||
|
||||
intentBytes := bytes.NewBuffer([]byte{})
|
||||
if err := f.encoder.Encode(&intent, intentBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeFile(f.pullingDir, cacheFilename(image), intentBytes.Bytes())
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) ListImagePullIntents() ([]*kubeletconfiginternal.ImagePullIntent, error) {
|
||||
var intents []*kubeletconfiginternal.ImagePullIntent
|
||||
// walk the pulling directory for any pull intent records
|
||||
err := processDirFiles(f.pullingDir,
|
||||
func(filePath string, fileContent []byte) error {
|
||||
intent, err := decodeIntent(f.decoder, fileContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deserialize content of file %q into ImagePullIntent: %w", filePath, err)
|
||||
}
|
||||
intents = append(intents, intent)
|
||||
|
||||
return nil
|
||||
})
|
||||
return intents, err
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) ImagePullIntentExists(image string) (bool, error) {
|
||||
intentRecordPath := filepath.Join(f.pullingDir, cacheFilename(image))
|
||||
intentBytes, err := os.ReadFile(intentRecordPath)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
intent, err := decodeIntent(f.decoder, intentBytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return intent.Image == image, nil
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) DeleteImagePullIntent(image string) error {
|
||||
err := os.Remove(filepath.Join(f.pullingDir, cacheFilename(image)))
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) GetImagePulledRecord(imageRef string) (*kubeletconfiginternal.ImagePulledRecord, bool, error) {
|
||||
recordBytes, err := os.ReadFile(filepath.Join(f.pulledDir, cacheFilename(imageRef)))
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
pulledRecord, err := decodePulledRecord(f.decoder, recordBytes)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if pulledRecord.ImageRef != imageRef {
|
||||
return nil, false, nil
|
||||
}
|
||||
return pulledRecord, true, err
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) ListImagePulledRecords() ([]*kubeletconfiginternal.ImagePulledRecord, error) {
|
||||
var pullRecords []*kubeletconfiginternal.ImagePulledRecord
|
||||
err := processDirFiles(f.pulledDir,
|
||||
func(filePath string, fileContent []byte) error {
|
||||
pullRecord, err := decodePulledRecord(f.decoder, fileContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deserialize content of file %q into ImagePulledRecord: %w", filePath, err)
|
||||
}
|
||||
pullRecords = append(pullRecords, pullRecord)
|
||||
return nil
|
||||
})
|
||||
|
||||
return pullRecords, err
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) WriteImagePulledRecord(pulledRecord *kubeletconfiginternal.ImagePulledRecord) error {
|
||||
recordBytes := bytes.NewBuffer([]byte{})
|
||||
if err := f.encoder.Encode(pulledRecord, recordBytes); err != nil {
|
||||
return fmt.Errorf("failed to serialize ImagePulledRecord: %w", err)
|
||||
}
|
||||
|
||||
return writeFile(f.pulledDir, cacheFilename(pulledRecord.ImageRef), recordBytes.Bytes())
|
||||
}
|
||||
|
||||
func (f *fsPullRecordsAccessor) DeleteImagePulledRecord(imageRef string) error {
|
||||
err := os.Remove(filepath.Join(f.pulledDir, cacheFilename(imageRef)))
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cacheFilename(image string) string {
|
||||
return fmt.Sprintf("%s%x", cacheFilesSHA256Prefix, sha256.Sum256([]byte(image)))
|
||||
}
|
||||
|
||||
// writeFile writes `content` to the file with name `filename` in directory `dir`.
|
||||
// It assures write atomicity by creating a temporary file first and only after
|
||||
// a successful write, it move the temp file in place of the target.
|
||||
func writeFile(dir, filename string, content []byte) error {
|
||||
// create target folder if it does not exists yet
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create directory %q: %w", dir, err)
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(dir, filename)
|
||||
tmpPath := targetPath + tmpFilesSuffix
|
||||
if err := os.WriteFile(tmpPath, content, 0600); err != nil {
|
||||
_ = os.Remove(tmpPath) // attempt a delete in case the file was at least partially written
|
||||
return fmt.Errorf("failed to create temporary file %q: %w", tmpPath, err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, targetPath); err != nil {
|
||||
_ = os.Remove(tmpPath) // attempt a cleanup
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processDirFiles reads files in a given directory and peforms `fileAction` action on those.
|
||||
func processDirFiles(dirName string, fileAction func(filePath string, fileContent []byte) error) error {
|
||||
var walkErrors []error
|
||||
err := filepath.WalkDir(dirName, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
walkErrors = append(walkErrors, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if path == dirName {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// skip files we didn't write or .tmp files
|
||||
if filename := d.Name(); !strings.HasPrefix(filename, cacheFilesSHA256Prefix) || strings.HasSuffix(filename, tmpFilesSuffix) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
walkErrors = append(walkErrors, fmt.Errorf("failed to read %q: %w", path, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fileAction(path, fileContent); err != nil {
|
||||
walkErrors = append(walkErrors, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
walkErrors = append(walkErrors, err)
|
||||
}
|
||||
|
||||
return errors.NewAggregate(walkErrors)
|
||||
}
|
||||
|
||||
// createKubeletCOnfigSchemeEncoderDecoder creates strict-encoding encoder and
|
||||
// decoder for the internal and alpha kubelet config APIs.
|
||||
func createKubeletConfigSchemeEncoderDecoder() (runtime.Encoder, runtime.Decoder, error) {
|
||||
const mediaType = runtime.ContentTypeJSON
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
if err := kubeletconfigvint1alpha1.AddToScheme(scheme); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := kubeletconfiginternal.AddToScheme(scheme); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// use the strict scheme to fail on unknown fields
|
||||
codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict)
|
||||
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("unable to locate encoder -- %q is not a supported media type", mediaType)
|
||||
}
|
||||
return codecs.EncoderForVersion(info.Serializer, kubeletconfigv1alpha1.SchemeGroupVersion), codecs.UniversalDecoder(), nil
|
||||
}
|
||||
|
||||
func decodeIntent(d runtime.Decoder, objBytes []byte) (*kubeletconfiginternal.ImagePullIntent, error) {
|
||||
obj, _, err := d.Decode(objBytes, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
intentObj, ok := obj.(*kubeletconfiginternal.ImagePullIntent)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert object to *ImagePullIntent: %T", obj)
|
||||
}
|
||||
|
||||
return intentObj, nil
|
||||
}
|
||||
|
||||
func decodePulledRecord(d runtime.Decoder, objBytes []byte) (*kubeletconfiginternal.ImagePulledRecord, error) {
|
||||
obj, _, err := d.Decode(objBytes, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pulledRecord, ok := obj.(*kubeletconfiginternal.ImagePulledRecord)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert object to *ImagePulledRecord: %T", obj)
|
||||
}
|
||||
|
||||
return pulledRecord, nil
|
||||
}
|
||||
538
pkg/kubelet/images/pullmanager/image_pull_manager.go
Normal file
538
pkg/kubelet/images/pullmanager/image_pull_manager.go
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
/*
|
||||
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 pullmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/util/parsers"
|
||||
)
|
||||
|
||||
var _ ImagePullManager = &PullManager{}
|
||||
|
||||
// writeRecordWhileMatchingLimit is a limit at which we stop writing yet-uncached
|
||||
// records that we found when we were checking if an image pull must be attempted.
|
||||
// This is to prevent unbounded writes in cases of high namespace turnover.
|
||||
const writeRecordWhileMatchingLimit = 100
|
||||
|
||||
// PullManager is an implementation of the ImagePullManager. It
|
||||
// tracks images pulled by the kubelet by creating records about ongoing and
|
||||
// successful pulls.
|
||||
// It tracks the credentials used with each successful pull in order to be able
|
||||
// to distinguish tenants requesting access to an image that exists on the kubelet's
|
||||
// node.
|
||||
type PullManager struct {
|
||||
recordsAccessor PullRecordsAccessor
|
||||
|
||||
imagePolicyEnforcer ImagePullPolicyEnforcer
|
||||
|
||||
imageService kubecontainer.ImageService
|
||||
|
||||
intentAccessors *StripedLockSet // image -> sync.Mutex
|
||||
intentCounters *sync.Map // image -> number of current in-flight pulls
|
||||
|
||||
pulledAccessors *StripedLockSet // imageRef -> sync.Mutex
|
||||
}
|
||||
|
||||
func NewImagePullManager(ctx context.Context, recordsAccessor PullRecordsAccessor, imagePullPolicy ImagePullPolicyEnforcer, imageService kubecontainer.ImageService, lockStripesNum int32) (*PullManager, error) {
|
||||
m := &PullManager{
|
||||
recordsAccessor: recordsAccessor,
|
||||
|
||||
imagePolicyEnforcer: imagePullPolicy,
|
||||
|
||||
imageService: imageService,
|
||||
|
||||
intentAccessors: NewStripedLockSet(lockStripesNum),
|
||||
intentCounters: &sync.Map{},
|
||||
|
||||
pulledAccessors: NewStripedLockSet(lockStripesNum),
|
||||
}
|
||||
|
||||
m.initialize(ctx)
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (f *PullManager) RecordPullIntent(image string) error {
|
||||
f.intentAccessors.Lock(image)
|
||||
defer f.intentAccessors.Unlock(image)
|
||||
|
||||
if err := f.recordsAccessor.WriteImagePullIntent(image); err != nil {
|
||||
return fmt.Errorf("failed to record image pull intent: %w", err)
|
||||
}
|
||||
|
||||
f.incrementIntentCounterForImage(image)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *PullManager) RecordImagePulled(image, imageRef string, credentials *kubeletconfiginternal.ImagePullCredentials) {
|
||||
if err := f.writePulledRecordIfChanged(image, imageRef, credentials); err != nil {
|
||||
klog.ErrorS(err, "failed to write image pulled record", "imageRef", imageRef)
|
||||
return
|
||||
}
|
||||
|
||||
// Notice we don't decrement in case of record write error, which leaves dangling
|
||||
// imagePullIntents and refCount in the intentCounters map.
|
||||
// This is done so that the successfully pulled image is still considered as pulled by the kubelet.
|
||||
// The kubelet will attempt to turn the imagePullIntent into a pulled record again when
|
||||
// it's restarted.
|
||||
f.decrementImagePullIntent(image)
|
||||
}
|
||||
|
||||
// writePulledRecordIfChanged writes an ImagePulledRecord into the f.pulledDir directory.
|
||||
// `image` is an image from a container of a Pod object.
|
||||
// `imageRef` is a reference to the `image“ as used by the CRI.
|
||||
// `credentials` is a set of credentials that should be written to a new/merged into
|
||||
// an existing record.
|
||||
//
|
||||
// If `credentials` is nil, it marks a situation where an image was pulled under
|
||||
// unknown circumstances. We should record the image as tracked but no credentials
|
||||
// should be written in order to force credential verification when the image is
|
||||
// accessed the next time.
|
||||
func (f *PullManager) writePulledRecordIfChanged(image, imageRef string, credentials *kubeletconfiginternal.ImagePullCredentials) error {
|
||||
f.pulledAccessors.Lock(imageRef)
|
||||
defer f.pulledAccessors.Unlock(imageRef)
|
||||
|
||||
sanitizedImage, err := trimImageTagDigest(image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid image name %q: %w", image, err)
|
||||
}
|
||||
|
||||
pulledRecord, _, err := f.recordsAccessor.GetImagePulledRecord(imageRef)
|
||||
if err != nil {
|
||||
klog.InfoS("failed to retrieve an ImagePulledRecord", "image", image, "err", err)
|
||||
pulledRecord = nil
|
||||
}
|
||||
|
||||
var pulledRecordChanged bool
|
||||
if pulledRecord == nil {
|
||||
pulledRecordChanged = true
|
||||
pulledRecord = &kubeletconfiginternal.ImagePulledRecord{
|
||||
LastUpdatedTime: metav1.Time{Time: time.Now()},
|
||||
ImageRef: imageRef,
|
||||
CredentialMapping: make(map[string]kubeletconfiginternal.ImagePullCredentials),
|
||||
}
|
||||
// just the existence of the pulled record for a given imageRef is enough
|
||||
// for us to consider it kubelet-pulled. The kubelet should fail safe
|
||||
// if it does not find a credential record for the specific image, and it
|
||||
// must require credential validation
|
||||
if credentials != nil {
|
||||
pulledRecord.CredentialMapping[sanitizedImage] = *credentials
|
||||
}
|
||||
} else {
|
||||
pulledRecord, pulledRecordChanged = pulledRecordMergeNewCreds(pulledRecord, sanitizedImage, credentials)
|
||||
}
|
||||
|
||||
if !pulledRecordChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f.recordsAccessor.WriteImagePulledRecord(pulledRecord)
|
||||
}
|
||||
|
||||
func (f *PullManager) RecordImagePullFailed(image string) {
|
||||
f.decrementImagePullIntent(image)
|
||||
}
|
||||
|
||||
// decrementImagePullIntent decreses the number of how many times image pull
|
||||
// intent for a given `image` was requested, and removes the ImagePullIntent file
|
||||
// if the reference counter for the image reaches zero.
|
||||
func (f *PullManager) decrementImagePullIntent(image string) {
|
||||
f.intentAccessors.Lock(image)
|
||||
defer f.intentAccessors.Unlock(image)
|
||||
|
||||
if f.getIntentCounterForImage(image) <= 1 {
|
||||
if err := f.recordsAccessor.DeleteImagePullIntent(image); err != nil {
|
||||
klog.ErrorS(err, "failed to remove image pull intent", "image", image)
|
||||
return
|
||||
}
|
||||
// only delete the intent counter once the file was deleted to be consistent
|
||||
// with the records
|
||||
f.intentCounters.Delete(image)
|
||||
return
|
||||
}
|
||||
|
||||
f.decrementIntentCounterForImage(image)
|
||||
}
|
||||
|
||||
func (f *PullManager) MustAttemptImagePull(image, imageRef string, podSecrets []kubeletconfiginternal.ImagePullSecret) bool {
|
||||
if len(imageRef) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
var imagePulledByKubelet bool
|
||||
var pulledRecord *kubeletconfiginternal.ImagePulledRecord
|
||||
|
||||
err := func() error {
|
||||
// don't allow changes to the files we're using for our decision
|
||||
f.pulledAccessors.Lock(imageRef)
|
||||
defer f.pulledAccessors.Unlock(imageRef)
|
||||
f.intentAccessors.Lock(image)
|
||||
defer f.intentAccessors.Unlock(image)
|
||||
|
||||
var err error
|
||||
var exists bool
|
||||
pulledRecord, exists, err = f.recordsAccessor.GetImagePulledRecord(imageRef)
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case exists:
|
||||
imagePulledByKubelet = true
|
||||
case pulledRecord != nil:
|
||||
imagePulledByKubelet = true
|
||||
default:
|
||||
// optimized check - we can check the intent number, however, if it's zero
|
||||
// it may only mean kubelet restarted since writing the intent record and
|
||||
// we must fall back to the actual cache
|
||||
imagePulledByKubelet = f.getIntentCounterForImage(image) > 0
|
||||
if imagePulledByKubelet {
|
||||
break
|
||||
}
|
||||
|
||||
if exists, err := f.recordsAccessor.ImagePullIntentExists(image); err != nil {
|
||||
return fmt.Errorf("failed to check existence of an image pull intent: %w", err)
|
||||
} else if exists {
|
||||
imagePulledByKubelet = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Unable to access cache records about image pulls")
|
||||
return true
|
||||
}
|
||||
|
||||
if !f.imagePolicyEnforcer.RequireCredentialVerificationForImage(image, imagePulledByKubelet) {
|
||||
return false
|
||||
}
|
||||
|
||||
if pulledRecord == nil {
|
||||
// we have no proper records of the image being pulled in the past, we can short-circuit here
|
||||
return true
|
||||
}
|
||||
|
||||
sanitizedImage, err := trimImageTagDigest(image)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to parse image name, forcing image credentials reverification", "image", sanitizedImage)
|
||||
return true
|
||||
}
|
||||
|
||||
cachedCreds, ok := pulledRecord.CredentialMapping[sanitizedImage]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if cachedCreds.NodePodsAccessible {
|
||||
// anyone on this node can access the image
|
||||
return false
|
||||
}
|
||||
|
||||
if len(cachedCreds.KubernetesSecrets) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, podSecret := range podSecrets {
|
||||
for _, cachedSecret := range cachedCreds.KubernetesSecrets {
|
||||
|
||||
// we need to check hash len in case hashing failed while storing the record in the keyring
|
||||
hashesMatch := len(cachedSecret.CredentialHash) > 0 && podSecret.CredentialHash == cachedSecret.CredentialHash
|
||||
secretCoordinatesMatch := podSecret.UID == cachedSecret.UID &&
|
||||
podSecret.Namespace == cachedSecret.Namespace &&
|
||||
podSecret.Name == cachedSecret.Name
|
||||
|
||||
if hashesMatch {
|
||||
if !secretCoordinatesMatch && len(cachedCreds.KubernetesSecrets) < writeRecordWhileMatchingLimit {
|
||||
// While we're only matching at this point, we want to ensure this secret is considered valid in the future
|
||||
// and so we make an additional write to the cache.
|
||||
// writePulledRecord() is a noop in case the secret with the updated hash already appears in the cache.
|
||||
if err := f.writePulledRecordIfChanged(image, imageRef, &kubeletconfiginternal.ImagePullCredentials{KubernetesSecrets: []kubeletconfiginternal.ImagePullSecret{podSecret}}); err != nil {
|
||||
klog.ErrorS(err, "failed to write an image pulled record", "image", image, "imageRef", imageRef)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if secretCoordinatesMatch {
|
||||
if !hashesMatch && len(cachedCreds.KubernetesSecrets) < writeRecordWhileMatchingLimit {
|
||||
// While we're only matching at this point, we want to ensure the updated credentials are considered valid in the future
|
||||
// and so we make an additional write to the cache.
|
||||
// writePulledRecord() is a noop in case the hash got updated in the meantime.
|
||||
if err := f.writePulledRecordIfChanged(image, imageRef, &kubeletconfiginternal.ImagePullCredentials{KubernetesSecrets: []kubeletconfiginternal.ImagePullSecret{podSecret}}); err != nil {
|
||||
klog.ErrorS(err, "failed to write an image pulled record", "image", image, "imageRef", imageRef)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *PullManager) PruneUnknownRecords(imageList []string, until time.Time) {
|
||||
f.pulledAccessors.GlobalLock()
|
||||
defer f.pulledAccessors.GlobalUnlock()
|
||||
|
||||
pulledRecords, err := f.recordsAccessor.ListImagePulledRecords()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "there were errors listing ImagePulledRecords, garbage collection will proceed with incomplete records list")
|
||||
}
|
||||
|
||||
imagesInUse := sets.New(imageList...)
|
||||
for _, imageRecord := range pulledRecords {
|
||||
if !imageRecord.LastUpdatedTime.Time.Before(until) {
|
||||
// the image record was only updated after the GC started
|
||||
continue
|
||||
}
|
||||
|
||||
if imagesInUse.Has(imageRecord.ImageRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := f.recordsAccessor.DeleteImagePulledRecord(imageRecord.ImageRef); err != nil {
|
||||
klog.ErrorS(err, "failed to remove an ImagePulledRecord", "imageRef", imageRecord.ImageRef)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// initialize gathers all the images from pull intent records that exist
|
||||
// from the previous kubelet runs.
|
||||
// If the CRI reports any of the above images as already pulled, we turn the
|
||||
// pull intent into a pulled record and the original pull intent is deleted.
|
||||
//
|
||||
// This method is not thread-safe and it should only be called upon the creation
|
||||
// of the PullManager.
|
||||
func (f *PullManager) initialize(ctx context.Context) {
|
||||
pullIntents, err := f.recordsAccessor.ListImagePullIntents()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "there were errors listing ImagePullIntents, continuing with an incomplete records list")
|
||||
}
|
||||
|
||||
if len(pullIntents) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
imageObjs, err := f.imageService.ListImages(ctx)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to list images")
|
||||
}
|
||||
|
||||
inFlightPulls := sets.New[string]()
|
||||
for _, intent := range pullIntents {
|
||||
inFlightPulls.Insert(intent.Image)
|
||||
}
|
||||
|
||||
// Each of the images known to the CRI might consist of multiple tags and digests,
|
||||
// which is what we track in the ImagePullIntent - we need to go through all of these
|
||||
// for each image.
|
||||
for _, imageObj := range imageObjs {
|
||||
existingRecordedImages := searchForExistingTagDigest(inFlightPulls, imageObj)
|
||||
|
||||
for _, image := range existingRecordedImages.UnsortedList() {
|
||||
|
||||
if err := f.writePulledRecordIfChanged(image, imageObj.ID, nil); err != nil {
|
||||
klog.ErrorS(err, "failed to write an image pull record", "imageRef", imageObj.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := f.recordsAccessor.DeleteImagePullIntent(image); err != nil {
|
||||
klog.V(2).InfoS("failed to remove image pull intent file", "imageName", image, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (f *PullManager) incrementIntentCounterForImage(image string) {
|
||||
f.intentCounters.Store(image, f.getIntentCounterForImage(image)+1)
|
||||
}
|
||||
func (f *PullManager) decrementIntentCounterForImage(image string) {
|
||||
f.intentCounters.Store(image, f.getIntentCounterForImage(image)-1)
|
||||
}
|
||||
|
||||
func (f *PullManager) getIntentCounterForImage(image string) int32 {
|
||||
intentNumAny, ok := f.intentCounters.Load(image)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
intentNum, ok := intentNumAny.(int32)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("expected the intentCounters sync map to only contain int32 values, got %T", intentNumAny))
|
||||
}
|
||||
return intentNum
|
||||
}
|
||||
|
||||
// searchForExistingTagDigest loops through the `image` RepoDigests and RepoTags
|
||||
// and tries to find all image digests/tags in `inFlightPulls`, which is a map of
|
||||
// containerImage -> pulling intent path.
|
||||
func searchForExistingTagDigest(inFlightPulls sets.Set[string], image kubecontainer.Image) sets.Set[string] {
|
||||
existingRecordedImages := sets.New[string]()
|
||||
for _, digest := range image.RepoDigests {
|
||||
if ok := inFlightPulls.Has(digest); ok {
|
||||
existingRecordedImages.Insert(digest)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tag := range image.RepoTags {
|
||||
if ok := inFlightPulls.Has(tag); ok {
|
||||
existingRecordedImages.Insert(tag)
|
||||
}
|
||||
}
|
||||
|
||||
return existingRecordedImages
|
||||
}
|
||||
|
||||
type kubeSecretCoordinates struct {
|
||||
UID string
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
// pulledRecordMergeNewCreds merges the credentials from `newCreds` into the `orig`
|
||||
// record for the `imageNoTagDigest` image.
|
||||
// `imageNoTagDigest` is the content of the `image` field from a pod's container
|
||||
// after any tag or digest were removed from it.
|
||||
//
|
||||
// NOTE: pulledRecordMergeNewCreds() may be often called in the read path of
|
||||
// PullManager.MustAttemptImagePul() and so it's desirable to limit allocations
|
||||
// (e.g. DeepCopy()) until it is necessary.
|
||||
func pulledRecordMergeNewCreds(orig *kubeletconfiginternal.ImagePulledRecord, imageNoTagDigest string, newCreds *kubeletconfiginternal.ImagePullCredentials) (*kubeletconfiginternal.ImagePulledRecord, bool) {
|
||||
if newCreds == nil {
|
||||
// no new credential information to record
|
||||
return orig, false
|
||||
}
|
||||
|
||||
if !newCreds.NodePodsAccessible && len(newCreds.KubernetesSecrets) == 0 {
|
||||
// we don't have any secret credentials or node-wide access to record
|
||||
// TODO(stlaz,aramase): add in a serviceaccount dimension check
|
||||
return orig, false
|
||||
}
|
||||
selectedCreds, found := orig.CredentialMapping[imageNoTagDigest]
|
||||
if !found {
|
||||
ret := orig.DeepCopy()
|
||||
if ret.CredentialMapping == nil {
|
||||
ret.CredentialMapping = make(map[string]kubeletconfiginternal.ImagePullCredentials)
|
||||
}
|
||||
ret.CredentialMapping[imageNoTagDigest] = *newCreds
|
||||
ret.LastUpdatedTime = metav1.Time{Time: time.Now()}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
if selectedCreds.NodePodsAccessible {
|
||||
return orig, false
|
||||
}
|
||||
|
||||
if newCreds.NodePodsAccessible {
|
||||
selectedCreds.NodePodsAccessible = true
|
||||
selectedCreds.KubernetesSecrets = nil
|
||||
|
||||
ret := orig.DeepCopy()
|
||||
ret.CredentialMapping[imageNoTagDigest] = selectedCreds
|
||||
ret.LastUpdatedTime = metav1.Time{Time: time.Now()}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
var secretsChanged bool
|
||||
selectedCreds.KubernetesSecrets, secretsChanged = mergePullSecrets(selectedCreds.KubernetesSecrets, newCreds.KubernetesSecrets)
|
||||
if !secretsChanged {
|
||||
return orig, false
|
||||
}
|
||||
|
||||
ret := orig.DeepCopy()
|
||||
ret.CredentialMapping[imageNoTagDigest] = selectedCreds
|
||||
ret.LastUpdatedTime = metav1.Time{Time: time.Now()}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// mergePullSecrets merges two slices of ImagePullSecret object into one while
|
||||
// keeping the objects unique per `Namespace, Name, UID` key.
|
||||
//
|
||||
// In case an object from the `new` slice has the same `Namespace, Name, UID` combination
|
||||
// as an object from `orig`, the result will use the CredentialHash value of the
|
||||
// object from `new`.
|
||||
//
|
||||
// The returned slice is sorted by Namespace, Name and UID (in this order). Also
|
||||
// returns an indicator whether the set of input secrets chaged.
|
||||
func mergePullSecrets(orig, new []kubeletconfiginternal.ImagePullSecret) ([]kubeletconfiginternal.ImagePullSecret, bool) {
|
||||
credSet := make(map[kubeSecretCoordinates]string)
|
||||
for _, secret := range orig {
|
||||
credSet[kubeSecretCoordinates{
|
||||
UID: secret.UID,
|
||||
Namespace: secret.Namespace,
|
||||
Name: secret.Name,
|
||||
}] = secret.CredentialHash
|
||||
}
|
||||
|
||||
changed := false
|
||||
for _, s := range new {
|
||||
key := kubeSecretCoordinates{UID: s.UID, Namespace: s.Namespace, Name: s.Name}
|
||||
if existingHash, ok := credSet[key]; !ok || existingHash != s.CredentialHash {
|
||||
changed = true
|
||||
credSet[key] = s.CredentialHash
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return orig, false
|
||||
}
|
||||
|
||||
ret := make([]kubeletconfiginternal.ImagePullSecret, 0, len(credSet))
|
||||
for coords, hash := range credSet {
|
||||
ret = append(ret, kubeletconfiginternal.ImagePullSecret{
|
||||
UID: coords.UID,
|
||||
Namespace: coords.Namespace,
|
||||
Name: coords.Name,
|
||||
CredentialHash: hash,
|
||||
})
|
||||
}
|
||||
// we don't need to use the stable version because secret coordinates used for ordering are unique in the set
|
||||
slices.SortFunc(ret, imagePullSecretLess)
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// imagePullSecretLess is a helper function to define ordering in a slice of
|
||||
// ImagePullSecret objects.
|
||||
func imagePullSecretLess(a, b kubeletconfiginternal.ImagePullSecret) int {
|
||||
if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
if cmp := strings.Compare(a.Name, b.Name); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
return strings.Compare(a.UID, b.UID)
|
||||
}
|
||||
|
||||
// trimImageTagDigest removes the tag and digest from an image name
|
||||
func trimImageTagDigest(containerImage string) (string, error) {
|
||||
imageName, _, _, err := parsers.ParseImageName(containerImage)
|
||||
return imageName, err
|
||||
}
|
||||
1053
pkg/kubelet/images/pullmanager/image_pull_manager_test.go
Normal file
1053
pkg/kubelet/images/pullmanager/image_pull_manager_test.go
Normal file
File diff suppressed because it is too large
Load diff
171
pkg/kubelet/images/pullmanager/image_pull_policies.go
Normal file
171
pkg/kubelet/images/pullmanager/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 pullmanager
|
||||
|
||||
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/pullmanager/image_pull_policies_test.go
Normal file
188
pkg/kubelet/images/pullmanager/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 pullmanager
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
110
pkg/kubelet/images/pullmanager/interfaces.go
Normal file
110
pkg/kubelet/images/pullmanager/interfaces.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
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 pullmanager
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
// ImagePullManager keeps the state of images that were pulled and which are
|
||||
// currently still being pulled.
|
||||
// It should keep an internal state of images currently being pulled by the kubelet
|
||||
// in order to determine whether to destroy a "pulling" record should an image
|
||||
// pull fail.
|
||||
type ImagePullManager interface {
|
||||
// RecordPullIntent records an intent to pull an image and should be called
|
||||
// before a pull of the image occurs.
|
||||
//
|
||||
// RecordPullIntent() should be called before every image pull. Each call of
|
||||
// RecordPullIntent() must match exactly one call of RecordImagePulled()/RecordImagePullFailed().
|
||||
//
|
||||
// `image` is the content of the pod's container `image` field.
|
||||
RecordPullIntent(image string) error
|
||||
// RecordImagePulled writes a record of an image being successfully pulled
|
||||
// with ImagePullCredentials.
|
||||
//
|
||||
// `credentials` must not be nil and must contain either exactly one Kubernetes
|
||||
// Secret coordinates in the `.KubernetesSecrets` slice or set `.NodePodsAccessible`
|
||||
// to `true`.
|
||||
//
|
||||
// `image` is the content of the pod's container `image` field.
|
||||
RecordImagePulled(image, imageRef string, credentials *kubeletconfiginternal.ImagePullCredentials)
|
||||
// RecordImagePullFailed should be called if an image failed to pull.
|
||||
//
|
||||
// Internally, it lowers its reference counter for the given image. If the
|
||||
// counter reaches zero, the pull intent record for the image is removed.
|
||||
//
|
||||
// `image` is the content of the pod's container `image` field.
|
||||
RecordImagePullFailed(image string)
|
||||
// MustAttemptImagePull evaluates the policy for the image specified in
|
||||
// `image` and if the policy demands verification, it checks the internal
|
||||
// cache to see if there's a record of pulling the image with the presented
|
||||
// set of credentials or if the image can be accessed by any of the node's pods.
|
||||
//
|
||||
// Returns true if the policy demands verification and no record of the pull
|
||||
// was found in the cache.
|
||||
//
|
||||
// `image` is the content of the pod's container `image` field.
|
||||
MustAttemptImagePull(image, imageRef string, credentials []kubeletconfiginternal.ImagePullSecret) bool
|
||||
// PruneUnknownRecords deletes all of the cache ImagePulledRecords for each of the images
|
||||
// whose imageRef does not appear in the `imageList` iff such an record was last updated
|
||||
// _before_ the `until` timestamp.
|
||||
//
|
||||
// This method is only expected to be called by the kubelet's image garbage collector.
|
||||
// `until` is a timestamp created _before_ the `imageList` was requested from the CRI.
|
||||
PruneUnknownRecords(imageList []string, until time.Time)
|
||||
}
|
||||
|
||||
// PullRecordsAccessor allows unified access to ImagePullIntents/ImagePulledRecords
|
||||
// irregardless of the backing database implementation
|
||||
type PullRecordsAccessor interface {
|
||||
// ListImagePullIntents lists all the ImagePullIntents in the database.
|
||||
// ImagePullIntents that cannot be decoded will not appear in the list.
|
||||
// Returns nil and an error if there was a problem reading from the database.
|
||||
//
|
||||
// This method may return partial success in case there were errors listing
|
||||
// the results. A list of records that were successfully read and an aggregated
|
||||
// error is returned in that case.
|
||||
ListImagePullIntents() ([]*kubeletconfiginternal.ImagePullIntent, error)
|
||||
// ImagePullIntentExists returns whether a valid ImagePullIntent is present
|
||||
// for the given image.
|
||||
ImagePullIntentExists(image string) (bool, error)
|
||||
// WriteImagePullIntent writes a an intent record for the image into the database
|
||||
WriteImagePullIntent(image string) error
|
||||
// DeleteImagePullIntent removes an `image` intent record from the database
|
||||
DeleteImagePullIntent(image string) error
|
||||
|
||||
// ListImagePulledRecords lists the database ImagePulledRecords.
|
||||
// Records that cannot be decoded will be ignored.
|
||||
// Returns an error if there was a problem reading from the database.
|
||||
//
|
||||
// This method may return partial success in case there were errors listing
|
||||
// the results. A list of records that were successfully read and an aggregated
|
||||
// error is returned in that case.
|
||||
ListImagePulledRecords() ([]*kubeletconfiginternal.ImagePulledRecord, error)
|
||||
// GetImagePulledRecord fetches an ImagePulledRecord for the given `imageRef`.
|
||||
// If a file for the `imageRef` is present but the contents cannot be decoded,
|
||||
// it returns a exists=true with err equal to the decoding error.
|
||||
GetImagePulledRecord(imageRef string) (record *kubeletconfiginternal.ImagePulledRecord, exists bool, err error)
|
||||
// WriteImagePulledRecord writes an ImagePulledRecord into the database.
|
||||
WriteImagePulledRecord(record *kubeletconfiginternal.ImagePulledRecord) error
|
||||
// DeleteImagePulledRecord removes an ImagePulledRecord for `imageRef` from the
|
||||
// database.
|
||||
DeleteImagePulledRecord(imageRef string) error
|
||||
}
|
||||
67
pkg/kubelet/images/pullmanager/locks.go
Normal file
67
pkg/kubelet/images/pullmanager/locks.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
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 pullmanager
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// StripedLockSet allows context locking based on string keys, where each key
|
||||
// is mapped to a an index in a size-limited slice of locks.
|
||||
type StripedLockSet struct {
|
||||
locks []sync.Mutex
|
||||
size int32
|
||||
}
|
||||
|
||||
// NewStripedLockSet creates a StripedLockSet with `size` number of locks to be
|
||||
// used for locking context based on string keys.
|
||||
// The size will be normalized to stay in the <1, 31> interval.
|
||||
func NewStripedLockSet(size int32) *StripedLockSet {
|
||||
size = max(size, 1) // make sure we're at least at size 1
|
||||
|
||||
return &StripedLockSet{
|
||||
locks: make([]sync.Mutex, min(31, size)),
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StripedLockSet) Lock(key string) {
|
||||
s.locks[keyToID(key, s.size)].Lock()
|
||||
}
|
||||
|
||||
func (s *StripedLockSet) Unlock(key string) {
|
||||
s.locks[keyToID(key, s.size)].Unlock()
|
||||
}
|
||||
|
||||
func (s *StripedLockSet) GlobalLock() {
|
||||
for i := range s.locks {
|
||||
s.locks[i].Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StripedLockSet) GlobalUnlock() {
|
||||
for i := range s.locks {
|
||||
s.locks[i].Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func keyToID(key string, sliceSize int32) uint32 {
|
||||
h := fnv.New32()
|
||||
h.Write([]byte(key))
|
||||
return h.Sum32() % uint32(sliceSize)
|
||||
}
|
||||
36
pkg/kubelet/images/pullmanager/noop_pull_manager.go
Normal file
36
pkg/kubelet/images/pullmanager/noop_pull_manager.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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 pullmanager
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
var _ ImagePullManager = &NoopImagePullManager{}
|
||||
|
||||
type NoopImagePullManager struct{}
|
||||
|
||||
func (m *NoopImagePullManager) RecordPullIntent(_ string) error { return nil }
|
||||
func (m *NoopImagePullManager) RecordImagePulled(_, _ string, _ *kubeletconfiginternal.ImagePullCredentials) {
|
||||
}
|
||||
func (m *NoopImagePullManager) RecordImagePullFailed(image string) {}
|
||||
func (m *NoopImagePullManager) MustAttemptImagePull(_, _ string, _ []kubeletconfiginternal.ImagePullSecret) bool {
|
||||
return false
|
||||
}
|
||||
func (m *NoopImagePullManager) PruneUnknownRecords(_ []string, _ time.Time) {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePulledRecord","apiVersion":"kubelet.config.k8s.io/v1alpha1","lastUpdatedTime":"2024-10-21T12:26:40Z","imageRef":"test-brokenhash","credentialMapping":{"docker.io/testing/test":{"kubernetesSecrets":[{"uid":"testsecretuid","namespace":"default","name":"pull-secret","credentialHash":""}]}}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePulledRecord","apiVersion":"kubelet.config.k8s.io/v1alpha1","lastUpdatedTime":"2024-10-21T12:26:40Z","imageRef":"testimage-anonpull","credentialMapping":{"docker.io/testing/test":{"nodePodsAccessible":true}}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePulledRecord","apiVersion":"kubelet.config.k8s.io/v1alpha1","lastUpdatedTime":"2024-10-21T12:26:40Z","imageRef":"testimageref","credentialMapping":{"docker.io/testing/test":{"kubernetesSecrets":[{"uid":"testsecretuid","namespace":"default","name":"pull-secret","credentialHash":"testsecrethash"}]}}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePulledRecord","apiVersion":"kubelet.config.k8s.io/v1alpha1","lastUpdatedTime":"2024-10-21T12:26:40Z","imageRef":"testemptycredmapping"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePullIntent","apiVersion":"kubelet.config.k8s.io/v1alpha1","image":"docker.io/testing/test:latest"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePullIntent","apiVersion":"kubelet.config.k8s.io/v1alpha1","image":"repo.repo/test/test:v1"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kind":"ImagePullIntent","apiVersion":"kubelet.config.k8s.io/v1alpha1","image":"docker.io/testing/test:something"}
|
||||
|
|
@ -759,7 +759,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
|||
}
|
||||
}
|
||||
|
||||
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
|
||||
runtime, postImageGCHooks, err := kuberuntime.NewKubeGenericRuntimeManager(
|
||||
kubecontainer.FilterEventRecorder(kubeDeps.Recorder),
|
||||
klet.livenessManager,
|
||||
klet.readinessManager,
|
||||
|
|
@ -776,6 +776,8 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
|||
kubeCfg.MaxParallelImagePulls,
|
||||
float32(kubeCfg.RegistryPullQPS),
|
||||
int(kubeCfg.RegistryBurst),
|
||||
kubeCfg.ImagePullCredentialsVerificationPolicy,
|
||||
kubeCfg.PreloadedImagesVerificationAllowlist,
|
||||
imageCredentialProviderConfigFile,
|
||||
imageCredentialProviderBinDir,
|
||||
singleProcessOOMKill,
|
||||
|
|
@ -881,7 +883,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
|||
klet.containerDeletor = newPodContainerDeletor(klet.containerRuntime, max(containerGCPolicy.MaxPerPodContainer, minDeadContainerInPod))
|
||||
|
||||
// setup imageManager
|
||||
imageManager, err := images.NewImageGCManager(klet.containerRuntime, klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy, kubeDeps.TracerProvider)
|
||||
imageManager, err := images.NewImageGCManager(klet.containerRuntime, klet.StatsProvider, postImageGCHooks, kubeDeps.Recorder, nodeRef, imageGCPolicy, kubeDeps.TracerProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize image manager: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ func newTestKubeletWithImageList(
|
|||
HighThresholdPercent: 90,
|
||||
LowThresholdPercent: 80,
|
||||
}
|
||||
imageGCManager, err := images.NewImageGCManager(fakeRuntime, kubelet.StatsProvider, fakeRecorder, fakeNodeRef, fakeImageGCPolicy, noopoteltrace.NewTracerProvider())
|
||||
imageGCManager, err := images.NewImageGCManager(fakeRuntime, kubelet.StatsProvider, nil, fakeRecorder, fakeNodeRef, fakeImageGCPolicy, noopoteltrace.NewTracerProvider())
|
||||
assert.NoError(t, err)
|
||||
kubelet.imageManager = &fakeImageGCManager{
|
||||
fakeImageService: fakeRuntime,
|
||||
|
|
@ -3394,7 +3394,7 @@ func TestSyncPodSpans(t *testing.T) {
|
|||
imageSvc, err := remote.NewRemoteImageService(endpoint, 15*time.Second, tp, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
kubelet.containerRuntime, err = kuberuntime.NewKubeGenericRuntimeManager(
|
||||
kubelet.containerRuntime, _, err = kuberuntime.NewKubeGenericRuntimeManager(
|
||||
kubelet.recorder,
|
||||
kubelet.livenessManager,
|
||||
kubelet.readinessManager,
|
||||
|
|
@ -3411,6 +3411,8 @@ func TestSyncPodSpans(t *testing.T) {
|
|||
kubeCfg.MaxParallelImagePulls,
|
||||
float32(kubeCfg.RegistryPullQPS),
|
||||
int(kubeCfg.RegistryBurst),
|
||||
string(kubeletconfiginternal.NeverVerify),
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
nil,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/images"
|
||||
imagepullmanager "k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||
"k8s.io/kubernetes/pkg/kubelet/logs"
|
||||
proberesults "k8s.io/kubernetes/pkg/kubelet/prober/results"
|
||||
|
|
@ -94,7 +95,7 @@ func (f *fakePodPullingTimeRecorder) RecordImageStartedPulling(podUID types.UID)
|
|||
|
||||
func (f *fakePodPullingTimeRecorder) RecordImageFinishedPulling(podUID types.UID) {}
|
||||
|
||||
func newFakeKubeRuntimeManager(runtimeService internalapi.RuntimeService, imageService internalapi.ImageManagerService, machineInfo *cadvisorapi.MachineInfo, osInterface kubecontainer.OSInterface, runtimeHelper kubecontainer.RuntimeHelper, keyring credentialprovider.DockerKeyring, tracer trace.Tracer) (*kubeGenericRuntimeManager, error) {
|
||||
func newFakeKubeRuntimeManager(runtimeService internalapi.RuntimeService, imageService internalapi.ImageManagerService, machineInfo *cadvisorapi.MachineInfo, osInterface kubecontainer.OSInterface, runtimeHelper kubecontainer.RuntimeHelper, tracer trace.Tracer) (*kubeGenericRuntimeManager, error) {
|
||||
ctx := context.Background()
|
||||
recorder := &record.FakeRecorder{}
|
||||
logManager, err := logs.NewContainerLogManager(runtimeService, osInterface, "1", 2, 10, metav1.Duration{Duration: 10 * time.Second})
|
||||
|
|
@ -113,7 +114,6 @@ func newFakeKubeRuntimeManager(runtimeService internalapi.RuntimeService, imageS
|
|||
runtimeHelper: runtimeHelper,
|
||||
runtimeService: runtimeService,
|
||||
imageService: imageService,
|
||||
keyring: keyring,
|
||||
seccompProfileRoot: fakeSeccompProfileRoot,
|
||||
internalLifecycle: cm.NewFakeInternalContainerLifecycle(),
|
||||
logReduction: logreduction.NewLogReduction(identicalErrorDelay),
|
||||
|
|
@ -134,7 +134,9 @@ func newFakeKubeRuntimeManager(runtimeService internalapi.RuntimeService, imageS
|
|||
kubeRuntimeManager.runtimeName = typedVersion.RuntimeName
|
||||
kubeRuntimeManager.imagePuller = images.NewImageManager(
|
||||
kubecontainer.FilterEventRecorder(recorder),
|
||||
&credentialprovider.BasicDockerKeyring{},
|
||||
kubeRuntimeManager,
|
||||
&imagepullmanager.NoopImagePullManager{},
|
||||
flowcontrol.NewBackOff(time.Second, 300*time.Second),
|
||||
false,
|
||||
ptr.To[int32](0), // No limit on max parallel image pulls,
|
||||
|
|
|
|||
|
|
@ -19,62 +19,35 @@ package kuberuntime
|
|||
import (
|
||||
"context"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
credentialproviderplugin "k8s.io/kubernetes/pkg/credentialprovider/plugin"
|
||||
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
|
||||
crededentialprovider "k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/util/parsers"
|
||||
)
|
||||
|
||||
// PullImage pulls an image from the network to local storage using the supplied
|
||||
// secrets if necessary.
|
||||
func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context, image kubecontainer.ImageSpec, credentials []crededentialprovider.TrackedAuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, *crededentialprovider.TrackedAuthConfig, error) {
|
||||
img := image.Image
|
||||
repoToPull, _, _, err := parsers.ParseImageName(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// construct the dynamic keyring using the providers we have in the kubelet
|
||||
var podName, podNamespace, podUID string
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
sandboxMetadata := podSandboxConfig.GetMetadata()
|
||||
|
||||
podName = sandboxMetadata.Name
|
||||
podNamespace = sandboxMetadata.Namespace
|
||||
podUID = sandboxMetadata.Uid
|
||||
}
|
||||
|
||||
externalCredentialProviderKeyring := credentialproviderplugin.NewExternalCredentialProviderDockerKeyring(podNamespace, podName, podUID, serviceAccountName)
|
||||
|
||||
keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, credentialprovider.UnionDockerKeyring{m.keyring, externalCredentialProviderKeyring})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imgSpec := toRuntimeAPIImageSpec(image)
|
||||
|
||||
creds, withCredentials := keyring.Lookup(repoToPull)
|
||||
if !withCredentials {
|
||||
if len(credentials) == 0 {
|
||||
klog.V(3).InfoS("Pulling image without credentials", "image", img)
|
||||
|
||||
imageRef, err := m.imageService.PullImage(ctx, imgSpec, nil, podSandboxConfig)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Failed to pull image", "image", img)
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return imageRef, nil
|
||||
return imageRef, nil, nil
|
||||
}
|
||||
|
||||
var pullErrs []error
|
||||
for _, currentCreds := range creds {
|
||||
for _, currentCreds := range credentials {
|
||||
auth := &runtimeapi.AuthConfig{
|
||||
Username: currentCreds.Username,
|
||||
Password: currentCreds.Password,
|
||||
|
|
@ -87,13 +60,13 @@ func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context, image kubecon
|
|||
imageRef, err := m.imageService.PullImage(ctx, imgSpec, auth, podSandboxConfig)
|
||||
// If there was no error, return success
|
||||
if err == nil {
|
||||
return imageRef, nil
|
||||
return imageRef, ¤tCreds, nil
|
||||
}
|
||||
|
||||
pullErrs = append(pullErrs, err)
|
||||
}
|
||||
|
||||
return "", utilerrors.NewAggregate(pullErrs)
|
||||
return "", nil, utilerrors.NewAggregate(pullErrs)
|
||||
}
|
||||
|
||||
// GetImageRef gets the ID of the image which has already been in
|
||||
|
|
|
|||
|
|
@ -21,15 +21,20 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/images"
|
||||
imagepullmanager "k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestPullImage(t *testing.T) {
|
||||
|
|
@ -37,9 +42,10 @@ func TestPullImage(t *testing.T) {
|
|||
_, _, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
imageRef, creds, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "busybox", imageRef)
|
||||
assert.Nil(t, creds) // as this was an anonymous pull
|
||||
|
||||
images, err := fakeManager.ListImages(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -52,36 +58,17 @@ func TestPullImageWithError(t *testing.T) {
|
|||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// trying to pull an image with an invalid name should return an error
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: ":invalid"}, nil, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
fakeImageService.InjectError("PullImage", fmt.Errorf("test-error"))
|
||||
imageRef, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
imageRef, creds, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
assert.Nil(t, creds)
|
||||
|
||||
images, err := fakeManager.ListImages(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, images)
|
||||
}
|
||||
|
||||
func TestPullImageWithInvalidImageName(t *testing.T) {
|
||||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
imageList := []string{"FAIL", "http://fail", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}
|
||||
fakeImageService.SetFakeImages(imageList)
|
||||
for _, val := range imageList {
|
||||
ctx := context.Background()
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: val}, nil, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImages(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
|
|
@ -196,7 +183,7 @@ func TestRemoveImage(t *testing.T) {
|
|||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
_, _, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fakeImageService.Images, 1)
|
||||
|
||||
|
|
@ -219,7 +206,7 @@ func TestRemoveImageWithError(t *testing.T) {
|
|||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
_, _, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fakeImageService.Images, 1)
|
||||
|
||||
|
|
@ -280,13 +267,13 @@ func TestPullWithSecrets(t *testing.T) {
|
|||
expectedAuth *runtimeapi.AuthConfig
|
||||
}{
|
||||
"no matching secrets": {
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
[]v1.Secret{},
|
||||
credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{}),
|
||||
nil,
|
||||
},
|
||||
"default keyring secrets": {
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
[]v1.Secret{},
|
||||
credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{
|
||||
"index.docker.io/v1/": {Username: "built-in", Password: "password", Provider: nil},
|
||||
|
|
@ -294,7 +281,7 @@ func TestPullWithSecrets(t *testing.T) {
|
|||
&runtimeapi.AuthConfig{Username: "built-in", Password: "password"},
|
||||
},
|
||||
"default keyring secrets unused": {
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
[]v1.Secret{},
|
||||
credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{
|
||||
"extraneous": {Username: "built-in", Password: "password", Provider: nil},
|
||||
|
|
@ -302,7 +289,7 @@ func TestPullWithSecrets(t *testing.T) {
|
|||
nil,
|
||||
},
|
||||
"builtin keyring secrets, but use passed": {
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
[]v1.Secret{{Type: v1.SecretTypeDockercfg, Data: map[string][]byte{v1.DockerConfigKey: dockercfgContent}}},
|
||||
credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{
|
||||
"index.docker.io/v1/": {Username: "built-in", Password: "password", Provider: nil},
|
||||
|
|
@ -310,7 +297,7 @@ func TestPullWithSecrets(t *testing.T) {
|
|||
&runtimeapi.AuthConfig{Username: "passed-user", Password: "passed-password"},
|
||||
},
|
||||
"builtin keyring secrets, but use passed with new docker config": {
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
[]v1.Secret{{Type: v1.SecretTypeDockerConfigJson, Data: map[string][]byte{v1.DockerConfigJsonKey: dockerConfigJSONContent}}},
|
||||
credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{
|
||||
"index.docker.io/v1/": {Username: "built-in", Password: "password", Provider: nil},
|
||||
|
|
@ -320,11 +307,35 @@ func TestPullWithSecrets(t *testing.T) {
|
|||
}
|
||||
for description, test := range tests {
|
||||
builtInKeyRing := &credentialprovider.BasicDockerKeyring{}
|
||||
builtInKeyRing.Add(test.builtInDockerConfig)
|
||||
_, fakeImageService, fakeManager, err := customTestRuntimeManager(builtInKeyRing)
|
||||
builtInKeyRing.Add(nil, test.builtInDockerConfig)
|
||||
|
||||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil, "")
|
||||
fsRecordAccessor, err := imagepullmanager.NewFSPullRecordsAccessor(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal("failed to setup an file pull records accessor")
|
||||
}
|
||||
|
||||
imagePullManager, err := imagepullmanager.NewImagePullManager(context.Background(), fsRecordAccessor, imagepullmanager.AlwaysVerifyImagePullPolicy(), fakeManager, 10)
|
||||
if err != nil {
|
||||
t.Fatal("failed to setup an image pull manager")
|
||||
}
|
||||
|
||||
fakeManager.imagePuller = images.NewImageManager(
|
||||
fakeManager.recorder,
|
||||
builtInKeyRing,
|
||||
fakeManager,
|
||||
imagePullManager,
|
||||
flowcontrol.NewBackOff(time.Second, 300*time.Second),
|
||||
false,
|
||||
ptr.To[int32](0), // No limit on max parallel image pulls,
|
||||
0, // Disable image pull throttling by setting QPS to 0,
|
||||
0,
|
||||
&fakePodPullingTimeRecorder{},
|
||||
)
|
||||
|
||||
_, _, err = fakeManager.imagePuller.EnsureImageExists(ctx, nil, makeTestPod("testpod", "testpod-ns", "testpod-uid", []v1.Container{}), test.imageName, test.passedSecrets, nil, "", v1.PullAlways)
|
||||
require.NoError(t, err)
|
||||
fakeImageService.AssertImagePulledWithAuth(t, &runtimeapi.ImageSpec{Image: test.imageName, Annotations: make(map[string]string)}, test.expectedAuth, description)
|
||||
}
|
||||
|
|
@ -355,12 +366,12 @@ func TestPullWithSecretsWithError(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "invalid docker secret",
|
||||
imageName: "ubuntu",
|
||||
imageName: "ubuntu:latest",
|
||||
passedSecrets: []v1.Secret{{Type: v1.SecretTypeDockercfg, Data: map[string][]byte{v1.DockerConfigKey: []byte("invalid")}}},
|
||||
},
|
||||
{
|
||||
name: "secret provided, pull failed",
|
||||
imageName: "ubuntu",
|
||||
imageName: "ubuntu:latest",
|
||||
passedSecrets: []v1.Secret{
|
||||
{Type: v1.SecretTypeDockerConfigJson, Data: map[string][]byte{v1.DockerConfigKey: dockerConfigJSON}},
|
||||
},
|
||||
|
|
@ -375,7 +386,30 @@ func TestPullWithSecretsWithError(t *testing.T) {
|
|||
fakeImageService.InjectError("PullImage", fmt.Errorf("test-error"))
|
||||
}
|
||||
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil, "")
|
||||
fsRecordAccessor, err := imagepullmanager.NewFSPullRecordsAccessor(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal("failed to setup an file pull records accessor")
|
||||
}
|
||||
|
||||
imagePullManager, err := imagepullmanager.NewImagePullManager(context.Background(), fsRecordAccessor, imagepullmanager.AlwaysVerifyImagePullPolicy(), fakeManager, 10)
|
||||
if err != nil {
|
||||
t.Fatal("failed to setup an image pull manager")
|
||||
}
|
||||
|
||||
fakeManager.imagePuller = images.NewImageManager(
|
||||
fakeManager.recorder,
|
||||
&credentialprovider.BasicDockerKeyring{},
|
||||
fakeManager,
|
||||
imagePullManager,
|
||||
flowcontrol.NewBackOff(time.Second, 300*time.Second),
|
||||
false,
|
||||
ptr.To[int32](0), // No limit on max parallel image pulls,
|
||||
0, // Disable image pull throttling by setting QPS to 0,
|
||||
0,
|
||||
&fakePodPullingTimeRecorder{},
|
||||
)
|
||||
|
||||
imageRef, _, err := fakeManager.imagePuller.EnsureImageExists(ctx, nil, makeTestPod("testpod", "testpod-ns", "testpod-uid", []v1.Container{}), test.imageName, test.passedSecrets, nil, "", v1.PullAlways)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
|
|
@ -398,7 +432,7 @@ func TestPullThenListWithAnnotations(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, imageSpec, nil, nil, "")
|
||||
_, _, err = fakeManager.PullImage(ctx, imageSpec, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
images, err := fakeManager.ListImages(ctx)
|
||||
|
|
|
|||
|
|
@ -51,10 +51,12 @@ import (
|
|||
"k8s.io/kubernetes/pkg/credentialprovider/plugin"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/kubelet/allocation"
|
||||
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/events"
|
||||
"k8s.io/kubernetes/pkg/kubelet/images"
|
||||
imagepullmanager "k8s.io/kubernetes/pkg/kubelet/images/pullmanager"
|
||||
runtimeutil "k8s.io/kubernetes/pkg/kubelet/kuberuntime/util"
|
||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||
"k8s.io/kubernetes/pkg/kubelet/logs"
|
||||
|
|
@ -67,6 +69,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/kubelet/util/cache"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/format"
|
||||
sc "k8s.io/kubernetes/pkg/securitycontext"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -107,9 +110,6 @@ type kubeGenericRuntimeManager struct {
|
|||
// Container GC manager
|
||||
containerGC *containerGC
|
||||
|
||||
// Keyring for pulling images
|
||||
keyring credentialprovider.DockerKeyring
|
||||
|
||||
// Runner of lifecycle events.
|
||||
runner kubecontainer.HandlerRunner
|
||||
|
||||
|
|
@ -207,6 +207,8 @@ func NewKubeGenericRuntimeManager(
|
|||
maxParallelImagePulls *int32,
|
||||
imagePullQPS float32,
|
||||
imagePullBurst int,
|
||||
imagePullsCredentialVerificationPolicy string,
|
||||
preloadedImagesCredentialVerificationWhitelist []string,
|
||||
imageCredentialProviderConfigFile string,
|
||||
imageCredentialProviderBinDir string,
|
||||
singleProcessOOMKill *bool,
|
||||
|
|
@ -226,7 +228,7 @@ func NewKubeGenericRuntimeManager(
|
|||
tracerProvider trace.TracerProvider,
|
||||
tokenManager *token.Manager,
|
||||
getServiceAccount plugin.GetServiceAccountFunc,
|
||||
) (KubeGenericRuntime, error) {
|
||||
) (KubeGenericRuntime, []images.PostImageGCHook, error) {
|
||||
ctx := context.Background()
|
||||
runtimeService = newInstrumentedRuntimeService(runtimeService)
|
||||
imageService = newInstrumentedImageManagerService(imageService)
|
||||
|
|
@ -261,7 +263,7 @@ func NewKubeGenericRuntimeManager(
|
|||
typedVersion, err := kubeRuntimeManager.getTypedVersion(ctx)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Get runtime version failed")
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Only matching kubeRuntimeAPIVersion is supported now
|
||||
|
|
@ -270,7 +272,7 @@ func NewKubeGenericRuntimeManager(
|
|||
klog.ErrorS(err, "This runtime api version is not supported",
|
||||
"apiVersion", typedVersion.Version,
|
||||
"supportedAPIVersion", kubeRuntimeAPIVersion)
|
||||
return nil, ErrVersionNotSupported
|
||||
return nil, nil, ErrVersionNotSupported
|
||||
}
|
||||
|
||||
kubeRuntimeManager.runtimeName = typedVersion.RuntimeName
|
||||
|
|
@ -285,11 +287,37 @@ func NewKubeGenericRuntimeManager(
|
|||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
kubeRuntimeManager.keyring = credentialprovider.NewDefaultDockerKeyring()
|
||||
|
||||
var imageGCHooks []images.PostImageGCHook
|
||||
var imagePullManager imagepullmanager.ImagePullManager = &imagepullmanager.NoopImagePullManager{}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
|
||||
imagePullCredentialsVerificationPolicy, err := imagepullmanager.NewImagePullCredentialVerificationPolicy(
|
||||
kubeletconfiginternal.ImagePullCredentialsVerificationPolicy(imagePullsCredentialVerificationPolicy),
|
||||
preloadedImagesCredentialVerificationWhitelist)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fsRecordAccessor, err := imagepullmanager.NewFSPullRecordsAccessor(rootDirectory)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to setup the FSPullRecordsAccessor: %w", err)
|
||||
}
|
||||
|
||||
imagePullManager, err = imagepullmanager.NewImagePullManager(ctx, fsRecordAccessor, imagePullCredentialsVerificationPolicy, kubeRuntimeManager, ptr.Deref(maxParallelImagePulls, 0))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create image pull manager: %w", err)
|
||||
}
|
||||
|
||||
imageGCHooks = append(imageGCHooks, imagePullManager.PruneUnknownRecords)
|
||||
}
|
||||
|
||||
nodeKeyring := credentialprovider.NewDefaultDockerKeyring()
|
||||
kubeRuntimeManager.imagePuller = images.NewImageManager(
|
||||
kubecontainer.FilterEventRecorder(recorder),
|
||||
nodeKeyring,
|
||||
kubeRuntimeManager,
|
||||
imagePullManager,
|
||||
imageBackOff,
|
||||
serializeImagePulls,
|
||||
maxParallelImagePulls,
|
||||
|
|
@ -307,7 +335,7 @@ func NewKubeGenericRuntimeManager(
|
|||
versionCacheTTL,
|
||||
)
|
||||
|
||||
return kubeRuntimeManager, nil
|
||||
return kubeRuntimeManager, imageGCHooks, nil
|
||||
}
|
||||
|
||||
// Type returns the type of the container runtime.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import (
|
|||
apitest "k8s.io/cri-api/pkg/apis/testing"
|
||||
crierror "k8s.io/cri-api/pkg/errors"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm"
|
||||
cmtesting "k8s.io/kubernetes/pkg/kubelet/cm/testing"
|
||||
|
|
@ -68,10 +67,6 @@ var (
|
|||
)
|
||||
|
||||
func createTestRuntimeManager() (*apitest.FakeRuntimeService, *apitest.FakeImageService, *kubeGenericRuntimeManager, error) {
|
||||
return customTestRuntimeManager(&credentialprovider.BasicDockerKeyring{})
|
||||
}
|
||||
|
||||
func customTestRuntimeManager(keyring *credentialprovider.BasicDockerKeyring) (*apitest.FakeRuntimeService, *apitest.FakeImageService, *kubeGenericRuntimeManager, error) {
|
||||
fakeRuntimeService := apitest.NewFakeRuntimeService()
|
||||
fakeImageService := apitest.NewFakeImageService()
|
||||
// Only an empty machineInfo is needed here, because in unit test all containers are besteffort,
|
||||
|
|
@ -82,7 +77,7 @@ func customTestRuntimeManager(keyring *credentialprovider.BasicDockerKeyring) (*
|
|||
MemoryCapacity: uint64(memoryCapacityQuantity.Value()),
|
||||
}
|
||||
osInterface := &containertest.FakeOS{}
|
||||
manager, err := newFakeKubeRuntimeManager(fakeRuntimeService, fakeImageService, machineInfo, osInterface, &containertest.FakeRuntimeHelper{}, keyring, noopoteltrace.NewTracerProvider().Tracer(""))
|
||||
manager, err := newFakeKubeRuntimeManager(fakeRuntimeService, fakeImageService, machineInfo, osInterface, &containertest.FakeRuntimeHelper{}, noopoteltrace.NewTracerProvider().Tracer(""))
|
||||
return fakeRuntimeService, fakeImageService, manager, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ var (
|
|||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&CredentialProviderConfig{},
|
||||
&ImagePullIntent{},
|
||||
&ImagePulledRecord{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,3 +96,75 @@ type ExecEnvVar struct {
|
|||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ImagePullIntent is a record of the kubelet attempting to pull an image.
|
||||
//
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ImagePullIntent struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// Image is the image spec from a Container's `image` field.
|
||||
// The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe
|
||||
// characters like ':' and '/'.
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
// ImagePullRecord is a record of an image that was pulled by the kubelet.
|
||||
//
|
||||
// If there are no records in the `kubernetesSecrets` field and both `nodeWideCredentials`
|
||||
// and `anonymous` are `false`, credentials must be re-checked the next time an
|
||||
// image represented by this record is being requested.
|
||||
//
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ImagePulledRecord struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// LastUpdatedTime is the time of the last update to this record
|
||||
LastUpdatedTime metav1.Time `json:"lastUpdatedTime"`
|
||||
|
||||
// ImageRef is a reference to the image represented by this file as received
|
||||
// from the CRI.
|
||||
// The filename is a SHA-256 hash of this value. This is to avoid filename-unsafe
|
||||
// characters like ':' and '/'.
|
||||
ImageRef string `json:"imageRef"`
|
||||
|
||||
// CredentialMapping maps `image` to the set of credentials that it was
|
||||
// previously pulled with.
|
||||
// `image` in this case is the content of a pod's container `image` field that's
|
||||
// got its tag/digest removed.
|
||||
//
|
||||
// Example:
|
||||
// Container requests the `hello-world:latest@sha256:91fb4b041da273d5a3273b6d587d62d518300a6ad268b28628f74997b93171b2` image:
|
||||
// "credentialMapping": {
|
||||
// "hello-world": { "nodePodsAccessible": true }
|
||||
// }
|
||||
CredentialMapping map[string]ImagePullCredentials `json:"credentialMapping,omitempty"`
|
||||
}
|
||||
|
||||
// ImagePullCredentials describe credentials that can be used to pull an image.
|
||||
type ImagePullCredentials struct {
|
||||
// KuberneteSecretCoordinates is an index of coordinates of all the kubernetes
|
||||
// secrets that were used to pull the image.
|
||||
// +optional
|
||||
// +listType=set
|
||||
KubernetesSecrets []ImagePullSecret `json:"kubernetesSecrets"`
|
||||
|
||||
// NodePodsAccessible is a flag denoting the pull credentials are accessible
|
||||
// by all the pods on the node, or that no credentials are needed for the pull.
|
||||
//
|
||||
// If true, it is mutually exclusive with the `kubernetesSecrets` field.
|
||||
// +optional
|
||||
NodePodsAccessible bool `json:"nodePodsAccessible,omitempty"`
|
||||
}
|
||||
|
||||
// ImagePullSecret is a representation of a Kubernetes secret object coordinates along
|
||||
// with a credential hash of the pull secret credentials this object contains.
|
||||
type ImagePullSecret struct {
|
||||
UID string `json:"uid"`
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
|
||||
// CredentialHash is a SHA-256 retrieved by hashing the image pull credentials
|
||||
// content of the secret specified by the UID/Namespace/Name coordinates.
|
||||
CredentialHash string `json:"credentialHash"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,3 +109,98 @@ func (in *ExecEnvVar) DeepCopy() *ExecEnvVar {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullCredentials) DeepCopyInto(out *ImagePullCredentials) {
|
||||
*out = *in
|
||||
if in.KubernetesSecrets != nil {
|
||||
in, out := &in.KubernetesSecrets, &out.KubernetesSecrets
|
||||
*out = make([]ImagePullSecret, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullCredentials.
|
||||
func (in *ImagePullCredentials) DeepCopy() *ImagePullCredentials {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullCredentials)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullIntent) DeepCopyInto(out *ImagePullIntent) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullIntent.
|
||||
func (in *ImagePullIntent) DeepCopy() *ImagePullIntent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullIntent)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImagePullIntent) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecret.
|
||||
func (in *ImagePullSecret) DeepCopy() *ImagePullSecret {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePullSecret)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImagePulledRecord) DeepCopyInto(out *ImagePulledRecord) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.LastUpdatedTime.DeepCopyInto(&out.LastUpdatedTime)
|
||||
if in.CredentialMapping != nil {
|
||||
in, out := &in.CredentialMapping, &out.CredentialMapping
|
||||
*out = make(map[string]ImagePullCredentials, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePulledRecord.
|
||||
func (in *ImagePulledRecord) DeepCopy() *ImagePulledRecord {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImagePulledRecord)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImagePulledRecord) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,25 @@ const (
|
|||
StaticMemoryManagerPolicy = "Static"
|
||||
)
|
||||
|
||||
// ImagePullCredentialsVerificationPolicy is an enum for the policy that is enforced
|
||||
// when pod is requesting an image that appears on the system
|
||||
type ImagePullCredentialsVerificationPolicy string
|
||||
|
||||
const (
|
||||
// NeverVerify will never require credential verification for images that
|
||||
// already exist on the node
|
||||
NeverVerify ImagePullCredentialsVerificationPolicy = "NeverVerify"
|
||||
// NeverVerifyPreloadedImages does not require credential verification for images
|
||||
// pulled outside the kubelet process
|
||||
NeverVerifyPreloadedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyPreloadedImages"
|
||||
// NeverVerifyAllowlistedImages does not require credential verification for
|
||||
// a list of images that were pulled outside the kubelet process
|
||||
NeverVerifyAllowlistedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyAllowlistedImages"
|
||||
// AlwaysVerify requires credential verification for accessing any image on the
|
||||
// node irregardless how it was pulled
|
||||
AlwaysVerify ImagePullCredentialsVerificationPolicy = "AlwaysVerify"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// KubeletConfiguration contains the configuration for the Kubelet
|
||||
|
|
@ -210,6 +229,28 @@ type KubeletConfiguration struct {
|
|||
// Default: 10
|
||||
// +optional
|
||||
RegistryBurst int32 `json:"registryBurst,omitempty"`
|
||||
// imagePullCredentialsVerificationPolicy determines how credentials should be
|
||||
// verified when pod requests an image that is already present on the node:
|
||||
// - NeverVerify
|
||||
// - anyone on a node can use any image present on the node
|
||||
// - NeverVerifyPreloadedImages
|
||||
// - images that were pulled to the node by something else than the kubelet
|
||||
// can be used without reverifying pull credentials
|
||||
// - NeverVerifyAllowlistedImages
|
||||
// - like "NeverVerifyPreloadedImages" but only node images from
|
||||
// `preloadedImagesVerificationAllowlist` don't require reverification
|
||||
// - AlwaysVerify
|
||||
// - all images require credential reverification
|
||||
// +optional
|
||||
ImagePullCredentialsVerificationPolicy ImagePullCredentialsVerificationPolicy `json:"imagePullCredentialsVerificationPolicy,omitempty"`
|
||||
// preloadedImagesVerificationAllowlist specifies a list of images that are
|
||||
// exempted from credential reverification for the "NeverVerifyAllowlistedImages"
|
||||
// `imagePullCredentialsVerificationPolicy`.
|
||||
// The list accepts a full path segment wildcard suffix "/*".
|
||||
// Only use image specs without an image tag or digest.
|
||||
// +optional
|
||||
// +listType=set
|
||||
PreloadedImagesVerificationAllowlist []string `json:"preloadedImagesVerificationAllowlist,omitempty"`
|
||||
// eventRecordQPS is the maximum event creations per second. If 0, there
|
||||
// is no limit enforced. The value cannot be a negative number.
|
||||
// Default: 50
|
||||
|
|
|
|||
|
|
@ -229,6 +229,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) {
|
|||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
if in.PreloadedImagesVerificationAllowlist != nil {
|
||||
in, out := &in.PreloadedImagesVerificationAllowlist, &out.PreloadedImagesVerificationAllowlist
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.EventRecordQPS != nil {
|
||||
in, out := &in.EventRecordQPS, &out.EventRecordQPS
|
||||
*out = new(int32)
|
||||
|
|
|
|||
|
|
@ -651,6 +651,12 @@
|
|||
lockToDefault: false
|
||||
preRelease: Alpha
|
||||
version: "1.32"
|
||||
- name: KubeletEnsureSecretPulledImages
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
lockToDefault: false
|
||||
preRelease: Alpha
|
||||
version: "1.33"
|
||||
- name: KubeletFineGrainedAuthz
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
|
|
|
|||
Loading…
Reference in a new issue