Skip to content

Commit c8f88a3

Browse files
committed
Add support for creating a bulk of resources via kubectl apply
1 parent d6e36a4 commit c8f88a3

File tree

5 files changed

+501
-0
lines changed

5 files changed

+501
-0
lines changed

pkg/config/config.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2014 Google Inc. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package config
18+
19+
import (
20+
errs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
21+
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
22+
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
23+
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
24+
)
25+
26+
// ClientFunc returns the RESTClient defined for given resource
27+
type ClientFunc func(mapping *meta.RESTMapping) (*client.RESTClient, error)
28+
29+
// ApplyItems creates bulk of resources provided by items list. Each item must
30+
// be valid API type. It requires ObjectTyper to parse the Version and Kind and
31+
// RESTMapper to get the resource URI and REST client that knows how to create
32+
// given type
33+
func CreateObjects(typer runtime.ObjectTyper, mapper meta.RESTMapper, clientFor ClientFunc, objects []runtime.Object) errs.ValidationErrorList {
34+
allErrors := errs.ValidationErrorList{}
35+
for i, obj := range objects {
36+
version, kind, err := typer.ObjectVersionAndKind(obj)
37+
if err != nil {
38+
reportError(&allErrors, i, errs.NewFieldInvalid("kind", obj))
39+
continue
40+
}
41+
42+
mapping, err := mapper.RESTMapping(version, kind)
43+
if err != nil {
44+
reportError(&allErrors, i, errs.NewFieldNotSupported("mapping", err))
45+
continue
46+
}
47+
48+
client, err := clientFor(mapping)
49+
if err != nil {
50+
reportError(&allErrors, i, errs.NewFieldNotSupported("client", obj))
51+
continue
52+
}
53+
54+
if err := CreateObject(client, mapping, obj); err != nil {
55+
reportError(&allErrors, i, *err)
56+
}
57+
}
58+
59+
return allErrors.Prefix("Config")
60+
}
61+
62+
// Apply creates the obj using the provided clients and the resource URI
63+
// mapping. It reports ValidationError when the object is missing the Metadata
64+
// or the Name and it will report any error occured during create REST call
65+
func CreateObject(client *client.RESTClient, mapping *meta.RESTMapping, obj runtime.Object) *errs.ValidationError {
66+
name, err := mapping.MetadataAccessor.Name(obj)
67+
if err != nil || name == "" {
68+
e := errs.NewFieldRequired("name", err)
69+
return &e
70+
}
71+
72+
namespace, err := mapping.Namespace(obj)
73+
if err != nil {
74+
e := errs.NewFieldRequired("namespace", err)
75+
return &e
76+
}
77+
78+
// TODO: This should be using RESTHelper
79+
err = client.Post().Path(mapping.Resource).Namespace(namespace).Body(obj).Do().Error()
80+
if err != nil {
81+
return &errs.ValidationError{errs.ValidationErrorTypeInvalid, name, err}
82+
}
83+
84+
return nil
85+
}
86+
87+
// reportError reports the single item validation error and properly set the
88+
// prefix and index to match the Config item JSON index
89+
func reportError(allErrs *errs.ValidationErrorList, index int, err errs.ValidationError) {
90+
i := errs.ValidationErrorList{}
91+
*allErrs = append(*allErrs, append(i, err).PrefixIndex(index).Prefix("item")...)
92+
}

pkg/config/config_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2014 Google Inc. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package config
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"net/http/httptest"
23+
"net/url"
24+
"testing"
25+
26+
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
27+
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
28+
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
29+
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
30+
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
31+
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
32+
)
33+
34+
func getTyperAndMapper() (runtime.ObjectTyper, meta.RESTMapper) {
35+
return api.Scheme, latest.RESTMapper
36+
}
37+
38+
func getFakeClient(t *testing.T, validURLs []string) (ClientFunc, *httptest.Server) {
39+
handlerFunc := func(w http.ResponseWriter, r *http.Request) {
40+
for _, u := range validURLs {
41+
if u == r.RequestURI {
42+
return
43+
}
44+
}
45+
t.Errorf("Unexpected HTTP request: %s, expected %v", r.RequestURI, validURLs)
46+
}
47+
server := httptest.NewServer(http.HandlerFunc(handlerFunc))
48+
return func(mapping *meta.RESTMapping) (*client.RESTClient, error) {
49+
fakeCodec := runtime.CodecFor(api.Scheme, "v1beta1")
50+
fakeUri, _ := url.Parse(server.URL + "/api/v1beta1")
51+
return client.NewRESTClient(fakeUri, fakeCodec), nil
52+
}, server
53+
}
54+
55+
func TestCreateObjects(t *testing.T) {
56+
items := []runtime.Object{}
57+
58+
items = append(items, &api.Pod{
59+
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"},
60+
ObjectMeta: api.ObjectMeta{Name: "test-pod"},
61+
})
62+
63+
items = append(items, &api.Service{
64+
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"},
65+
ObjectMeta: api.ObjectMeta{Name: "test-service"},
66+
})
67+
68+
typer, mapper := getTyperAndMapper()
69+
client, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"})
70+
71+
errs := CreateObjects(typer, mapper, client, items)
72+
s.Close()
73+
if len(errs) != 0 {
74+
t.Errorf("Unexpected errors during config.Create(): %v", errs)
75+
}
76+
}
77+
78+
func TestCreateNoNameItem(t *testing.T) {
79+
items := []runtime.Object{}
80+
81+
items = append(items, &api.Service{
82+
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Service"},
83+
})
84+
85+
typer, mapper := getTyperAndMapper()
86+
client, s := getFakeClient(t, []string{"/api/v1beta1/services"})
87+
88+
errs := CreateObjects(typer, mapper, client, items)
89+
s.Close()
90+
91+
if len(errs) == 0 {
92+
t.Errorf("Expected required value error for missing name")
93+
}
94+
95+
e := errs[0].(errors.ValidationError)
96+
if errors.ValueOf(e.Type) != "required value" {
97+
t.Errorf("Expected ValidationErrorTypeRequired error, got %#v", e)
98+
}
99+
100+
if e.Field != "Config.item[0].name" {
101+
t.Errorf("Expected 'Config.item[0].name' as error field, got '%#v'", e.Field)
102+
}
103+
}
104+
105+
type InvalidItem struct{}
106+
107+
func (*InvalidItem) IsAnAPIObject() {}
108+
109+
func TestCreateInvalidItem(t *testing.T) {
110+
items := []runtime.Object{
111+
&InvalidItem{},
112+
}
113+
114+
typer, mapper := getTyperAndMapper()
115+
client, s := getFakeClient(t, []string{})
116+
117+
errs := CreateObjects(typer, mapper, client, items)
118+
s.Close()
119+
120+
if len(errs) == 0 {
121+
t.Errorf("Expected invalid value error for kind")
122+
}
123+
124+
e := errs[0].(errors.ValidationError)
125+
if errors.ValueOf(e.Type) != "invalid value" {
126+
t.Errorf("Expected ValidationErrorTypeInvalid error, got %#v", e)
127+
}
128+
129+
if e.Field != "Config.item[0].kind" {
130+
t.Errorf("Expected 'Config.item[0].kind' as error field, got '%#v'", e.Field)
131+
}
132+
}
133+
134+
func TestCreateNoClientItems(t *testing.T) {
135+
items := []runtime.Object{}
136+
137+
items = append(items, &api.Pod{
138+
TypeMeta: api.TypeMeta{APIVersion: "v1beta1", Kind: "Pod"},
139+
ObjectMeta: api.ObjectMeta{Name: "test-pod"},
140+
})
141+
142+
typer, mapper := getTyperAndMapper()
143+
_, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"})
144+
145+
noClientFunc := func(mapping *meta.RESTMapping) (*client.RESTClient, error) {
146+
return nil, fmt.Errorf("no client")
147+
}
148+
149+
errs := CreateObjects(typer, mapper, noClientFunc, items)
150+
s.Close()
151+
152+
if len(errs) == 0 {
153+
t.Errorf("Expected invalid value error for client")
154+
}
155+
156+
e := errs[0].(errors.ValidationError)
157+
if errors.ValueOf(e.Type) != "unsupported value" {
158+
t.Errorf("Expected ValidationErrorTypeUnsupported error, got %#v", e)
159+
}
160+
161+
if e.Field != "Config.item[0].client" {
162+
t.Errorf("Expected 'Config.item[0].client' as error field, got '%#v'", e.Field)
163+
}
164+
}

pkg/config/config_test.json

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
[
2+
{
3+
"id": "frontend",
4+
"name": "frontend",
5+
"kind": "Service",
6+
"apiVersion": "v1beta2",
7+
"port": 5432,
8+
"selector": {
9+
"name": "frontend"
10+
}
11+
},
12+
{
13+
"id": "redismaster",
14+
"name": "redismaster",
15+
"kind": "Service",
16+
"apiVersion": "v1beta1",
17+
"port": 10000,
18+
"selector": {
19+
"name": "redis-master"
20+
}
21+
},
22+
{
23+
"id": "redisslave",
24+
"name": "redisslave",
25+
"kind": "Service",
26+
"apiVersion": "v1beta1",
27+
"port": 10001,
28+
"labels": {
29+
"name": "redisslave"
30+
},
31+
"selector": {
32+
"name": "redisslave"
33+
}
34+
},
35+
{
36+
"id": "redis-master-2",
37+
"name": "redis-master-2",
38+
"kind": "Pod",
39+
"apiVersion": "v1beta1",
40+
"desiredState": {
41+
"manifest": {
42+
"version": "v1beta1",
43+
"containers": [{
44+
"name": "master",
45+
"image": "dockerfile/redis",
46+
"env": [
47+
{
48+
"name": "REDIS_PASSWORD",
49+
"value": "secret"
50+
}
51+
],
52+
"ports": [{
53+
"containerPort": 6379
54+
}]
55+
}]
56+
}
57+
},
58+
"labels": {
59+
"name": "redis-master"
60+
}
61+
},
62+
{
63+
"id": "frontendController",
64+
"name": "frontendController",
65+
"kind": "ReplicationController",
66+
"apiVersion": "v1beta1",
67+
"desiredState": {
68+
"replicas": 3,
69+
"replicaSelector": {"name": "frontend"},
70+
"podTemplate": {
71+
"desiredState": {
72+
"manifest": {
73+
"version": "v1beta1",
74+
"containers": [{
75+
"name": "php-redis",
76+
"image": "brendanburns/php-redis",
77+
"env": [
78+
{
79+
"name": "ADMIN_USERNAME",
80+
"value": "admin"
81+
},
82+
{
83+
"name": "ADMIN_PASSWORD",
84+
"value": "secret"
85+
},
86+
{
87+
"name": "REDIS_PASSWORD",
88+
"value": "secret"
89+
}
90+
],
91+
"ports": [{"containerPort": 80}]
92+
}]
93+
}
94+
},
95+
"labels": {"name": "frontend"}
96+
}},
97+
"labels": {"name": "frontend"}
98+
},
99+
{
100+
"id": "redisSlaveController",
101+
"name": "redisSlaveController",
102+
"kind": "ReplicationController",
103+
"apiVersion": "v1beta1",
104+
"desiredState": {
105+
"replicas": 2,
106+
"replicaSelector": {"name": "redisslave"},
107+
"podTemplate": {
108+
"desiredState": {
109+
"manifest": {
110+
"version": "v1beta1",
111+
"containers": [{
112+
"name": "slave",
113+
"image": "brendanburns/redis-slave",
114+
"env": [
115+
{
116+
"name": "REDIS_PASSWORD",
117+
"value": "secret"
118+
}
119+
],
120+
"ports": [{"containerPort": 6379}]
121+
}]
122+
}
123+
},
124+
"labels": {"name": "redisslave"}
125+
}},
126+
"labels": {"name": "redisslave"}
127+
}
128+
]

pkg/kubectl/cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
6868
cmds.AddCommand(NewCmdDelete(out))
6969
cmds.AddCommand(NewCmdNamespace(out))
7070
cmds.AddCommand(NewCmdLog(out))
71+
cmds.AddCommand(NewCmdCreateAll(out))
7172

7273
if err := cmds.Execute(); err != nil {
7374
os.Exit(1)

0 commit comments

Comments
 (0)