From fb001ada21710c87fc0af1cd18c8798303ea000a Mon Sep 17 00:00:00 2001 From: Paul Morie Date: Tue, 17 Feb 2015 20:24:50 -0500 Subject: [PATCH] Secret API resource --- pkg/api/register.go | 4 + pkg/api/testing/fuzzer.go | 6 + pkg/api/types.go | 32 ++++++ pkg/api/v1beta1/conversion.go | 6 + pkg/api/v1beta1/defaults.go | 5 + pkg/api/v1beta1/defaults_test.go | 10 ++ pkg/api/v1beta1/register.go | 4 + pkg/api/v1beta1/types.go | 27 +++++ pkg/api/v1beta2/conversion.go | 6 + pkg/api/v1beta2/defaults.go | 7 ++ pkg/api/v1beta2/defaults_test.go | 10 ++ pkg/api/v1beta2/register.go | 4 + pkg/api/v1beta2/types.go | 28 +++++ pkg/api/v1beta3/defaults.go | 5 + pkg/api/v1beta3/defaults_test.go | 10 ++ pkg/api/v1beta3/register.go | 4 + pkg/api/v1beta3/types.go | 31 ++++++ pkg/api/validation/validation.go | 43 ++++++++ pkg/api/validation/validation_test.go | 52 ++++++++- pkg/client/client.go | 5 + pkg/client/fake.go | 6 + pkg/client/fake_secrets.go | 60 ++++++++++ pkg/client/secrets.go | 140 ++++++++++++++++++++++++ pkg/kubectl/resource_printer.go | 18 +++ pkg/master/master.go | 3 + pkg/registry/secret/doc.go | 19 ++++ pkg/registry/secret/registry.go | 48 ++++++++ pkg/registry/secret/registry_test.go | 108 ++++++++++++++++++ pkg/registry/secret/rest.go | 152 ++++++++++++++++++++++++++ 29 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 pkg/client/fake_secrets.go create mode 100644 pkg/client/secrets.go create mode 100644 pkg/registry/secret/doc.go create mode 100644 pkg/registry/secret/registry.go create mode 100644 pkg/registry/secret/registry_test.go create mode 100644 pkg/registry/secret/rest.go diff --git a/pkg/api/register.go b/pkg/api/register.go index f640317948a..976b409c37a 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -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() {} diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 68818d945d5..bdd6b46d308 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -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 } diff --git a/pkg/api/types.go b/pkg/api/types.go index a9017bfc1e5..5af815c162d 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -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 diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 63edaeddc61..cc69d0293f8 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -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 }, diff --git a/pkg/api/v1beta1/defaults.go b/pkg/api/v1beta1/defaults.go index 19cb2e0228a..713458902c2 100644 --- a/pkg/api/v1beta1/defaults.go +++ b/pkg/api/v1beta1/defaults.go @@ -80,5 +80,10 @@ func init() { obj.TimeoutSeconds = 1 } }, + func(obj *Secret) { + if obj.Type == "" { + obj.Type = SecretTypeOpaque + } + }, ) } diff --git a/pkg/api/v1beta1/defaults_test.go b/pkg/api/v1beta1/defaults_test.go index 9115978d62f..bef10ab9016 100644 --- a/pkg/api/v1beta1/defaults_test.go +++ b/pkg/api/v1beta1/defaults_test.go @@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) { current.ProtocolTCP, container.Ports[0].Protocol) } } + +func TestSetDefaultSecret(t *testing.T) { + s := ¤t.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) + } +} diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 89a6ba88322..f61f806f08a 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -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() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 9a3c318f9b1..bd7b69e4d29 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -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"` +} diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 7e8b662a882..df63f91abe3 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -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 }, diff --git a/pkg/api/v1beta2/defaults.go b/pkg/api/v1beta2/defaults.go index 4f385884e08..ef0abf4100a 100644 --- a/pkg/api/v1beta2/defaults.go +++ b/pkg/api/v1beta2/defaults.go @@ -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 + } + }, ) } diff --git a/pkg/api/v1beta2/defaults_test.go b/pkg/api/v1beta2/defaults_test.go index 5808860ae6e..fb14920244d 100644 --- a/pkg/api/v1beta2/defaults_test.go +++ b/pkg/api/v1beta2/defaults_test.go @@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) { current.ProtocolTCP, container.Ports[0].Protocol) } } + +func TestSetDefaultSecret(t *testing.T) { + s := ¤t.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) + } +} diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index c803a01e42b..990aa7b2039 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -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() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 36bd2d9a975..c76f148f5dc 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -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"` +} diff --git a/pkg/api/v1beta3/defaults.go b/pkg/api/v1beta3/defaults.go index f312b0ca2c2..4abfa2c8178 100644 --- a/pkg/api/v1beta3/defaults.go +++ b/pkg/api/v1beta3/defaults.go @@ -75,5 +75,10 @@ func init() { obj.TimeoutSeconds = 1 } }, + func(obj *Secret) { + if obj.Type == "" { + obj.Type = SecretTypeOpaque + } + }, ) } diff --git a/pkg/api/v1beta3/defaults_test.go b/pkg/api/v1beta3/defaults_test.go index 49152fedd85..370e3f574c4 100644 --- a/pkg/api/v1beta3/defaults_test.go +++ b/pkg/api/v1beta3/defaults_test.go @@ -92,3 +92,13 @@ func TestSetDefaultContainer(t *testing.T) { current.ProtocolTCP, container.Ports[0].Protocol) } } + +func TestSetDefaultSecret(t *testing.T) { + s := ¤t.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) + } +} diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index 65106f9ce6b..93217e99d19 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -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() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 7135155808b..b7bb118844d 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -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"` +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 1e68c927bbc..e12b2a1e4b5 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -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())} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index ac3bd0b5801..5d28c23fab3 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -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) + } + } +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 273e77e4395..d12bebd741f 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -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) } diff --git a/pkg/client/fake.go b/pkg/client/fake.go index 28269cbebe1..e00ffb17c0b 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -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} } diff --git a/pkg/client/fake_secrets.go b/pkg/client/fake_secrets.go new file mode 100644 index 00000000000..284fff567fa --- /dev/null +++ b/pkg/client/fake_secrets.go @@ -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 +} diff --git a/pkg/client/secrets.go b/pkg/client/secrets.go new file mode 100644 index 00000000000..8dfd7ccdea4 --- /dev/null +++ b/pkg/client/secrets.go @@ -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 +} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index fc87eaef9d3..652103f0313 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -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} diff --git a/pkg/master/master.go b/pkg/master/master.go index 18f22fc9fa9..5ea92dc4cd9 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -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"} diff --git a/pkg/registry/secret/doc.go b/pkg/registry/secret/doc.go new file mode 100644 index 00000000000..1f28340fdf5 --- /dev/null +++ b/pkg/registry/secret/doc.go @@ -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 diff --git a/pkg/registry/secret/registry.go b/pkg/registry/secret/registry.go new file mode 100644 index 00000000000..4988c5c985f --- /dev/null +++ b/pkg/registry/secret/registry.go @@ -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, + }, + } +} diff --git a/pkg/registry/secret/registry_test.go b/pkg/registry/secret/registry_test.go new file mode 100644 index 00000000000..79a60130c55 --- /dev/null +++ b/pkg/registry/secret/registry_test.go @@ -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)) + } + } +} diff --git a/pkg/registry/secret/rest.go b/pkg/registry/secret/rest.go new file mode 100644 index 00000000000..65f21269e1e --- /dev/null +++ b/pkg/registry/secret/rest.go @@ -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{} +}