diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index a87a1def4da..854f3fe25a0 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -259,6 +259,7 @@ _kubectl_get() must_have_one_noun+=("resourcequota") must_have_one_noun+=("secret") must_have_one_noun+=("service") + must_have_one_noun+=("serviceaccount") } _kubectl_describe() @@ -286,6 +287,7 @@ _kubectl_describe() must_have_one_noun+=("resourcequota") must_have_one_noun+=("secret") must_have_one_noun+=("service") + must_have_one_noun+=("serviceaccount") } _kubectl_create() diff --git a/pkg/api/register.go b/pkg/api/register.go index 176b8852e9d..ca879ffd32f 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -51,6 +51,8 @@ func init() { &ResourceQuotaList{}, &Namespace{}, &NamespaceList{}, + &ServiceAccount{}, + &ServiceAccountList{}, &Secret{}, &SecretList{}, &PersistentVolume{}, @@ -98,6 +100,8 @@ func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} +func (*ServiceAccount) IsAnAPIObject() {} +func (*ServiceAccountList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index d3525f6a8ee..f1077b24345 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -814,6 +814,10 @@ type PodSpec struct { // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // ServiceAccount is the name of the ServiceAccount to use to run this pod + // The pod will be allowed to use secrets referenced by the ServiceAccount + ServiceAccount string `json:"serviceAccount"` + // Host is a request to schedule this pod onto a specific host. If it is non-empty, // the the scheduler simply schedules this pod onto that host, assuming that it fits // resource requirements. @@ -1035,6 +1039,26 @@ type Service struct { Status ServiceStatus `json:"status,omitempty"` } +// ServiceAccount binds together: +// * a name, understood by users, and perhaps by peripheral systems, for an identity +// * a principal that can be authenticated and authorized +// * a set of secrets +type ServiceAccount struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount + Secrets []ObjectReference `json:"secrets"` +} + +// ServiceAccountList is a list of ServiceAccount objects +type ServiceAccountList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + Items []ServiceAccount `json:"items"` +} + // Endpoints is a collection of endpoints that implement the actual service. Example: // Name: "mysvc", // Subsets: [ diff --git a/pkg/api/v1/conversion.go b/pkg/api/v1/conversion.go index 032c5300786..53b2153c7a5 100644 --- a/pkg/api/v1/conversion.go +++ b/pkg/api/v1/conversion.go @@ -1878,6 +1878,7 @@ func init() { out.NodeSelector[key] = val } } + out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork return nil @@ -1911,6 +1912,7 @@ func init() { out.NodeSelector[key] = val } } + out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork return nil @@ -2475,6 +2477,74 @@ func init() { } return nil }, + func(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if in.Secrets != nil { + out.Secrets = make([]newer.ObjectReference, len(in.Secrets)) + for i := range in.Secrets { + if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil { + return err + } + } + } + return nil + }, + func(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if in.Secrets != nil { + out.Secrets = make([]ObjectReference, len(in.Secrets)) + for i := range in.Secrets { + if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil { + return err + } + } + } + return nil + }, + func(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]newer.ServiceAccount, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil + }, + func(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ServiceAccount, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil + }, func(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err diff --git a/pkg/api/v1/register.go b/pkg/api/v1/register.go index c2f971320f4..10d8423c604 100644 --- a/pkg/api/v1/register.go +++ b/pkg/api/v1/register.go @@ -52,6 +52,8 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &ServiceAccount{}, + &ServiceAccountList{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, @@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*ServiceAccount) IsAnAPIObject() {} +func (*ServiceAccountList) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 8c698af7560..74ebb5811c9 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -816,6 +816,9 @@ type PodSpec struct { // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` + // ServiceAccount is the name of the ServiceAccount to use to run this pod + ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"` + // Host is a request to schedule this pod onto a specific host. If it is non-empty, // the the scheduler simply schedules this pod onto that host, assuming that it fits // resource requirements. @@ -1036,6 +1039,26 @@ type ServiceList struct { Items []Service `json:"items" description:"list of services"` } +// ServiceAccount binds together: +// * a name, understood by users, and perhaps by peripheral systems, for an identity +// * a principal that can be authenticated and authorized +// * a set of secrets +type ServiceAccount struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + // Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount + Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"` +} + +// ServiceAccountList is a list of ServiceAccount objects +type ServiceAccountList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"` +} + // Endpoints is a collection of endpoints that implement the actual service. Example: // Name: "mysvc", // Subsets: [ diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index dba9b5d4689..77c6840451f 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -377,6 +377,7 @@ func init() { } out.DesiredState.Host = in.Spec.Host out.CurrentState.Host = in.Spec.Host + out.ServiceAccount = in.Spec.ServiceAccount if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil { return err } @@ -399,6 +400,7 @@ func init() { return err } out.Spec.Host = in.DesiredState.Host + out.Spec.ServiceAccount = in.ServiceAccount if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil { return err } @@ -503,6 +505,7 @@ func init() { return err } out.DesiredState.Host = in.Spec.Host + out.ServiceAccount = in.Spec.ServiceAccount if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil { return err } @@ -519,6 +522,7 @@ func init() { return err } out.Spec.Host = in.DesiredState.Host + out.Spec.ServiceAccount = in.ServiceAccount if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil { return err } @@ -1685,4 +1689,17 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "ServiceAccount", + func(label, value string) (string, string, error) { + switch label { + case "name": + return "metadata.name", value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 369db60ba58..870246e5d20 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -59,6 +59,8 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &ServiceAccount{}, + &ServiceAccountList{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, @@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*ServiceAccount) IsAnAPIObject() {} +func (*ServiceAccountList) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 90558708656..ee634c43027 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -755,6 +755,8 @@ type Pod struct { Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` + // ServiceAccount is the name of the ServiceAccount to use to run this pod + ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"` // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` } @@ -782,10 +784,11 @@ type ReplicationController struct { // PodTemplate holds the information used for creating pods. type PodTemplate struct { - DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` - NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` - Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` - Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` + DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` + ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"` + NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` + Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` } // Session Affinity Type string @@ -884,6 +887,24 @@ type ServicePort struct { ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` } +// ServiceAccount binds together: +// * a name, understood by users, and perhaps by peripheral systems, for an identity +// * a principal that can be authenticated and authorized +// * a set of secrets +type ServiceAccount struct { + TypeMeta `json:",inline"` + + // Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount + Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"` +} + +// ServiceAccountList is a list of ServiceAccount objects +type ServiceAccountList struct { + TypeMeta `json:",inline"` + + Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"` +} + // EndpointObjectReference is a reference to an object exposing the endpoint type EndpointObjectReference struct { Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index d4b1d59dffb..91b18b7dd28 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -180,6 +180,7 @@ func init() { } out.DesiredState.Host = in.Spec.Host out.CurrentState.Host = in.Spec.Host + out.ServiceAccount = in.Spec.ServiceAccount if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil { return err } @@ -201,6 +202,7 @@ func init() { if err := s.Convert(&in.DesiredState.Manifest, &out.Spec, 0); err != nil { return err } + out.Spec.ServiceAccount = in.ServiceAccount out.Spec.Host = in.DesiredState.Host if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil { return err @@ -282,6 +284,7 @@ func init() { return err } out.DesiredState.Host = in.Spec.Host + out.ServiceAccount = in.Spec.ServiceAccount if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil { return err } @@ -298,6 +301,7 @@ func init() { return err } out.Spec.Host = in.DesiredState.Host + out.Spec.ServiceAccount = in.ServiceAccount if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil { return err } @@ -1601,4 +1605,17 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "ServiceAccount", + func(label, value string) (string, string, error) { + switch label { + case "name": + return "metadata.name", value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 337015150cd..4182ae4419c 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -59,6 +59,8 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &ServiceAccount{}, + &ServiceAccountList{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, @@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*ServiceAccount) IsAnAPIObject() {} +func (*ServiceAccountList) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index b2345994bc9..c3978ed372f 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -756,6 +756,8 @@ type Pod struct { Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` + // ServiceAccount is the name of the ServiceAccount to use to run this pod + ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"` // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` } @@ -787,10 +789,11 @@ type ReplicationController struct { // // http://docs.k8s.io/replication-controller.md#pod-template type PodTemplate struct { - DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` - NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` - Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` - Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` + DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` + ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"` + NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` + Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` } // Session Affinity Type string @@ -891,6 +894,24 @@ type ServicePort struct { ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` } +// ServiceAccount binds together: +// * a name, understood by users, and perhaps by peripheral systems, for an identity +// * a principal that can be authenticated and authorized +// * a set of secrets +type ServiceAccount struct { + TypeMeta `json:",inline"` + + // Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount + Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"` +} + +// ServiceAccountList is a list of ServiceAccount objects +type ServiceAccountList struct { + TypeMeta `json:",inline"` + + Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"` +} + // EndpointObjectReference is a reference to an object exposing the endpoint type EndpointObjectReference struct { Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index bc0b3a17052..7a3659e4052 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -128,6 +128,19 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "ServiceAccount", + func(label, value string) (string, string, error) { + switch label { + case "metadata.name": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error { diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index 7f654794f09..8e2a9d3f43c 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -2637,6 +2637,7 @@ func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s c } else { out.NodeSelector = nil } + out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork return nil @@ -2682,6 +2683,7 @@ func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s c } else { out.NodeSelector = nil } + out.ServiceAccount = in.ServiceAccount out.Host = in.Host out.HostNetwork = in.HostNetwork return nil @@ -3623,6 +3625,98 @@ func convert_api_Service_To_v1beta3_Service(in *newer.Service, out *Service, s c return nil } +func convert_v1beta3_ServiceAccount_To_api_ServiceAccount(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ServiceAccount))(in) + } + if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1beta3_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if in.Secrets != nil { + out.Secrets = make([]newer.ObjectReference, len(in.Secrets)) + for i := range in.Secrets { + if err := convert_v1beta3_ObjectReference_To_api_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil { + return err + } + } + } else { + out.Secrets = nil + } + return nil +} + +func convert_api_ServiceAccount_To_v1beta3_ServiceAccount(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*newer.ServiceAccount))(in) + } + if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1beta3_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if in.Secrets != nil { + out.Secrets = make([]ObjectReference, len(in.Secrets)) + for i := range in.Secrets { + if err := convert_api_ObjectReference_To_v1beta3_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil { + return err + } + } + } else { + out.Secrets = nil + } + return nil +} + +func convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ServiceAccountList))(in) + } + if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1beta3_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]newer.ServiceAccount, len(in.Items)) + for i := range in.Items { + if err := convert_v1beta3_ServiceAccount_To_api_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*newer.ServiceAccountList))(in) + } + if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ListMeta_To_v1beta3_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ServiceAccount, len(in.Items)) + for i := range in.Items { + if err := convert_api_ServiceAccount_To_v1beta3_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func convert_v1beta3_ServiceList_To_api_ServiceList(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*ServiceList))(in) @@ -4242,6 +4336,8 @@ func init() { convert_api_Secret_To_v1beta3_Secret, convert_api_SecurityContext_To_v1beta3_SecurityContext, convert_api_SerializedReference_To_v1beta3_SerializedReference, + convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList, + convert_api_ServiceAccount_To_v1beta3_ServiceAccount, convert_api_ServiceList_To_v1beta3_ServiceList, convert_api_ServicePort_To_v1beta3_ServicePort, convert_api_ServiceSpec_To_v1beta3_ServiceSpec, @@ -4348,6 +4444,8 @@ func init() { convert_v1beta3_Secret_To_api_Secret, convert_v1beta3_SecurityContext_To_api_SecurityContext, convert_v1beta3_SerializedReference_To_api_SerializedReference, + convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList, + convert_v1beta3_ServiceAccount_To_api_ServiceAccount, convert_v1beta3_ServiceList_To_api_ServiceList, convert_v1beta3_ServicePort_To_api_ServicePort, convert_v1beta3_ServiceSpec_To_api_ServiceSpec, diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index 11dbda95740..83edfede61f 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -52,6 +52,8 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &ServiceAccount{}, + &ServiceAccountList{}, &PersistentVolume{}, &PersistentVolumeList{}, &PersistentVolumeClaim{}, @@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*ServiceAccount) IsAnAPIObject() {} +func (*ServiceAccountList) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 84d9d1d59ee..555a4513ff2 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -816,6 +816,9 @@ type PodSpec struct { // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` + // ServiceAccount is the name of the ServiceAccount to use to run this pod + ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"` + // Host is a request to schedule this pod onto a specific host. If it is non-empty, // the the scheduler simply schedules this pod onto that host, assuming that it fits // resource requirements. @@ -1036,6 +1039,26 @@ type ServiceList struct { Items []Service `json:"items" description:"list of services"` } +// ServiceAccount binds together: +// * a name, understood by users, and perhaps by peripheral systems, for an identity +// * a principal that can be authenticated and authorized +// * a set of secrets +type ServiceAccount struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + // Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount + Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"` +} + +// ServiceAccountList is a list of ServiceAccount objects +type ServiceAccountList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"` +} + // Endpoints is a collection of endpoints that implement the actual service. Example: // Name: "mysvc", // Subsets: [ diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index fa012e6a83b..c505b2e3b51 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -153,6 +153,13 @@ func ValidateSecretName(name string, prefix bool) (bool, string) { return nameIsDNSSubdomain(name, prefix) } +// ValidateServiceAccountName can be used to check whether the given service account name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +func ValidateServiceAccountName(name string, prefix bool) (bool, string) { + return nameIsDNSSubdomain(name, prefix) +} + // ValidateEndpointsName can be used to check whether the given endpoints name is valid. // Prefix indicates this name will be used as part of generation, in which case // trailing dashes are allowed. @@ -1225,6 +1232,21 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { return allErrs } +// ValidateServiceAccount tests if required fields in the ServiceAccount are set. +func ValidateServiceAccount(serviceAccount *api.ServiceAccount) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&serviceAccount.ObjectMeta, true, ValidateServiceAccountName).Prefix("metadata")...) + return allErrs +} + +// ValidateServiceAccountUpdate tests if required fields in the ServiceAccount are set. +func ValidateServiceAccountUpdate(oldServiceAccount, newServiceAccount *api.ServiceAccount) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldServiceAccount.ObjectMeta, &newServiceAccount.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateServiceAccount(newServiceAccount)...) + return allErrs +} + // ValidateSecret tests if required fields in the Secret are set. func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} diff --git a/pkg/client/client.go b/pkg/client/client.go index ef1294ad1af..96f0ad289cd 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -39,6 +39,7 @@ type Interface interface { EventNamespacer LimitRangesNamespacer ResourceQuotasNamespacer + ServiceAccountsNamespacer SecretsNamespacer NamespacesInterface PersistentVolumesInterface @@ -77,6 +78,10 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface { return newResourceQuotas(c, namespace) } +func (c *Client) ServiceAccounts(namespace string) ServiceAccountsInterface { + return newServiceAccounts(c, namespace) +} + func (c *Client) Secrets(namespace string) SecretsInterface { return newSecrets(c, namespace) } diff --git a/pkg/client/request.go b/pkg/client/request.go index 0c794a9b47d..89b44759a70 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -310,6 +310,9 @@ var fieldMappings = versionToResourceToFieldMapping{ "secrets": clientFieldNameToAPIVersionFieldName{ SecretType: "type", }, + "serviceAccounts": clientFieldNameToAPIVersionFieldName{ + ObjectNameField: "name", + }, }, "v1beta2": resourceTypeToFieldMapping{ "nodes": clientFieldNameToAPIVersionFieldName{ @@ -326,6 +329,9 @@ var fieldMappings = versionToResourceToFieldMapping{ "secrets": clientFieldNameToAPIVersionFieldName{ SecretType: "type", }, + "serviceAccounts": clientFieldNameToAPIVersionFieldName{ + ObjectNameField: "name", + }, }, "v1beta3": resourceTypeToFieldMapping{ "nodes": clientFieldNameToAPIVersionFieldName{ @@ -342,6 +348,9 @@ var fieldMappings = versionToResourceToFieldMapping{ "secrets": clientFieldNameToAPIVersionFieldName{ SecretType: "type", }, + "serviceAccounts": clientFieldNameToAPIVersionFieldName{ + ObjectNameField: "metadata.name", + }, }, } diff --git a/pkg/client/service_accounts.go b/pkg/client/service_accounts.go new file mode 100644 index 00000000000..96a98c4ec72 --- /dev/null +++ b/pkg/client/service_accounts.go @@ -0,0 +1,125 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 client + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +type ServiceAccountsNamespacer interface { + ServiceAccounts(namespace string) ServiceAccountsInterface +} + +type ServiceAccountsInterface interface { + Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) + Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) + Delete(name string) error + List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) + Get(name string) (*api.ServiceAccount, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) +} + +// serviceAccounts implements ServiceAccounts interface +type serviceAccounts struct { + client *Client + namespace string +} + +// newServiceAccounts returns a new serviceAccounts object. +func newServiceAccounts(c *Client, ns string) ServiceAccountsInterface { + return &serviceAccounts{ + client: c, + namespace: ns, + } +} + +func (s *serviceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) { + result := &api.ServiceAccount{} + err := s.client.Post(). + Namespace(s.namespace). + Resource("serviceAccounts"). + Body(serviceAccount). + Do(). + Into(result) + + return result, err +} + +// List returns a list of serviceAccounts matching the selectors. +func (s *serviceAccounts) List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) { + result := &api.ServiceAccountList{} + + err := s.client.Get(). + Namespace(s.namespace). + Resource("serviceAccounts"). + LabelsSelectorParam(label). + FieldsSelectorParam(field). + Do(). + Into(result) + + return result, err +} + +// Get returns the given serviceAccount, or an error. +func (s *serviceAccounts) Get(name string) (*api.ServiceAccount, error) { + result := &api.ServiceAccount{} + err := s.client.Get(). + Namespace(s.namespace). + Resource("serviceAccounts"). + Name(name). + Do(). + Into(result) + + return result, err +} + +// Watch starts watching for serviceAccounts matching the given selectors. +func (s *serviceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.client.Get(). + Prefix("watch"). + Namespace(s.namespace). + Resource("serviceAccounts"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(label). + FieldsSelectorParam(field). + Watch() +} + +func (s *serviceAccounts) Delete(name string) error { + return s.client.Delete(). + Namespace(s.namespace). + Resource("serviceAccounts"). + Name(name). + Do(). + Error() +} + +func (s *serviceAccounts) Update(serviceAccount *api.ServiceAccount) (result *api.ServiceAccount, err error) { + result = &api.ServiceAccount{} + err = s.client.Put(). + Namespace(s.namespace). + Resource("serviceAccounts"). + Name(serviceAccount.Name). + Body(serviceAccount). + Do(). + Into(result) + + return +} diff --git a/pkg/client/testclient/fake_service_accounts.go b/pkg/client/testclient/fake_service_accounts.go new file mode 100644 index 00000000000..9185c97284c --- /dev/null +++ b/pkg/client/testclient/fake_service_accounts.go @@ -0,0 +1,61 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 testclient + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// FakeServiceAccounts implements ServiceAccountsInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the method you want to test easier. +type FakeServiceAccounts struct { + Fake *Fake + Namespace string +} + +func (c *FakeServiceAccounts) List(labels labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "list-serviceaccounts"}, &api.ServiceAccountList{}) + return obj.(*api.ServiceAccountList), err +} + +func (c *FakeServiceAccounts) Get(name string) (*api.ServiceAccount, error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "get-serviceaccount", Value: name}, &api.ServiceAccount{}) + return obj.(*api.ServiceAccount), err +} + +func (c *FakeServiceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "create-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{}) + return obj.(*api.ServiceAccount), err +} + +func (c *FakeServiceAccounts) Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "update-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{}) + return obj.(*api.ServiceAccount), err +} + +func (c *FakeServiceAccounts) Delete(name string) error { + _, err := c.Fake.Invokes(FakeAction{Action: "delete-serviceaccount", Value: name}, &api.ServiceAccount{}) + return err +} + +func (c *FakeServiceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-serviceAccounts", Value: resourceVersion}) + return c.Fake.Watch, c.Fake.Err +} diff --git a/pkg/client/testclient/testclient.go b/pkg/client/testclient/testclient.go index 22246024142..74fe67037c4 100644 --- a/pkg/client/testclient/testclient.go +++ b/pkg/client/testclient/testclient.go @@ -107,6 +107,10 @@ func (c *Fake) Services(namespace string) client.ServiceInterface { return &FakeServices{Fake: c, Namespace: namespace} } +func (c *Fake) ServiceAccounts(namespace string) client.ServiceAccountsInterface { + return &FakeServiceAccounts{Fake: c, Namespace: namespace} +} + func (c *Fake) Secrets(namespace string) client.SecretsInterface { return &FakeSecrets{Fake: c, Namespace: namespace} } diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 5c391cc6be9..255721082d5 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -66,6 +66,7 @@ func describerMap(c *client.Client) map[string]Describer { "ReplicationController": &ReplicationControllerDescriber{c}, "Secret": &SecretDescriber{c}, "Service": &ServiceDescriber{c}, + "ServiceAccount": &ServiceAccountDescriber{c}, "Minion": &NodeDescriber{c}, "Node": &NodeDescriber{c}, "LimitRange": &LimitRangeDescriber{c}, @@ -510,6 +511,67 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api }) } +// ServiceAccountDescriber generates information about a service. +type ServiceAccountDescriber struct { + client.Interface +} + +func (d *ServiceAccountDescriber) Describe(namespace, name string) (string, error) { + c := d.ServiceAccounts(namespace) + + serviceAccount, err := c.Get(name) + if err != nil { + return "", err + } + + tokens := []api.Secret{} + + tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)}) + secrets, err := d.Secrets(namespace).List(labels.Everything(), tokenSelector) + if err == nil { + for _, s := range secrets.Items { + name, _ := s.Annotations[api.ServiceAccountNameKey] + uid, _ := s.Annotations[api.ServiceAccountUIDKey] + if name == serviceAccount.Name && uid == string(serviceAccount.UID) { + tokens = append(tokens, s) + } + } + } + + return describeServiceAccount(serviceAccount, tokens) +} + +func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Secret) (string, error) { + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", serviceAccount.Name) + fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(serviceAccount.Labels)) + + if len(serviceAccount.Secrets) == 0 { + fmt.Fprintf(out, "Secrets:\t\n") + } else { + prefix := "Secrets:" + for _, s := range serviceAccount.Secrets { + fmt.Fprintf(out, "%s\t%s\n", prefix, s) + prefix = " " + } + fmt.Fprintln(out) + } + + if len(tokens) == 0 { + fmt.Fprintf(out, "Tokens: \t\n") + } else { + prefix := "Tokens: " + for _, t := range tokens { + fmt.Fprintf(out, "%s\t%s\n", prefix, t.Name) + prefix = " " + } + fmt.Fprintln(out) + } + + return nil + }) +} + // NodeDescriber generates information about a node. type NodeDescriber struct { client.Interface diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 620efec2d8c..c3ca097b718 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -255,6 +255,7 @@ var limitRangeColumns = []string{"NAME"} var resourceQuotaColumns = []string{"NAME"} var namespaceColumns = []string{"NAME", "LABELS", "STATUS"} var secretColumns = []string{"NAME", "TYPE", "DATA"} +var serviceAccountColumns = []string{"NAME", "SECRETS"} var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"} var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"} var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"} @@ -283,6 +284,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(namespaceColumns, printNamespaceList) h.Handler(secretColumns, printSecret) h.Handler(secretColumns, printSecretList) + h.Handler(serviceAccountColumns, printServiceAccount) + h.Handler(serviceAccountColumns, printServiceAccountList) h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaim) h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList) h.Handler(persistentVolumeColumns, printPersistentVolume) @@ -596,6 +599,21 @@ func printSecretList(list *api.SecretList, w io.Writer) error { return nil } +func printServiceAccount(item *api.ServiceAccount, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%d\n", item.Name, len(item.Secrets)) + return err +} + +func printServiceAccountList(list *api.ServiceAccountList, w io.Writer) error { + for _, item := range list.Items { + if err := printServiceAccount(&item, w); err != nil { + return err + } + } + + return nil +} + func printNode(node *api.Node, w io.Writer) error { conditionMap := make(map[api.NodeConditionType]*api.NodeCondition) NodeAllConditions := []api.NodeConditionType{api.NodeReady} diff --git a/pkg/master/master.go b/pkg/master/master.go index 5fa1734ce7d..01dd8ee0a35 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -65,6 +65,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator" etcdipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/etcd" + serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/ui" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -398,6 +399,7 @@ func (m *Master) init(c *Config) { resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper) secretStorage := secretetcd.NewStorage(c.EtcdHelper) + serviceAccountStorage := serviceaccountetcd.NewStorage(c.EtcdHelper) persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewStorage(c.EtcdHelper) persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcetcd.NewStorage(c.EtcdHelper) @@ -449,6 +451,7 @@ func (m *Master) init(c *Config) { "namespaces/status": namespaceStatusStorage, "namespaces/finalize": namespaceFinalizeStorage, "secrets": secretStorage, + "serviceAccounts": serviceAccountStorage, "persistentVolumes": persistentVolumeStorage, "persistentVolumes/status": persistentVolumeStatusStorage, "persistentVolumeClaims": persistentVolumeClaimStorage, diff --git a/pkg/registry/serviceaccount/doc.go b/pkg/registry/serviceaccount/doc.go new file mode 100644 index 00000000000..40606205d30 --- /dev/null +++ b/pkg/registry/serviceaccount/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 serviceaccount provides a Registry interface and a strategy +// implementation for storing ServiceAccount API objects. +package serviceaccount diff --git a/pkg/registry/serviceaccount/etcd/etcd.go b/pkg/registry/serviceaccount/etcd/etcd.go new file mode 100644 index 00000000000..579b1b4f62b --- /dev/null +++ b/pkg/registry/serviceaccount/etcd/etcd.go @@ -0,0 +1,64 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 etcd + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// REST implements a RESTStorage for service accounts against etcd +type REST struct { + *etcdgeneric.Etcd +} + +const Prefix = "/serviceaccounts" + +// NewStorage returns a RESTStorage object that will work against service accounts objects. +func NewStorage(h tools.EtcdHelper) *REST { + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.ServiceAccount{} }, + NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} }, + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, Prefix) + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, Prefix, name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.ServiceAccount).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return serviceaccount.Matcher(label, field) + }, + EndpointName: "serviceaccounts", + + Helper: h, + } + + store.CreateStrategy = serviceaccount.Strategy + store.UpdateStrategy = serviceaccount.Strategy + store.ReturnDeletedObject = true + + return &REST{store} +} diff --git a/pkg/registry/serviceaccount/etcd/etcd_test.go b/pkg/registry/serviceaccount/etcd/etcd_test.go new file mode 100644 index 00000000000..9bdcb47d9c6 --- /dev/null +++ b/pkg/registry/serviceaccount/etcd/etcd_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 etcd + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.NewEtcdHelper(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix()) + return fakeEtcdClient, helper +} + +func validNewServiceAccount(name string) *api.ServiceAccount { + return &api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: api.NamespaceDefault, + }, + Secrets: []api.ObjectReference{}, + } +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewStorage(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + serviceAccount := validNewServiceAccount("foo") + serviceAccount.Name = "" + serviceAccount.GenerateName = "foo-" + test.TestCreate( + // valid + serviceAccount, + // invalid + &api.ServiceAccount{}, + &api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{Name: "name with spaces"}, + }, + ) +} + +func TestUpdate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewStorage(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + key := etcdtest.AddPrefix("serviceaccounts/default/foo") + + fakeEtcdClient.ExpectNotFoundGet(key) + fakeEtcdClient.ChangeIndex = 2 + serviceAccount := validNewServiceAccount("foo") + existing := validNewServiceAccount("exists") + obj, err := storage.Create(api.NewDefaultContext(), existing) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + older := obj.(*api.ServiceAccount) + older.ResourceVersion = "1" + + test.TestUpdate( + serviceAccount, + existing, + older, + ) +} diff --git a/pkg/registry/serviceaccount/registry.go b/pkg/registry/serviceaccount/registry.go new file mode 100644 index 00000000000..9c79813e6ee --- /dev/null +++ b/pkg/registry/serviceaccount/registry.go @@ -0,0 +1,87 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 serviceaccount + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// Registry is an interface implemented by things that know how to store ServiceAccount objects. +type Registry interface { + // ListServiceAccounts obtains a list of ServiceAccounts having labels which match selector. + ListServiceAccounts(ctx api.Context, selector labels.Selector) (*api.ServiceAccountList, error) + // Watch for new/changed/deleted service accounts + WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific ServiceAccount + GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error) + // Create a ServiceAccount based on a specification. + CreateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error + // Update an existing ServiceAccount + UpdateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error + // Delete an existing ServiceAccount + DeleteServiceAccount(ctx api.Context, name string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListServiceAccounts(ctx api.Context, label labels.Selector) (*api.ServiceAccountList, error) { + obj, err := s.List(ctx, label, fields.Everything()) + if err != nil { + return nil, err + } + return obj.(*api.ServiceAccountList), nil +} + +func (s *storage) WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error) { + obj, err := s.Get(ctx, name) + if err != nil { + return nil, err + } + return obj.(*api.ServiceAccount), nil +} + +func (s *storage) CreateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error { + _, err := s.Create(ctx, serviceAccount) + return err +} + +func (s *storage) UpdateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error { + _, _, err := s.Update(ctx, serviceAccount) + return err +} + +func (s *storage) DeleteServiceAccount(ctx api.Context, name string) error { + _, err := s.Delete(ctx, name, nil) + return err +} diff --git a/pkg/registry/serviceaccount/strategy.go b/pkg/registry/serviceaccount/strategy.go new file mode 100644 index 00000000000..3d7373aafce --- /dev/null +++ b/pkg/registry/serviceaccount/strategy.go @@ -0,0 +1,88 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 serviceaccount + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +// strategy implements behavior for ServiceAccount objects +type strategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating ServiceAccount +// objects via the REST API. +var Strategy = strategy{api.Scheme, api.SimpleNameGenerator} + +func (strategy) NamespaceScoped() bool { + return true +} + +func (strategy) PrepareForCreate(obj runtime.Object) { + cleanSecretReferences(obj.(*api.ServiceAccount)) +} + +func (strategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateServiceAccount(obj.(*api.ServiceAccount)) +} + +func (strategy) AllowCreateOnUpdate() bool { + return false +} + +func (strategy) PrepareForUpdate(obj, old runtime.Object) { + cleanSecretReferences(obj.(*api.ServiceAccount)) +} + +func cleanSecretReferences(serviceAccount *api.ServiceAccount) { + for i, secret := range serviceAccount.Secrets { + serviceAccount.Secrets[i] = api.ObjectReference{Name: secret.Name} + } +} + +func (strategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateServiceAccountUpdate(old.(*api.ServiceAccount), obj.(*api.ServiceAccount)) +} + +// Matcher returns a generic matcher for a given label and field selector. +func Matcher(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + sa, ok := obj.(*api.ServiceAccount) + if !ok { + return false, fmt.Errorf("not a serviceaccount") + } + fields := SelectableFields(sa) + return label.Matches(labels.Set(sa.Labels)) && field.Matches(fields), nil + }) +} + +// SelectableFields returns a label set that represents the object +func SelectableFields(obj *api.ServiceAccount) labels.Set { + return labels.Set{ + "metadata.name": obj.Name, + } +}