diff --git a/pkg/api/types.go b/pkg/api/types.go index d247f259b47..8deac6862f9 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -831,6 +831,10 @@ type PodSpec struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // PodStatus represents information about the status of a pod. Status may trail the actual @@ -1630,6 +1634,12 @@ type ObjectReference struct { FieldPath string `json:"fieldPath,omitempty"` } +// LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. +type LocalObjectReference struct { + //TODO: Add other useful fields. apiVersion, kind, uid? + Name string +} + type SerializedReference struct { TypeMeta `json:",inline"` Reference ObjectReference `json:"reference,omitempty" description:"the reference to an object in the system"` @@ -1704,6 +1714,10 @@ type ContainerManifest struct { // Required: Set DNS policy. DNSPolicy DNSPolicy `json:"dnsPolicy"` HostNetwork bool `json:"hostNetwork,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // ContainerManifestList is used to communicate container manifests to kubelet. diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 073b3eeb495..0169de49a8e 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -1599,6 +1599,22 @@ func convert_api_ListOptions_To_v1_ListOptions(in *newer.ListOptions, out *ListO return nil } +func convert_v1_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *newer.LocalObjectReference, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*LocalObjectReference))(in) + } + out.Name = in.Name + return nil +} + +func convert_api_LocalObjectReference_To_v1_LocalObjectReference(in *newer.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*newer.LocalObjectReference))(in) + } + out.Name = in.Name + return nil +} + func convert_v1_NFSVolumeSource_To_api_NFSVolumeSource(in *NFSVolumeSource, out *newer.NFSVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*NFSVolumeSource))(in) @@ -2856,6 +2872,16 @@ func convert_v1_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s conver out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork + if in.ImagePullSecrets != nil { + out.ImagePullSecrets = make([]newer.LocalObjectReference, len(in.ImagePullSecrets)) + for i := range in.ImagePullSecrets { + if err := convert_v1_LocalObjectReference_To_api_LocalObjectReference(&in.ImagePullSecrets[i], &out.ImagePullSecrets[i], s); err != nil { + return err + } + } + } else { + out.ImagePullSecrets = nil + } return nil } @@ -2908,6 +2934,16 @@ func convert_api_PodSpec_To_v1_PodSpec(in *newer.PodSpec, out *PodSpec, s conver out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork + if in.ImagePullSecrets != nil { + out.ImagePullSecrets = make([]LocalObjectReference, len(in.ImagePullSecrets)) + for i := range in.ImagePullSecrets { + if err := convert_api_LocalObjectReference_To_v1_LocalObjectReference(&in.ImagePullSecrets[i], &out.ImagePullSecrets[i], s); err != nil { + return err + } + } + } else { + out.ImagePullSecrets = nil + } return nil } @@ -4518,6 +4554,7 @@ func init() { convert_api_ListMeta_To_v1_ListMeta, convert_api_ListOptions_To_v1_ListOptions, convert_api_List_To_v1_List, + convert_api_LocalObjectReference_To_v1_LocalObjectReference, convert_api_NFSVolumeSource_To_v1_NFSVolumeSource, convert_api_NamespaceList_To_v1_NamespaceList, convert_api_NamespaceSpec_To_v1_NamespaceSpec, @@ -4628,6 +4665,7 @@ func init() { convert_v1_ListMeta_To_api_ListMeta, convert_v1_ListOptions_To_api_ListOptions, convert_v1_List_To_api_List, + convert_v1_LocalObjectReference_To_api_LocalObjectReference, convert_v1_NFSVolumeSource_To_api_NFSVolumeSource, convert_v1_NamespaceList_To_api_NamespaceList, convert_v1_NamespaceSpec_To_api_NamespaceSpec, diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index c4c38652f45..8a1460f3493 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -825,6 +825,10 @@ type PodSpec struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images" patchStrategy:"merge" patchMergeKey:"name"` } // PodStatus represents information about the status of a pod. Status may trail the actual @@ -1555,6 +1559,12 @@ type ObjectReference struct { FieldPath string `json:"fieldPath,omitempty" description:"if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"` } +// LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. +type LocalObjectReference struct { + //TODO: Add other useful fields. apiVersion, kind, uid? + Name string `json:"name,omitempty" description:"name of the referent"` +} + type SerializedReference struct { TypeMeta `json:",inline"` Reference ObjectReference `json:"reference,omitempty" description:"the reference to an object in the system"` diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index dea7f90d79a..56cf5c2395e 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -706,6 +706,9 @@ func addConversionFuncs() { if err := s.Convert(&in.RestartPolicy, &out.RestartPolicy, 0); err != nil { return err } + if err := s.Convert(&in.ImagePullSecrets, &out.ImagePullSecrets, 0); err != nil { + return err + } if in.TerminationGracePeriodSeconds != nil { out.TerminationGracePeriodSeconds = new(int64) *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds @@ -729,6 +732,9 @@ func addConversionFuncs() { if err := s.Convert(&in.RestartPolicy, &out.RestartPolicy, 0); err != nil { return err } + if err := s.Convert(&in.ImagePullSecrets, &out.ImagePullSecrets, 0); err != nil { + return err + } if in.TerminationGracePeriodSeconds != nil { out.TerminationGracePeriodSeconds = new(int64) *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index b89fbb5f3ba..ebc059407cb 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -75,6 +75,10 @@ type ContainerManifest struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // ContainerManifestList is used to communicate container manifests to kubelet. @@ -1409,6 +1413,12 @@ type ObjectReference struct { FieldPath string `json:"fieldPath,omitempty" description:"if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"` } +// LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. +type LocalObjectReference struct { + //TODO: Add other useful fields. apiVersion, kind, uid? + Name string `json:"name,omitempty" description:"name of the referent"` +} + type SerializedReference struct { TypeMeta `json:",inline"` Reference ObjectReference `json:"reference,omitempty" description:"the reference to an object in the system"` @@ -1505,6 +1515,10 @@ type PodSpec struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // List holds a list of objects, which may not be known by the server. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 27b29e90194..685c4866524 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -486,6 +486,9 @@ func addConversionFuncs() { if err := s.Convert(&in.RestartPolicy, &out.RestartPolicy, 0); err != nil { return err } + if err := s.Convert(&in.ImagePullSecrets, &out.ImagePullSecrets, 0); err != nil { + return err + } if in.TerminationGracePeriodSeconds != nil { out.TerminationGracePeriodSeconds = new(int64) *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds @@ -509,6 +512,9 @@ func addConversionFuncs() { if err := s.Convert(&in.RestartPolicy, &out.RestartPolicy, 0); err != nil { return err } + if err := s.Convert(&in.ImagePullSecrets, &out.ImagePullSecrets, 0); err != nil { + return err + } if in.TerminationGracePeriodSeconds != nil { out.TerminationGracePeriodSeconds = new(int64) *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index c59245a45b6..fe84122d2c2 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -1443,6 +1443,12 @@ type ObjectReference struct { FieldPath string `json:"fieldPath,omitempty" description:"if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"` } +// LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. +type LocalObjectReference struct { + //TODO: Add other useful fields. apiVersion, kind, uid? + Name string `json:"name,omitempty" description:"name of the referent"` +} + type SerializedReference struct { TypeMeta `json:",inline"` Reference ObjectReference `json:"reference,omitempty" description:"the reference to an object in the system"` @@ -1537,6 +1543,10 @@ type ContainerManifest struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // ContainerManifestList is used to communicate container manifests to kubelet. @@ -1581,6 +1591,10 @@ type PodSpec struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images"` } // List holds a list of objects, which may not be known by the server. diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index ae9406cd849..837c6f02560 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -1413,6 +1413,22 @@ func convert_api_ListOptions_To_v1beta3_ListOptions(in *newer.ListOptions, out * return nil } +func convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *newer.LocalObjectReference, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*LocalObjectReference))(in) + } + out.Name = in.Name + return nil +} + +func convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference(in *newer.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*newer.LocalObjectReference))(in) + } + out.Name = in.Name + return nil +} + func convert_v1beta3_NFSVolumeSource_To_api_NFSVolumeSource(in *NFSVolumeSource, out *newer.NFSVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*NFSVolumeSource))(in) @@ -2670,6 +2686,16 @@ func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s c out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork + if in.ImagePullSecrets != nil { + out.ImagePullSecrets = make([]newer.LocalObjectReference, len(in.ImagePullSecrets)) + for i := range in.ImagePullSecrets { + if err := convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference(&in.ImagePullSecrets[i], &out.ImagePullSecrets[i], s); err != nil { + return err + } + } + } else { + out.ImagePullSecrets = nil + } return nil } @@ -2722,6 +2748,16 @@ func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s c out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork + if in.ImagePullSecrets != nil { + out.ImagePullSecrets = make([]LocalObjectReference, len(in.ImagePullSecrets)) + for i := range in.ImagePullSecrets { + if err := convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference(&in.ImagePullSecrets[i], &out.ImagePullSecrets[i], s); err != nil { + return err + } + } + } else { + out.ImagePullSecrets = nil + } return nil } @@ -4331,6 +4367,7 @@ func init() { convert_api_ListMeta_To_v1beta3_ListMeta, convert_api_ListOptions_To_v1beta3_ListOptions, convert_api_List_To_v1beta3_List, + convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference, convert_api_NFSVolumeSource_To_v1beta3_NFSVolumeSource, convert_api_NamespaceList_To_v1beta3_NamespaceList, convert_api_NamespaceSpec_To_v1beta3_NamespaceSpec, @@ -4440,6 +4477,7 @@ func init() { convert_v1beta3_ListMeta_To_api_ListMeta, convert_v1beta3_ListOptions_To_api_ListOptions, convert_v1beta3_List_To_api_List, + convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference, convert_v1beta3_NFSVolumeSource_To_api_NFSVolumeSource, convert_v1beta3_NamespaceList_To_api_NamespaceList, convert_v1beta3_NamespaceSpec_To_api_NamespaceSpec, diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index efb6cefce08..50e0bc4cc69 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -829,6 +829,10 @@ type PodSpec struct { // used must be specified. // Optional: Default to false. HostNetwork bool `json:"hostNetwork,omitempty" description:"host networking requested for this pod"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + ImagePullSecrets []LocalObjectReference `json:"imagePullSecrets,omitempty" description:"list of references to secrets in the same namespace available for pulling the container images" patchStrategy:"merge" patchMergeKey:"name"` } // PodStatus represents information about the status of a pod. Status may trail the actual @@ -1559,6 +1563,12 @@ type ObjectReference struct { FieldPath string `json:"fieldPath,omitempty" description:"if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"` } +// LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. +type LocalObjectReference struct { + //TODO: Add other useful fields. apiVersion, kind, uid? + Name string `json:"name,omitempty" description:"name of the referent"` +} + type SerializedReference struct { TypeMeta `json:",inline"` Reference ObjectReference `json:"reference,omitempty" description:"the reference to an object in the system"` diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index bc47d2be2ee..f5dd81d7ed4 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -21,6 +21,7 @@ import ( "fmt" "net" "path" + "reflect" "regexp" "strings" @@ -890,6 +891,20 @@ func validateHostNetwork(hostNetwork bool, containers []api.Container) errs.Vali return allErrors } +// validateImagePullSecrets checks to make sure the pull secrets are well formed. Right now, we only expect name to be set (it's the only field). If this ever changes +// and someone decides to set those fields, we'd like to know. +func validateImagePullSecrets(imagePullSecrets []api.LocalObjectReference) errs.ValidationErrorList { + allErrors := errs.ValidationErrorList{} + for i, currPullSecret := range imagePullSecrets { + strippedRef := api.LocalObjectReference{Name: currPullSecret.Name} + + if !reflect.DeepEqual(strippedRef, currPullSecret) { + allErrors = append(allErrors, errs.NewFieldInvalid(fmt.Sprintf("[%d]", i), currPullSecret, "only name may be set")) + } + } + return allErrors +} + // ValidatePod tests if required fields in the pod are set. func ValidatePod(pod *api.Pod) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -913,6 +928,7 @@ func ValidatePodSpec(spec *api.PodSpec) errs.ValidationErrorList { allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy).Prefix("dnsPolicy")...) allErrs = append(allErrs, ValidateLabels(spec.NodeSelector, "nodeSelector")...) allErrs = append(allErrs, validateHostNetwork(spec.HostNetwork, spec.Containers).Prefix("hostNetwork")...) + allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets).Prefix("imagePullSecrets")...) if spec.ActiveDeadlineSeconds != nil { if *spec.ActiveDeadlineSeconds <= 0 { diff --git a/pkg/credentialprovider/config.go b/pkg/credentialprovider/config.go index d9ffe4803f3..bd04be4098f 100644 --- a/pkg/credentialprovider/config.go +++ b/pkg/credentialprovider/config.go @@ -147,9 +147,9 @@ func readDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error return } -// DockerConfigEntryWithAuth is used solely for deserializing the Auth field +// dockerConfigEntryWithAuth is used solely for deserializing the Auth field // into a dockerConfigEntry during JSON deserialization. -type DockerConfigEntryWithAuth struct { +type dockerConfigEntryWithAuth struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Email string `json:"email,omitempty"` @@ -157,7 +157,7 @@ type DockerConfigEntryWithAuth struct { } func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error { - var tmp DockerConfigEntryWithAuth + var tmp dockerConfigEntryWithAuth err := json.Unmarshal(data, &tmp) if err != nil { return err @@ -195,8 +195,8 @@ func decodeDockerConfigFieldAuth(field string) (username, password string, err e return } -func (ident DockerConfigEntry) ConvertToDockerConfigCompatible() DockerConfigEntryWithAuth { - ret := DockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""} +func (ident DockerConfigEntry) ConvertToDockerConfigCompatible() dockerConfigEntryWithAuth { + ret := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""} ret.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password) return ret diff --git a/pkg/credentialprovider/keyring.go b/pkg/credentialprovider/keyring.go index 94855b8a710..334cfadb1a9 100644 --- a/pkg/credentialprovider/keyring.go +++ b/pkg/credentialprovider/keyring.go @@ -17,6 +17,7 @@ limitations under the License. package credentialprovider import ( + "encoding/json" "net/url" "sort" "strings" @@ -24,6 +25,7 @@ import ( docker "github.com/fsouza/go-dockerclient" "github.com/golang/glog" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -164,3 +166,50 @@ type FakeKeyring struct { func (f *FakeKeyring) Lookup(image string) ([]docker.AuthConfiguration, bool) { return f.auth, f.ok } + +// unionDockerKeyring delegates to a set of keyrings. +type unionDockerKeyring struct { + keyrings []DockerKeyring +} + +func (k *unionDockerKeyring) Lookup(image string) ([]docker.AuthConfiguration, bool) { + authConfigs := []docker.AuthConfiguration{} + + for _, subKeyring := range k.keyrings { + if subKeyring == nil { + continue + } + + currAuthResults, _ := subKeyring.Lookup(image) + authConfigs = append(authConfigs, currAuthResults...) + } + + return authConfigs, (len(authConfigs) > 0) +} + +// MakeDockerKeyring inspects the passedSecrets to see if they contain any DockerConfig secrets. If they do, +// 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 []api.Secret, defaultKeyring DockerKeyring) (DockerKeyring, error) { + passedCredentials := []DockerConfig{} + for _, passedSecret := range passedSecrets { + if dockercfgBytes, dockercfgExists := passedSecret.Data[api.DockerConfigKey]; (passedSecret.Type == api.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) { + dockercfg := DockerConfig{} + if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil { + return nil, err + } + + passedCredentials = append(passedCredentials, dockercfg) + } + } + + if len(passedCredentials) > 0 { + basicKeyring := &BasicDockerKeyring{} + for _, currCredentials := range passedCredentials { + basicKeyring.Add(currCredentials) + } + return &unionDockerKeyring{[]DockerKeyring{basicKeyring, defaultKeyring}}, nil + } + + return defaultKeyring, nil +} diff --git a/pkg/kubelet/container/runtime.go b/pkg/kubelet/container/runtime.go index b96424c9dd2..6fa2073da49 100644 --- a/pkg/kubelet/container/runtime.go +++ b/pkg/kubelet/container/runtime.go @@ -52,7 +52,7 @@ type Runtime interface { // exited and dead containers (used for garbage collection). GetPods(all bool) ([]*Pod, error) // Syncs the running pod into the desired pod. - SyncPod(pod *api.Pod, runningPod Pod, podStatus api.PodStatus) error + SyncPod(pod *api.Pod, runningPod Pod, podStatus api.PodStatus, pullSecrets []api.Secret) error // KillPod kills all the containers of a pod. KillPod(pod Pod) error // GetPodStatus retrieves the status of the pod, including the information of @@ -60,7 +60,7 @@ type Runtime interface { GetPodStatus(*api.Pod) (*api.PodStatus, error) // PullImage pulls an image from the network to local storage using the supplied // secrets if necessary. - PullImage(image ImageSpec, secrets []api.Secret) error + PullImage(image ImageSpec, pullSecrets []api.Secret) error // IsImagePresent checks whether the container image is already in the local storage. IsImagePresent(image ImageSpec) (bool, error) // Gets all images currently on the machine. diff --git a/pkg/kubelet/dockertools/docker.go b/pkg/kubelet/dockertools/docker.go index 9952d4e4299..340838e89e3 100644 --- a/pkg/kubelet/dockertools/docker.go +++ b/pkg/kubelet/dockertools/docker.go @@ -78,7 +78,7 @@ type KubeletContainerName struct { // DockerPuller is an abstract interface for testability. It abstracts image pull operations. type DockerPuller interface { - Pull(image string) error + Pull(image string, secrets []api.Secret) error IsImagePresent(image string) (bool, error) } @@ -113,7 +113,7 @@ func parseImageName(image string) (string, string) { return parsers.ParseRepositoryTag(image) } -func (p dockerPuller) Pull(image string) error { +func (p dockerPuller) Pull(image string, secrets []api.Secret) error { repoToPull, tag := parseImageName(image) // If no tag was specified, use the default "latest". @@ -126,7 +126,12 @@ func (p dockerPuller) Pull(image string) error { Tag: tag, } - creds, haveCredentials := p.keyring.Lookup(repoToPull) + keyring, err := credentialprovider.MakeDockerKeyring(secrets, p.keyring) + if err != nil { + return err + } + + creds, haveCredentials := keyring.Lookup(repoToPull) if !haveCredentials { glog.V(1).Infof("Pulling image %s without credentials", image) @@ -161,9 +166,9 @@ func (p dockerPuller) Pull(image string) error { return utilerrors.NewAggregate(pullErrs) } -func (p throttledDockerPuller) Pull(image string) error { +func (p throttledDockerPuller) Pull(image string, secrets []api.Secret) error { if p.limiter.CanAccept() { - return p.puller.Pull(image) + return p.puller.Pull(image, secrets) } return fmt.Errorf("pull QPS exceeded.") } diff --git a/pkg/kubelet/dockertools/docker_test.go b/pkg/kubelet/dockertools/docker_test.go index 8c4e1c04901..5b7b2e863dc 100644 --- a/pkg/kubelet/dockertools/docker_test.go +++ b/pkg/kubelet/dockertools/docker_test.go @@ -17,6 +17,7 @@ limitations under the License. package dockertools import ( + "encoding/json" "fmt" "hash/adler32" "reflect" @@ -220,7 +221,7 @@ func TestPullWithNoSecrets(t *testing.T) { keyring: fakeKeyring, } - err := dp.Pull(test.imageName) + err := dp.Pull(test.imageName, []api.Secret{}) if err != nil { t.Errorf("unexpected non-nil err: %s", err) continue @@ -237,6 +238,73 @@ func TestPullWithNoSecrets(t *testing.T) { } } +func TestPullWithSecrets(t *testing.T) { + // auth value is equivalent to: "username":"passed-user","password":"passed-password" + dockerCfg := map[string]map[string]string{"index.docker.io/v1/": {"email": "passed-email", "auth": "cGFzc2VkLXVzZXI6cGFzc2VkLXBhc3N3b3Jk"}} + dockercfgContent, err := json.Marshal(dockerCfg) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + tests := map[string]struct { + imageName string + passedSecrets []api.Secret + builtInDockerConfig credentialprovider.DockerConfig + expectedPulls []string + }{ + "no matching secrets": { + "ubuntu", + []api.Secret{}, + credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{}), + []string{"ubuntu:latest using {}"}, + }, + "default keyring secrets": { + "ubuntu", + []api.Secret{}, + credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{"index.docker.io/v1/": {"built-in", "password", "email"}}), + []string{`ubuntu:latest using {"username":"built-in","password":"password","email":"email"}`}, + }, + "default keyring secrets unused": { + "ubuntu", + []api.Secret{}, + credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{"extraneous": {"built-in", "password", "email"}}), + []string{`ubuntu:latest using {}`}, + }, + "builtin keyring secrets, but use passed": { + "ubuntu", + []api.Secret{{Type: api.SecretTypeDockercfg, Data: map[string][]byte{api.DockerConfigKey: dockercfgContent}}}, + credentialprovider.DockerConfig(map[string]credentialprovider.DockerConfigEntry{"index.docker.io/v1/": {"built-in", "password", "email"}}), + []string{`ubuntu:latest using {"username":"passed-user","password":"passed-password","email":"passed-email"}`}, + }, + } + for _, test := range tests { + builtInKeyRing := &credentialprovider.BasicDockerKeyring{} + builtInKeyRing.Add(test.builtInDockerConfig) + + fakeClient := &FakeDockerClient{} + + dp := dockerPuller{ + client: fakeClient, + keyring: builtInKeyRing, + } + + err := dp.Pull(test.imageName, test.passedSecrets) + if err != nil { + t.Errorf("unexpected non-nil err: %s", err) + continue + } + + if e, a := 1, len(fakeClient.pulled); e != a { + t.Errorf("%s: expected 1 pulled image, got %d: %v", test.imageName, a, fakeClient.pulled) + continue + } + + if e, a := test.expectedPulls, fakeClient.pulled; !reflect.DeepEqual(e, a) { + t.Errorf("%s: expected pull of %v, but got %v", test.imageName, e, a) + } + } +} + func TestDockerKeyringLookupFails(t *testing.T) { fakeKeyring := &credentialprovider.FakeKeyring{} fakeClient := &FakeDockerClient{ @@ -248,7 +316,7 @@ func TestDockerKeyringLookupFails(t *testing.T) { keyring: fakeKeyring, } - err := dp.Pull("host/repository/image:version") + err := dp.Pull("host/repository/image:version", []api.Secret{}) if err == nil { t.Errorf("unexpected non-error") } diff --git a/pkg/kubelet/dockertools/fake_docker_client.go b/pkg/kubelet/dockertools/fake_docker_client.go index 1a29fdef1fb..1ae9e0954ae 100644 --- a/pkg/kubelet/dockertools/fake_docker_client.go +++ b/pkg/kubelet/dockertools/fake_docker_client.go @@ -26,6 +26,7 @@ import ( "github.com/fsouza/go-dockerclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -327,7 +328,7 @@ type FakeDockerPuller struct { } // Pull records the image pull attempt, and optionally injects an error. -func (f *FakeDockerPuller) Pull(image string) (err error) { +func (f *FakeDockerPuller) Pull(image string, secrets []api.Secret) (err error) { f.Lock() defer f.Unlock() f.ImagesPulled = append(f.ImagesPulled, image) diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index 924945c887d..2454f395000 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -774,7 +774,7 @@ func (dm *DockerManager) ListImages() ([]kubecontainer.Image, error) { // TODO(vmarmol): Consider unexporting. // PullImage pulls an image from network to local storage. func (dm *DockerManager) PullImage(image kubecontainer.ImageSpec, secrets []api.Secret) error { - return dm.Puller.Pull(image.Image) + return dm.Puller.Pull(image.Image, secrets) } // IsImagePresent checks whether the container image is already in the local storage. @@ -1264,8 +1264,7 @@ func (dm *DockerManager) createPodInfraContainer(pod *api.Pod) (kubeletTypes.Doc return "", err } if !ok { - // TODO get the pull secrets from the container's ImageSpec and the pod's service account - if err := dm.PullImage(spec, nil); err != nil { + if err := dm.PullImage(spec, nil /* no pod secrets for the infra container */); err != nil { if ref != nil { dm.recorder.Eventf(ref, "failed", "Failed to pull image %q: %v", container.Image, err) } @@ -1438,7 +1437,7 @@ func (dm *DockerManager) clearReasonCache(pod *api.Pod, container *api.Container } // Pull the image for the specified pod and container. -func (dm *DockerManager) pullImage(pod *api.Pod, container *api.Container) error { +func (dm *DockerManager) pullImage(pod *api.Pod, container *api.Container, pullSecrets []api.Secret) error { spec := kubecontainer.ImageSpec{container.Image} present, err := dm.IsImagePresent(spec) @@ -1456,14 +1455,13 @@ func (dm *DockerManager) pullImage(pod *api.Pod, container *api.Container) error return nil } - // TODO get the pull secrets from the container's ImageSpec and the pod's service account - err = dm.PullImage(spec, nil) + err = dm.PullImage(spec, pullSecrets) dm.runtimeHooks.ReportImagePull(pod, container, err) return err } // Sync the running pod to match the specified desired pod. -func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus api.PodStatus) error { +func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus api.PodStatus, pullSecrets []api.Secret) error { podFullName := kubecontainer.GetPodFullName(pod) containerChanges, err := dm.computePodContainerChanges(pod, runningPod, podStatus) glog.V(3).Infof("Got container changes for pod %q: %+v", podFullName, containerChanges) @@ -1517,7 +1515,7 @@ func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, pod for idx := range containerChanges.ContainersToStart { container := &pod.Spec.Containers[idx] glog.V(4).Infof("Creating container %+v", container) - err := dm.pullImage(pod, container) + err := dm.pullImage(pod, container, pullSecrets) dm.updateReasonCache(pod, container, err) if err != nil { glog.Warningf("Failed to pull image %q from pod %q and container %q: %v", container.Image, kubecontainer.GetPodFullName(pod), container.Name, err) diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index 1320c531d18..8c54bd0ac4f 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -1060,7 +1060,13 @@ func (kl *Kubelet) syncPod(pod *api.Pod, mirrorPod *api.Pod, runningPod kubecont return err } - err = kl.containerRuntime.SyncPod(pod, runningPod, podStatus) + pullSecrets, err := kl.getPullSecretsForPod(pod) + if err != nil { + glog.Errorf("Unable to get pull secrets for pod %q (uid %q): %v", podFullName, uid, err) + return err + } + + err = kl.containerRuntime.SyncPod(pod, runningPod, podStatus, pullSecrets) if err != nil { return err } @@ -1088,6 +1094,24 @@ func (kl *Kubelet) syncPod(pod *api.Pod, mirrorPod *api.Pod, runningPod kubecont return nil } +// getPullSecretsForPod inspects the Pod and retrieves the referenced pull secrets +// TODO transitively search through the referenced service account to find the required secrets +// TODO duplicate secrets are being retrieved multiple times and there is no cache. Creating and using a secret manager interface will make this easier to address. +func (kl *Kubelet) getPullSecretsForPod(pod *api.Pod) ([]api.Secret, error) { + pullSecrets := []api.Secret{} + + for _, secretRef := range pod.Spec.ImagePullSecrets { + secret, err := kl.kubeClient.Secrets(pod.Namespace).Get(secretRef.Name) + if err != nil { + return nil, err + } + + pullSecrets = append(pullSecrets, *secret) + } + + return pullSecrets, nil +} + // Stores all volumes defined by the set of pods into a map. // Keys for each entry are in the format (POD_ID)/(VOLUME_NAME) func getDesiredVolumes(pods []*api.Pod) map[string]api.Volume { diff --git a/pkg/kubelet/rkt/rkt.go b/pkg/kubelet/rkt/rkt.go index 52584314a2c..690e1adfc1b 100644 --- a/pkg/kubelet/rkt/rkt.go +++ b/pkg/kubelet/rkt/rkt.go @@ -789,7 +789,12 @@ func (r *runtime) PullImage(image kubecontainer.ImageSpec, pullSecrets []api.Sec tag = "latest" } - creds, ok := r.dockerKeyring.Lookup(repoToPull) + keyring, err := credentialprovider.MakeDockerKeyring(pullSecrets, r.dockerKeyring) + if err != nil { + return err + } + + creds, ok := keyring.Lookup(repoToPull) if !ok { glog.V(1).Infof("Pulling image %s without credentials", img) } @@ -827,7 +832,7 @@ func (r *runtime) RemoveImage(image kubecontainer.ImageSpec) error { } // SyncPod syncs the running pod to match the specified desired pod. -func (r *runtime) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus api.PodStatus) error { +func (r *runtime) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus api.PodStatus, pullSecrets []api.Secret) error { podFullName := kubecontainer.GetPodFullName(pod) if len(runningPod.Containers) == 0 { glog.V(4).Infof("Pod %q is not running, will start it", podFullName)