Skip to content

Commit 08f3de2

Browse files
committed
Merge pull request #285 from batikanu/deploy-file-backend
Deploy file
2 parents 9ee926b + 5956298 commit 08f3de2

File tree

6 files changed

+226
-2
lines changed

6 files changed

+226
-2
lines changed

src/app/backend/apihandler.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ func CreateHttpApiHandler(client *client.Client, heapsterClient *unversioned.RES
8686
Writes(Protocols{}))
8787
wsContainer.Add(deployWs)
8888

89+
deployFromFileWs := new(restful.WebService)
90+
deployFromFileWs.Path("/api/appdeploymentfromfile").
91+
Consumes(restful.MIME_JSON).
92+
Produces(restful.MIME_JSON)
93+
deployFromFileWs.Route(
94+
deployFromFileWs.POST("").
95+
To(apiHandler.handleDeployFromFile).
96+
Reads(AppDeploymentFromFileSpec{}).
97+
Writes(AppDeploymentFromFileSpec{}))
98+
wsContainer.Add(deployFromFileWs)
99+
89100
replicaSetWs := new(restful.WebService)
90101
replicaSetWs.Filter(wsLogger)
91102
replicaSetWs.Path("/api/replicasets").
@@ -188,6 +199,21 @@ func (apiHandler *ApiHandler) handleDeploy(request *restful.Request, response *r
188199
response.WriteHeaderAndEntity(http.StatusCreated, appDeploymentSpec)
189200
}
190201

202+
// Handles deploy from file API call.
203+
func (apiHandler *ApiHandler) handleDeployFromFile(request *restful.Request, response *restful.Response) {
204+
deploymentSpec := new(AppDeploymentFromFileSpec)
205+
if err := request.ReadEntity(deploymentSpec); err != nil {
206+
handleInternalError(response, err)
207+
return
208+
}
209+
if err := DeployAppFromFile(deploymentSpec); err != nil {
210+
handleInternalError(response, err)
211+
return
212+
}
213+
214+
response.WriteHeaderAndEntity(http.StatusCreated, deploymentSpec)
215+
}
216+
191217
// Handles app name validation API call.
192218
func (apiHandler *ApiHandler) handleNameValidity(request *restful.Request, response *restful.Response) {
193219
spec := new(AppNameValiditySpec)

src/app/backend/deploy.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"k8s.io/kubernetes/pkg/api"
2323
"k8s.io/kubernetes/pkg/api/resource"
2424
client "k8s.io/kubernetes/pkg/client/unversioned"
25+
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
26+
kubectlResource "k8s.io/kubernetes/pkg/kubectl/resource"
2527
"k8s.io/kubernetes/pkg/util/intstr"
2628
)
2729

@@ -76,6 +78,15 @@ type AppDeploymentSpec struct {
7678
RunAsPrivileged bool `json:"runAsPrivileged"`
7779
}
7880

81+
// Specification for deployment from file
82+
type AppDeploymentFromFileSpec struct {
83+
// Name of the file
84+
Name string `json:"name"`
85+
86+
// File content
87+
Content string `json:"content"`
88+
}
89+
7990
// Port mapping for an application deployment.
8091
type PortMapping struct {
8192
// Port that will be exposed on the service.
@@ -231,3 +242,38 @@ func getLabelsMap(labels []Label) map[string]string {
231242

232243
return result
233244
}
245+
246+
// Deploys an app based on the given yaml or json file.
247+
func DeployAppFromFile(spec *AppDeploymentFromFileSpec) error {
248+
const (
249+
validate = true
250+
emptyCacheDir = ""
251+
)
252+
253+
factory := cmdutil.NewFactory(nil)
254+
schema, err := factory.Validator(validate, emptyCacheDir)
255+
if err != nil {
256+
return err
257+
}
258+
259+
mapper, typer := factory.Object()
260+
reader := strings.NewReader(spec.Content)
261+
262+
r := kubectlResource.NewBuilder(mapper, typer, factory.ClientMapperForCommand()).
263+
Schema(schema).
264+
NamespaceParam(api.NamespaceDefault).DefaultNamespace().
265+
Stream(reader, spec.Name).
266+
Flatten().
267+
Do()
268+
269+
return r.Visit(func(info *kubectlResource.Info, err error) error {
270+
// creates an object from input info
271+
_, err = kubectlResource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
272+
if err != nil {
273+
return err
274+
}
275+
log.Printf("%s is deployed", info.Name)
276+
return nil
277+
})
278+
}
279+

src/app/externs/backendapi.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ backendApi.Label;
6060
*/
6161
backendApi.AppDeploymentSpec;
6262

63+
/**
64+
* @typedef {{
65+
* name: string,
66+
* content: string
67+
* }}
68+
*/
69+
backendApi.AppDeploymentFromFileSpec;
70+
6371
/**
6472
* @typedef {{
6573
* namespace: string,

src/app/frontend/deploy/deployfromfile_controller.js

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import {stateName as replicasetliststate} from 'replicasetlist/replicasetlist_state';
16+
1517
/**
1618
* Controller for the deploy from file directive.
1719
*
1820
* @final
1921
*/
2022
export default class DeployFromFileController {
21-
/** @ngInject */
22-
constructor() {
23+
/**
24+
* @param {!angular.$log} $log
25+
* @param {!ui.router.$state} $state
26+
* @param {!angular.$resource} $resource
27+
* @param {!angular.$q} $q
28+
* @ngInject */
29+
constructor($log, $state, $resource, $q, errorDialog) {
2330
/**
2431
* It initializes the scope output parameter
2532
*
@@ -33,5 +40,55 @@ export default class DeployFromFileController {
3340
* @export {{name:string, content:string}}
3441
*/
3542
this.file = {name: '', content: ''};
43+
44+
/** @private {!angular.$q} */
45+
this.q_ = $q;
46+
47+
/** @private {!angular.$resource} */
48+
this.resource_ = $resource;
49+
50+
/** @private {!angular.$log} */
51+
this.log_ = $log;
52+
53+
/** @private {!ui.router.$state} */
54+
this.state_ = $state;
55+
56+
/**
57+
* TODO (cheld) Set correct type after fixing issue #159
58+
* @private {!Object}
59+
*/
60+
this.errorDialog_ = errorDialog;
61+
}
62+
63+
/**
64+
* Deploys the application based on the state of the controller.
65+
*
66+
* @export
67+
* @return {angular.$q.Promise}
68+
*/
69+
deploy() {
70+
/** @type {!backendApi.AppDeploymentFromFileSpec} */
71+
let deploymentSpec = {
72+
name: this.file.name,
73+
content: this.file.content,
74+
};
75+
76+
let defer = this.q_.defer();
77+
78+
/** @type {!angular.Resource<!backendApi.AppDeploymentFromFileSpec>} */
79+
let resource = this.resource_('api/appdeploymentfromfile');
80+
resource.save(
81+
deploymentSpec,
82+
(savedConfig) => {
83+
defer.resolve(savedConfig); // Progress ends
84+
this.log_.info('Successfully deployed application: ', savedConfig);
85+
this.state_.go(replicasetliststate);
86+
},
87+
(err) => {
88+
defer.reject(err); // Progress ends
89+
this.log_.error('Error deploying application:', err);
90+
this.errorDialog_.open('Deploying file has failed', err.data);
91+
});
92+
return defer.promise;
3693
}
3794
}

src/test/backend/deploy_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,35 @@ func TestGetAvailableProtocols(t *testing.T) {
159159
expected, actual)
160160
}
161161
}
162+
163+
func TestDeployAppFromFileWithValidContent(t *testing.T) {
164+
validContent := "{\"kind\": \"Namespace\"," +
165+
"\"apiVersion\": \"v1\"," +
166+
"\"metadata\": {" +
167+
"\"name\": \"development\"," +
168+
"\"labels\": {\"name\": \"development\"}}}"
169+
170+
spec := &AppDeploymentFromFileSpec{
171+
Name: "foo-name",
172+
Content: validContent,
173+
}
174+
175+
err := DeployAppFromFile(spec)
176+
177+
if err != nil {
178+
t.Errorf("Expected return value to be %#v but got %#v", nil, err)
179+
}
180+
}
181+
182+
func TestDeployAppFromFileWithInvalidContent(t *testing.T) {
183+
spec := &AppDeploymentFromFileSpec{
184+
Name: "foo-name",
185+
Content: "foo-content-invalid",
186+
}
187+
188+
err := DeployAppFromFile(spec)
189+
190+
if err == nil {
191+
t.Errorf("Expected return value to be an error but got %#v", nil)
192+
}
193+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2015 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import DeployFromFileController from 'deploy/deployfromfile_controller';
16+
import deployModule from 'deploy/deploy_module';
17+
18+
describe('DeployFromFile controller', () => {
19+
/** @type {!DeployFromFileController} */
20+
let ctrl;
21+
/** @type {!angular.$resource} */
22+
let mockResource;
23+
/** @type {!angular.FormController} */
24+
let form;
25+
26+
beforeEach(() => {
27+
angular.mock.module(deployModule.name);
28+
29+
angular.mock.inject(($controller) => {
30+
mockResource = jasmine.createSpy('$resource');
31+
ctrl = $controller(DeployFromFileController, {$resource: mockResource}, {form: form});
32+
});
33+
});
34+
35+
it('should deploy with file name and content', () => {
36+
// given
37+
let resourceObject = {
38+
save: jasmine.createSpy('save'),
39+
};
40+
ctrl.file.name = "test.yaml";
41+
ctrl.file.content = "test_content";
42+
mockResource.and.returnValue(resourceObject);
43+
resourceObject.save.and.callFake((spec) => {
44+
// then
45+
expect(spec.name).toBe("test.yaml");
46+
expect(spec.content).toBe('test_content');
47+
});
48+
// when
49+
ctrl.deploy();
50+
51+
// then
52+
expect(resourceObject.save).toHaveBeenCalled();
53+
});
54+
55+
});

0 commit comments

Comments
 (0)