Merge pull request #4514 from pmorie/secret_resource

Secret API resource
pull/6/head
Eric Tune 2015-02-18 09:41:33 -08:00
commit 6c4c258e2c
29 changed files with 852 additions and 1 deletions

View File

@ -52,6 +52,8 @@ func init() {
&ResourceQuotaUsage{},
&Namespace{},
&NamespaceList{},
&Secret{},
&SecretList{},
)
// Legacy names are supported
Scheme.AddKnownTypeWithName("", "Minion", &Node{})
@ -85,3 +87,5 @@ func (*ResourceQuotaList) IsAnAPIObject() {}
func (*ResourceQuotaUsage) IsAnAPIObject() {}
func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {}

View File

@ -227,6 +227,12 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
c.Fuzz(&e.Count)
}
},
func(s *api.Secret, c fuzz.Continue) {
c.Fuzz(&s.TypeMeta)
c.Fuzz(&s.ObjectMeta)
s.Type = api.SecretTypeOpaque
},
)
return f
}

View File

@ -178,6 +178,8 @@ type VolumeSource struct {
GCEPersistentDisk *GCEPersistentDisk `json:"persistentDisk"`
// GitRepo represents a git repository at a particular revision.
GitRepo *GitRepo `json:"gitRepo"`
// Secret represents a secret that should populate this volume.
Secret *SecretSource `json:"secret"`
}
// HostPath represents bare host directory volume.
@ -228,6 +230,12 @@ type GitRepo struct {
// TODO: Consider credentials here.
}
// Adapts a Secret into a VolumeSource
type SecretSource struct {
// Reference to a Secret
Target ObjectReference `json:"target"`
}
// Port represents a network port in a single container
type Port struct {
// Optional: If specified, this must be a DNS_LABEL. Each named port
@ -1309,3 +1317,27 @@ type ResourceQuotaList struct {
// Items is a list of ResourceQuota objects
Items []ResourceQuota `json:"items"`
}
// Secret holds secret data of a certain type
type Secret struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty"`
Data map[string][]byte `json:"data,omitempty"`
Type SecretType `json:"type,omitempty"`
}
type SecretType string
const (
SecretTypeOpaque SecretType = "opaque" // Default; arbitrary user-defined data
)
type SecretList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty"`
Items []Secret `json:"items"`
}
const MaxSecretSize = 1 * 1024 * 1024

View File

@ -1028,6 +1028,9 @@ func init() {
if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil {
return err
}
if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil {
return err
}
return nil
},
func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error {
@ -1043,6 +1046,9 @@ func init() {
if err := s.Convert(&in.HostDir, &out.HostPath, 0); err != nil {
return err
}
if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil {
return err
}
return nil
},

View File

@ -80,5 +80,10 @@ func init() {
obj.TimeoutSeconds = 1
}
},
func(obj *Secret) {
if obj.Type == "" {
obj.Type = SecretTypeOpaque
}
},
)
}

View File

@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) {
current.ProtocolTCP, container.Ports[0].Protocol)
}
}
func TestSetDefaultSecret(t *testing.T) {
s := &current.Secret{}
obj2 := roundTrip(t, runtime.Object(s))
s2 := obj2.(*current.Secret)
if s2.Type != current.SecretTypeOpaque {
t.Errorf("Expected secret type %v, got %v", current.SecretTypeOpaque, s2.Type)
}
}

View File

@ -53,6 +53,8 @@ func init() {
&ResourceQuotaUsage{},
&Namespace{},
&NamespaceList{},
&Secret{},
&SecretList{},
)
// Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{})
@ -86,3 +88,5 @@ func (*ResourceQuotaList) IsAnAPIObject() {}
func (*ResourceQuotaUsage) IsAnAPIObject() {}
func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {}

View File

@ -103,6 +103,8 @@ type VolumeSource struct {
GCEPersistentDisk *GCEPersistentDisk `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"`
// GitRepo represents a git repository at a particular revision.
GitRepo *GitRepo `json:"gitRepo" description:"git repository at a particular revision"`
// Secret represents a secret to populate the volume with
Secret *SecretSource `json:"secret" description:"secret to populate volume with"`
}
// HostPath represents bare host directory volume.
@ -153,6 +155,12 @@ type GitRepo struct {
Revision string `json:"revision" description:"commit hash for the specified revision"`
}
// Adapts a Secret into a VolumeSource
type SecretSource struct {
// Reference to a Secret
Target ObjectReference `json:"target"`
}
// Port represents a network port in a single container
type Port struct {
// Optional: If specified, this must be a DNS_LABEL. Each named port
@ -1091,3 +1099,22 @@ type ResourceQuotaList struct {
// Items is a list of ResourceQuota objects
Items []ResourceQuota `json:"items"`
}
type Secret struct {
TypeMeta `json:",inline"`
Data map[string][]byte `json:"data,omitempty"`
Type SecretType `json:"type,omitempty"`
}
type SecretType string
const (
SecretTypeOpaque SecretType = "opaque" // Default; arbitrary user-defined data
)
type SecretList struct {
TypeMeta `json:",inline"`
Items []Secret `json:"items"`
}

View File

@ -943,6 +943,9 @@ func init() {
if err := s.Convert(&in.HostPath, &out.HostDir, 0); err != nil {
return err
}
if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil {
return err
}
return nil
},
func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error {
@ -958,6 +961,9 @@ func init() {
if err := s.Convert(&in.HostDir, &out.HostPath, 0); err != nil {
return err
}
if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil {
return err
}
return nil
},

View File

@ -21,12 +21,14 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/golang/glog"
)
func init() {
api.Scheme.AddDefaultingFuncs(
func(obj *Volume) {
if util.AllPtrFieldsNil(&obj.Source) {
glog.Errorf("Defaulting volume source for %v", obj)
obj.Source = VolumeSource{
EmptyDir: &EmptyDir{},
}
@ -80,5 +82,10 @@ func init() {
obj.TimeoutSeconds = 1
}
},
func(obj *Secret) {
if obj.Type == "" {
obj.Type = SecretTypeOpaque
}
},
)
}

View File

@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) {
current.ProtocolTCP, container.Ports[0].Protocol)
}
}
func TestSetDefaultSecret(t *testing.T) {
s := &current.Secret{}
obj2 := roundTrip(t, runtime.Object(s))
s2 := obj2.(*current.Secret)
if s2.Type != current.SecretTypeOpaque {
t.Errorf("Expected secret type %v, got %v", current.SecretTypeOpaque, s2.Type)
}
}

View File

@ -53,6 +53,8 @@ func init() {
&ResourceQuotaUsage{},
&Namespace{},
&NamespaceList{},
&Secret{},
&SecretList{},
)
// Future names are supported
api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{})
@ -86,3 +88,5 @@ func (*ResourceQuotaList) IsAnAPIObject() {}
func (*ResourceQuotaUsage) IsAnAPIObject() {}
func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {}

View File

@ -72,6 +72,8 @@ type VolumeSource struct {
GCEPersistentDisk *GCEPersistentDisk `json:"persistentDisk" description:"GCE disk resource attached to the host machine on demand"`
// GitRepo represents a git repository at a particular revision.
GitRepo *GitRepo `json:"gitRepo" description:"git repository at a particular revision"`
// Secret is a secret to populate the volume with
Secret *SecretSource `json:"secret" description:"secret to populate volume"`
}
// HostPath represents bare host directory volume.
@ -81,6 +83,12 @@ type HostPath struct {
type EmptyDir struct{}
// Adapts a Secret into a VolumeSource
type SecretSource struct {
// Reference to a Secret
Target ObjectReference `json:"target"`
}
// Protocol defines network protocols supported for things like conatiner ports.
type Protocol string
@ -1094,3 +1102,23 @@ type ResourceQuotaList struct {
// Items is a list of ResourceQuota objects
Items []ResourceQuota `json:"items"`
}
// Secret holds secret data of a certain type
type Secret struct {
TypeMeta `json:",inline"`
Data map[string][]byte `json:"data,omitempty"`
Type SecretType `json:"type,omitempty"`
}
type SecretType string
const (
SecretTypeOpaque SecretType = "opaque" // Default; arbitrary user-defined data
)
type SecretList struct {
TypeMeta `json:",inline"`
Items []Secret `json:"items"`
}

View File

@ -75,5 +75,10 @@ func init() {
obj.TimeoutSeconds = 1
}
},
func(obj *Secret) {
if obj.Type == "" {
obj.Type = SecretTypeOpaque
}
},
)
}

View File

@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) {
current.ProtocolTCP, container.Ports[0].Protocol)
}
}
func TestSetDefaultSecret(t *testing.T) {
s := &current.Secret{}
obj2 := roundTrip(t, runtime.Object(s))
s2 := obj2.(*current.Secret)
if s2.Type != current.SecretTypeOpaque {
t.Errorf("Expected secret type %v, got %v", current.SecretTypeOpaque, s2.Type)
}
}

View File

@ -53,6 +53,8 @@ func init() {
&ResourceQuotaUsage{},
&Namespace{},
&NamespaceList{},
&Secret{},
&SecretList{},
)
// Legacy names are supported
api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{})
@ -86,3 +88,5 @@ func (*ResourceQuotaList) IsAnAPIObject() {}
func (*ResourceQuotaUsage) IsAnAPIObject() {}
func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {}

View File

@ -197,6 +197,8 @@ type VolumeSource struct {
GCEPersistentDisk *GCEPersistentDisk `json:"gcePersistentDisk"`
// GitRepo represents a git repository at a particular revision.
GitRepo *GitRepo `json:"gitRepo"`
// Secret represents a secret that should populate this volume.
Secret *SecretSource `json:"secret"`
}
// HostPath represents bare host directory volume.
@ -246,6 +248,12 @@ type GitRepo struct {
Revision string `json:"revision"`
}
// Adapts a Secret into a VolumeSource
type SecretSource struct {
// Reference to a Secret
Target ObjectReference `json:"target"`
}
// Port represents a network port in a single container.
type Port struct {
// Optional: If specified, this must be a DNS_LABEL. Each named port
@ -1234,3 +1242,26 @@ type ResourceQuotaList struct {
// Items is a list of ResourceQuota objects
Items []ResourceQuota `json:"items"`
}
// Secret holds mappings between paths and secret data
// TODO: shouldn't "Secret" be a plural?
type Secret struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty"`
Data map[string][]byte `json:"data,omitempty"`
Type SecretType `json:"type,omitempty"`
}
type SecretType string
const (
SecretTypeOpaque SecretType = "opaque" // Default; arbitrary user-defined data
)
type SecretList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty"`
Items []Secret `json:"items"`
}

View File

@ -247,6 +247,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList {
numVolumes++
allErrs = append(allErrs, validateGCEPersistentDisk(source.GCEPersistentDisk).Prefix("persistentDisk")...)
}
if source.Secret != nil {
numVolumes++
allErrs = append(allErrs, validateSecretSource(source.Secret).Prefix("secret")...)
}
if numVolumes != 1 {
allErrs = append(allErrs, errs.NewFieldInvalid("", source, "exactly 1 volume type is required"))
}
@ -283,6 +287,20 @@ func validateGCEPersistentDisk(PD *api.GCEPersistentDisk) errs.ValidationErrorLi
return allErrs
}
func validateSecretSource(secretSource *api.SecretSource) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if secretSource.Target.Name == "" {
allErrs = append(allErrs, errs.NewFieldRequired("target.name", ""))
}
if secretSource.Target.Namespace == "" {
allErrs = append(allErrs, errs.NewFieldRequired("target.namespace", ""))
}
if secretSource.Target.Kind != "Secret" {
allErrs = append(allErrs, errs.NewFieldInvalid("target.kind", secretSource.Target.Kind, "Secret"))
}
return allErrs
}
var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP))
func validatePorts(ports []api.Port) errs.ValidationErrorList {
@ -820,6 +838,31 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList {
return allErrs
}
// ValidateSecret tests if required fields in the Secret are set.
func ValidateSecret(secret *api.Secret) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if len(secret.Name) == 0 {
allErrs = append(allErrs, errs.NewFieldRequired("name", secret.Name))
} else if !util.IsDNSSubdomain(secret.Name) {
allErrs = append(allErrs, errs.NewFieldInvalid("name", secret.Name, ""))
}
if len(secret.Namespace) == 0 {
allErrs = append(allErrs, errs.NewFieldRequired("namespace", secret.Namespace))
} else if !util.IsDNSSubdomain(secret.Namespace) {
allErrs = append(allErrs, errs.NewFieldInvalid("namespace", secret.Namespace, ""))
}
totalSize := 0
for _, value := range secret.Data {
totalSize += len(value)
}
if totalSize > api.MaxSecretSize {
allErrs = append(allErrs, errs.NewFieldForbidden("data", "Maximum secret size exceeded"))
}
return allErrs
}
func validateBasicResource(quantity resource.Quantity) errs.ValidationErrorList {
if quantity.Value() < 0 {
return errs.ValidationErrorList{fmt.Errorf("%v is not a valid resource quantity", quantity.Value())}

View File

@ -153,12 +153,13 @@ func TestValidateVolumes(t *testing.T) {
{Name: "empty", Source: api.VolumeSource{EmptyDir: &api.EmptyDir{}}},
{Name: "gcepd", Source: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDisk{"my-PD", "ext4", 1, false}}},
{Name: "gitrepo", Source: api.VolumeSource{GitRepo: &api.GitRepo{"my-repo", "hashstring"}}},
{Name: "secret", Source: api.VolumeSource{Secret: &api.SecretSource{api.ObjectReference{Namespace: api.NamespaceDefault, Name: "my-secret", Kind: "Secret"}}}},
}
names, errs := validateVolumes(successCase)
if len(errs) != 0 {
t.Errorf("expected success: %v", errs)
}
if len(names) != 6 || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo") {
if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret") {
t.Errorf("wrong names result: %v", names)
}
emptyVS := api.VolumeSource{EmptyDir: &api.EmptyDir{}}
@ -2490,3 +2491,52 @@ func TestValidateNamespaceUpdate(t *testing.T) {
}
}
}
func TestValidateSecret(t *testing.T) {
validSecret := func() api.Secret {
return api.Secret{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
Data: map[string][]byte{
"foo": []byte("bar"),
},
}
}
var (
emptyName = validSecret()
invalidName = validSecret()
emptyNs = validSecret()
invalidNs = validSecret()
overMaxSize = validSecret()
)
emptyName.Name = ""
invalidName.Name = "NoUppercaseOrSpecialCharsLike=Equals"
emptyNs.Namespace = ""
invalidNs.Namespace = "NoUppercaseOrSpecialCharsLike=Equals"
overMaxSize.Data = map[string][]byte{
"over": make([]byte, api.MaxSecretSize+1),
}
tests := map[string]struct {
secret api.Secret
valid bool
}{
"valid": {validSecret(), true},
"empty name": {emptyName, false},
"invalid name": {invalidName, false},
"empty namespace": {emptyNs, false},
"invalid namespace": {invalidNs, false},
"over max size": {overMaxSize, false},
}
for name, tc := range tests {
errs := ValidateSecret(&tc.secret)
if tc.valid && len(errs) > 0 {
t.Errorf("%v: Unexpected error: %v", name, errs)
}
if !tc.valid && len(errs) == 0 {
t.Errorf("%v: Unexpected non-error", name)
}
}
}

View File

@ -40,6 +40,7 @@ type Interface interface {
LimitRangesNamespacer
ResourceQuotasNamespacer
ResourceQuotaUsagesNamespacer
SecretsNamespacer
NamespacesInterface
}
@ -79,6 +80,10 @@ func (c *Client) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterfa
return newResourceQuotaUsages(c, namespace)
}
func (c *Client) Secrets(namespace string) SecretsInterface {
return newSecrets(c, namespace)
}
func (c *Client) Namespaces() NamespaceInterface {
return newNamespaces(c)
}

View File

@ -45,6 +45,8 @@ type Fake struct {
LimitRangesList api.LimitRangeList
ResourceQuotasList api.ResourceQuotaList
NamespacesList api.NamespaceList
SecretList api.SecretList
Secret api.Secret
Err error
Watch watch.Interface
}
@ -85,6 +87,10 @@ func (c *Fake) Services(namespace string) ServiceInterface {
return &FakeServices{Fake: c, Namespace: namespace}
}
func (c *Fake) Secrets(namespace string) SecretsInterface {
return &FakeSecrets{Fake: c, Namespace: namespace}
}
func (c *Fake) Namespaces() NamespaceInterface {
return &FakeNamespaces{Fake: c}
}

View File

@ -0,0 +1,60 @@
/*
Copyright 2014 Google Inc. 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/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
// Fake implements SecretInterface. 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 FakeSecrets struct {
Fake *Fake
Namespace string
}
func (c *FakeSecrets) List(labels, fields labels.Selector) (*api.SecretList, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-secrets"})
return &c.Fake.SecretList, c.Fake.Err
}
func (c *FakeSecrets) Get(name string) (*api.Secret, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-secret", Value: name})
return api.Scheme.CopyOrDie(&c.Fake.Secret).(*api.Secret), nil
}
func (c *FakeSecrets) Create(secret *api.Secret) (*api.Secret, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-secret", Value: secret})
return &api.Secret{}, nil
}
func (c *FakeSecrets) Update(secret *api.Secret) (*api.Secret, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-secret", Value: secret})
return &api.Secret{}, nil
}
func (c *FakeSecrets) Delete(secret string) error {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-secret", Value: secret})
return nil
}
func (c *FakeSecrets) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-secrets", Value: resourceVersion})
return c.Fake.Watch, c.Fake.Err
}

140
pkg/client/secrets.go Normal file
View File

@ -0,0 +1,140 @@
/*
Copyright 2015 Google Inc. 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 (
"errors"
"fmt"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
type SecretsNamespacer interface {
Secrets(namespace string) SecretsInterface
}
type SecretsInterface interface {
Create(secret *api.Secret) (*api.Secret, error)
Update(secret *api.Secret) (*api.Secret, error)
Delete(name string) error
List(label, field labels.Selector) (*api.SecretList, error)
Get(name string) (*api.Secret, error)
Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error)
}
// events implements Secrets interface
type secrets struct {
client *Client
namespace string
}
// newSecrets returns a new secrets object.
func newSecrets(c *Client, ns string) *secrets {
return &secrets{
client: c,
namespace: ns,
}
}
func (s *secrets) Create(secret *api.Secret) (*api.Secret, error) {
if s.namespace != "" && secret.Namespace != s.namespace {
return nil, fmt.Errorf("can't create a secret with namespace '%v' in namespace '%v'", secret.Namespace, s.namespace)
}
result := &api.Secret{}
err := s.client.Post().
Namespace(secret.Namespace).
Resource("secrets").
Body(secret).
Do().
Into(result)
return result, err
}
// List returns a list of secrets matching the selectors.
func (s *secrets) List(label, field labels.Selector) (*api.SecretList, error) {
result := &api.SecretList{}
err := s.client.Get().
Namespace(s.namespace).
Resource("secrets").
SelectorParam("labels", label).
SelectorParam("fields", field).
Do().
Into(result)
return result, err
}
// Get returns the given secret, or an error.
func (s *secrets) Get(name string) (*api.Secret, error) {
if len(name) == 0 {
return nil, errors.New("name is required parameter to Get")
}
result := &api.Secret{}
err := s.client.Get().
Namespace(s.namespace).
Resource("secrets").
Name(name).
Do().
Into(result)
return result, err
}
// Watch starts watching for secrets matching the given selectors.
func (s *secrets) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
return s.client.Get().
Prefix("watch").
Namespace(s.namespace).
Resource("secrets").
Param("resourceVersion", resourceVersion).
SelectorParam("labels", label).
SelectorParam("fields", field).
Watch()
}
func (s *secrets) Delete(name string) error {
return s.client.Delete().
Namespace(s.namespace).
Resource("secrets").
Name(name).
Do().
Error()
}
func (s *secrets) Update(secret *api.Secret) (result *api.Secret, err error) {
result = &api.Secret{}
if len(secret.ResourceVersion) == 0 {
err = fmt.Errorf("invalid update object, missing resource version: %v", secret)
return
}
err = s.client.Put().
Namespace(s.namespace).
Resource("secrets").
Name(secret.Name).
Body(secret).
Do().
Into(result)
return
}

View File

@ -225,6 +225,7 @@ var eventColumns = []string{"FIRSTSEEN", "LASTSEEN", "COUNT", "NAME", "KIND", "S
var limitRangeColumns = []string{"NAME"}
var resourceQuotaColumns = []string{"NAME"}
var namespaceColumns = []string{"NAME", "LABELS"}
var secretColumns = []string{"NAME", "DATA"}
// addDefaultHandlers adds print handlers for default Kubernetes types.
func (h *HumanReadablePrinter) addDefaultHandlers() {
@ -246,6 +247,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() {
h.Handler(resourceQuotaColumns, printResourceQuotaList)
h.Handler(namespaceColumns, printNamespace)
h.Handler(namespaceColumns, printNamespaceList)
h.Handler(secretColumns, printSecret)
h.Handler(secretColumns, printSecretList)
}
func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error {
@ -383,6 +386,21 @@ func printNamespaceList(list *api.NamespaceList, w io.Writer) error {
return nil
}
func printSecret(item *api.Secret, w io.Writer) error {
_, err := fmt.Fprintf(w, "%s\t%v\n", item.Name, len(item.Data))
return err
}
func printSecretList(list *api.SecretList, w io.Writer) error {
for _, item := range list.Items {
if err := printSecret(&item, w); err != nil {
return err
}
}
return nil
}
func printMinion(minion *api.Node, w io.Writer) error {
conditionMap := make(map[api.NodeConditionKind]*api.NodeCondition)
NodeAllConditions := []api.NodeConditionKind{api.NodeReady, api.NodeReachable}

View File

@ -54,6 +54,7 @@ import (
podetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
@ -372,6 +373,7 @@ func (m *Master) init(c *Config) {
eventRegistry := event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds()))
limitRangeRegistry := limitrange.NewEtcdRegistry(c.EtcdHelper)
resourceQuotaRegistry := resourcequota.NewEtcdRegistry(c.EtcdHelper)
secretRegistry := secret.NewEtcdRegistry(c.EtcdHelper)
m.namespaceRegistry = namespace.NewEtcdRegistry(c.EtcdHelper)
// TODO: split me up into distinct storage registries
@ -411,6 +413,7 @@ func (m *Master) init(c *Config) {
"resourceQuotas": resourcequota.NewREST(resourceQuotaRegistry),
"resourceQuotaUsages": resourcequotausage.NewREST(resourceQuotaRegistry),
"namespaces": namespace.NewREST(m.namespaceRegistry),
"secrets": secret.NewREST(secretRegistry),
}
apiVersions := []string{"v1beta1", "v1beta2"}

View File

@ -0,0 +1,19 @@
/*
Copyright 2015 Google Inc. 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 secrets provides Registry interface and its REST
// implementation for storing Secret api objects.
package secret

View File

@ -0,0 +1,48 @@
/*
Copyright 2015 Google Inc. 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 secret
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
)
// registry implements custom changes to generic.Etcd.
type registry struct {
*etcdgeneric.Etcd
}
// NewEtcdRegistry returns a registry which will store Secret in the given helper
func NewEtcdRegistry(h tools.EtcdHelper) generic.Registry {
return registry{
Etcd: &etcdgeneric.Etcd{
NewFunc: func() runtime.Object { return &api.Secret{} },
NewListFunc: func() runtime.Object { return &api.SecretList{} },
EndpointName: "secrets",
KeyRootFunc: func(ctx api.Context) string {
return etcdgeneric.NamespaceKeyRootFunc(ctx, "/registry/secrets")
},
KeyFunc: func(ctx api.Context, id string) (string, error) {
return etcdgeneric.NamespaceKeyFunc(ctx, "/registry/secrets", id)
},
Helper: h,
},
}
}

View File

@ -0,0 +1,108 @@
/*
Copyright 2014 Google Inc. 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 secret
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
)
func NewTestSecretEtcdRegistry(t *testing.T) (*tools.FakeEtcdClient, generic.Registry) {
f := tools.NewFakeEtcdClient(t)
f.TestIndex = true
h := tools.EtcdHelper{f, testapi.Codec(), tools.RuntimeVersionAdapter{testapi.MetadataAccessor()}}
return f, NewEtcdRegistry(h)
}
func TestSecretCreate(t *testing.T) {
secret := &api.Secret{
ObjectMeta: api.ObjectMeta{
Name: "abc",
Namespace: "foo",
},
Data: map[string][]byte{
"data-1": []byte("value-1"),
},
}
nodeWithSecret := tools.EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Value: runtime.EncodeOrDie(testapi.Codec(), secret),
ModifiedIndex: 1,
CreatedIndex: 1,
},
},
E: nil,
}
emptyNode := tools.EtcdResponseWithError{
R: &etcd.Response{},
E: tools.EtcdErrorNotFound,
}
ctx := api.NewDefaultContext()
key := "foo"
path, err := etcdgeneric.NamespaceKeyFunc(ctx, "/registry/secrets", key)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
table := map[string]struct {
existing tools.EtcdResponseWithError
expect tools.EtcdResponseWithError
toCreate runtime.Object
errOK func(error) bool
}{
"normal": {
existing: emptyNode,
expect: nodeWithSecret,
toCreate: secret,
errOK: func(err error) bool { return err == nil },
},
"preExisting": {
existing: nodeWithSecret,
expect: nodeWithSecret,
toCreate: secret,
errOK: errors.IsAlreadyExists,
},
}
for name, item := range table {
fakeClient, registry := NewTestSecretEtcdRegistry(t)
fakeClient.Data[path] = item.existing
err := registry.CreateWithName(ctx, key, item.toCreate)
if !item.errOK(err) {
t.Errorf("%v: unexpected error: %v", name, err)
}
if e, a := item.expect, fakeClient.Data[path]; !reflect.DeepEqual(e, a) {
t.Errorf("%v:\n%s", name, util.ObjectDiff(e, a))
}
}
}

152
pkg/registry/secret/rest.go Normal file
View File

@ -0,0 +1,152 @@
/*
Copyright 2015 Google Inc. 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 secret
import (
"fmt"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
"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"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
// REST provides the RESTStorage access patterns to work with Secret objects.
type REST struct {
registry generic.Registry
}
// NewREST returns a new REST. You must use a registry created by
// NewEtcdRegistry unless you're testing.
func NewREST(registry generic.Registry) *REST {
return &REST{
registry: registry,
}
}
// Create a Secret object
func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) {
secret, ok := obj.(*api.Secret)
if !ok {
return nil, fmt.Errorf("invalid object type")
}
if !api.ValidNamespace(ctx, &secret.ObjectMeta) {
return nil, errors.NewConflict("secret", secret.Namespace, fmt.Errorf("Secret.Namespace does not match the provided context"))
}
if len(secret.Name) == 0 {
secret.Name = string(util.NewUUID())
}
if errs := validation.ValidateSecret(secret); len(errs) > 0 {
return nil, errors.NewInvalid("secret", secret.Name, errs)
}
api.FillObjectMetaSystemFields(ctx, &secret.ObjectMeta)
err := rs.registry.CreateWithName(ctx, secret.Name, secret)
if err != nil {
return nil, err
}
return rs.registry.Get(ctx, secret.Name)
}
// Update updates a Secret object.
func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, error) {
secret, ok := obj.(*api.Secret)
if !ok {
return nil, fmt.Errorf("not a secret: %#v", obj)
}
if !api.ValidNamespace(ctx, &secret.ObjectMeta) {
return nil, errors.NewConflict("secret", secret.Namespace, fmt.Errorf("Secret.Namespace does not match the provided context"))
}
oldObj, err := rs.registry.Get(ctx, secret.Name)
if err != nil {
return nil, err
}
editSecret := oldObj.(*api.Secret)
// set the editable fields on the existing object
editSecret.Labels = secret.Labels
editSecret.ResourceVersion = secret.ResourceVersion
editSecret.Annotations = secret.Annotations
if errs := validation.ValidateSecret(editSecret); len(errs) > 0 {
return nil, errors.NewInvalid("secret", editSecret.Name, errs)
}
err = rs.registry.UpdateWithName(ctx, editSecret.Name, editSecret)
if err != nil {
return nil, err
}
return rs.registry.Get(ctx, editSecret.Name)
}
// Delete deletes the Secret with the specified name
func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) {
obj, err := rs.registry.Get(ctx, name)
if err != nil {
return nil, err
}
_, ok := obj.(*api.Secret)
if !ok {
return nil, fmt.Errorf("invalid object type")
}
return rs.registry.Delete(ctx, name)
}
// Get gets a Secret with the specified name
func (rs *REST) Get(ctx api.Context, name string) (runtime.Object, error) {
obj, err := rs.registry.Get(ctx, name)
if err != nil {
return nil, err
}
secret, ok := obj.(*api.Secret)
if !ok {
return nil, fmt.Errorf("invalid object type")
}
return secret, err
}
func (rs *REST) getAttrs(obj runtime.Object) (objLabels, objFields labels.Set, err error) {
return labels.Set{}, labels.Set{}, nil
}
func (rs *REST) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) {
return rs.registry.List(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs})
}
func (rs *REST) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) {
return rs.registry.Watch(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}, resourceVersion)
}
// New returns a new api.Secret
func (*REST) New() runtime.Object {
return &api.Secret{}
}
func (*REST) NewList() runtime.Object {
return &api.SecretList{}
}