提交 f62b4688 编写于 作者: B bryk

Replica set list: handle endpoints and descirption

This change handles replica set description end-to-end: saving in on the
deploy form and then displaying. It also handles replica set endpoints.
I also attempted to write tests for the go code, but this change grew
too much. I'll do this later.
上级 863df6d1
......@@ -26,7 +26,7 @@ import goCommand from './gocommand';
* Compiles backend application in development mode and places the binary in the serve
* directory.
*/
gulp.task('backend', function(doneFn) {
gulp.task('backend', ['package-backend-source'], function(doneFn) {
goCommand(
[
'build',
......@@ -46,7 +46,7 @@ gulp.task('backend', function(doneFn) {
* The production binary difference from development binary is only that it contains all
* dependencies inside it and is targeted for Linux.
*/
gulp.task('backend:prod', function(doneFn) {
gulp.task('backend:prod', ['package-backend-source'], function(doneFn) {
let outputBinaryPath = path.join(conf.paths.dist, conf.backend.binaryName);
// Delete output binary first. This is required because prod build does not override it.
......@@ -72,3 +72,16 @@ gulp.task('backend:prod', function(doneFn) {
},
function(error) { doneFn(error); });
});
/**
* Moves all backend source files (app and tests) to a temporary package directory where it can be
* applied go commands.
*
* This is required to consolidate test and app files into single directories and to make packaging
* work.
*/
gulp.task('package-backend-source', function() {
return gulp
.src([path.join(conf.paths.backendSrc, '**/*'), path.join(conf.paths.backendTest, '**/*')])
.pipe(gulp.dest(conf.paths.backendTmpSrc));
});
......@@ -37,11 +37,7 @@ export default {
/**
* Name of the main backend package that is used in go build command.
*/
packageName: 'app/backend',
/**
* Name of the test backend package that is used in go test command.
*/
testPackageName: 'test/backend',
packageName: 'github.com/kubernetes/dashboard',
/**
* Port number of the backend server. Only used during development.
*/
......@@ -86,6 +82,7 @@ export default {
backendSrc: path.join(basePath, 'src/app/backend'),
backendTest: path.join(basePath, 'src/test/backend'),
backendTmp: path.join(basePath, '.tmp/backend'),
backendTmpSrc: path.join(basePath, '.tmp/backend/src/github.com/kubernetes/dashboard'),
bowerComponents: path.join(basePath, 'bower_components'),
build: path.join(basePath, 'build'),
deploySrc: path.join(basePath, 'src/app/deploy'),
......
......@@ -21,7 +21,8 @@ import lodash from 'lodash';
import conf from './conf';
/**
* Spawns Go process wrapped with the Godep command.
* Spawns Go process wrapped with the Godep command. Backend source files must me packaged with
* 'package-backend-source' task before running this command.
*
* @param {!Array<string>} args
* @param {function(?Error=)} doneFn
......@@ -33,7 +34,7 @@ export default function spawnGoProcess(args, doneFn) {
return;
}
let sourceGopath = `${process.env.GOPATH}:${conf.paths.base}`;
let sourceGopath = `${process.env.GOPATH}:${conf.paths.backendTmp}`;
let env = lodash.merge(process.env, {GOPATH: sourceGopath});
let goTask = child.spawn('godep', ['go'].concat(args), {
......
......@@ -28,7 +28,7 @@ import goCommand from './gocommand';
* @param {boolean} singleRun
* @param {function(?Error=)} doneFn
*/
function runUnitTests(singleRun, doneFn) {
function runFrontendUnitTests(singleRun, doneFn) {
let localConfig = {
configFile: conf.paths.karmaConf,
singleRun: singleRun,
......@@ -41,18 +41,6 @@ function runUnitTests(singleRun, doneFn) {
server.start();
}
/**
* @param {function(?Error=)} doneFn
*/
function runBackendTests(doneFn) {
goCommand(
[
'test',
conf.backend.testPackageName,
],
doneFn);
}
/**
* @param {function(?Error=)} doneFn
*/
......@@ -86,12 +74,19 @@ gulp.task('test', ['frontend-test', 'backend-test']);
/**
* Runs once all unit tests of the frontend application.
*/
gulp.task('frontend-test', function(doneFn) { runUnitTests(true, doneFn); });
gulp.task('frontend-test', function(doneFn) { runFrontendUnitTests(true, doneFn); });
/**
* Runs once all unit tests of the backend application.
*/
gulp.task('backend-test', runBackendTests);
gulp.task('backend-test', ['package-backend-source'], function(doneFn) {
goCommand(
[
'test',
conf.backend.packageName,
],
doneFn);
});
/**
* Runs all unit tests of the application. Watches for changes in the source files to rerun
......@@ -103,7 +98,7 @@ gulp.task('test:watch', ['frontend-test:watch', 'backend-test:watch']);
* Runs frontend backend application tests. Watches for changes in the source files to rerun
* the tests.
*/
gulp.task('frontend-test:watch', function(doneFn) { runUnitTests(false, doneFn); });
gulp.task('frontend-test:watch', function(doneFn) { runFrontendUnitTests(false, doneFn); });
/**
* Runs backend application tests. Watches for changes in the source files to rerun
......
......@@ -62,7 +62,7 @@ func CreateHttpApiHandler(client *client.Client) http.Handler {
namespacesWs.Route(
namespacesWs.GET("").
To(apiHandler.handleGetNamespaces).
Writes(NamespacesList{}))
Writes(NamespaceList{}))
wsContainer.Add(namespacesWs)
logsWs := new(restful.WebService)
......
......@@ -20,6 +20,11 @@ import (
"k8s.io/kubernetes/pkg/util"
)
const (
DescriptionAnnotationKey = "description"
NameLabelKey = "name"
)
// Specification for an app deployment.
type AppDeploymentSpec struct {
// Name of the application.
......@@ -38,6 +43,9 @@ type AppDeploymentSpec struct {
// Whether the created service is external.
IsExternal bool `json:"isExternal"`
// Description of the deployment.
Description string `json:"description"`
// Target namespace of the application.
Namespace string `json:"namespace"`
}
......@@ -59,10 +67,16 @@ type PortMapping struct {
// common labels.
// TODO(bryk): Write tests for this function.
func DeployApp(spec *AppDeploymentSpec, client *client.Client) error {
annotations := map[string]string{DescriptionAnnotationKey: spec.Description}
labels := map[string]string{NameLabelKey: spec.Name}
objectMeta := api.ObjectMeta{
Annotations: annotations,
Name: spec.Name,
Labels: labels,
}
podTemplate := &api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{
Labels: map[string]string{"name": spec.Name},
},
ObjectMeta: objectMeta,
Spec: api.PodSpec{
Containers: []api.Container{{
Name: spec.Name,
......@@ -72,12 +86,10 @@ func DeployApp(spec *AppDeploymentSpec, client *client.Client) error {
}
replicaSet := &api.ReplicationController{
ObjectMeta: api.ObjectMeta{
Name: spec.Name,
},
ObjectMeta: objectMeta,
Spec: api.ReplicationControllerSpec{
Replicas: spec.Replicas,
Selector: map[string]string{"name": spec.Name},
Selector: labels,
Template: podTemplate,
},
}
......@@ -91,12 +103,9 @@ func DeployApp(spec *AppDeploymentSpec, client *client.Client) error {
if len(spec.PortMappings) > 0 {
service := &api.Service{
ObjectMeta: api.ObjectMeta{
Name: spec.Name,
Labels: map[string]string{"name": spec.Name},
},
ObjectMeta: objectMeta,
Spec: api.ServiceSpec{
Selector: map[string]string{"name": spec.Name},
Selector: labels,
},
}
......
......@@ -28,7 +28,7 @@ type NamespaceSpec struct {
}
// List of Namespaces in the cluster.
type NamespacesList struct {
type NamespaceList struct {
// Unordered list of Namespaces.
Namespaces []string `json:"namespaces"`
}
......@@ -47,14 +47,14 @@ func CreateNamespace(spec *NamespaceSpec, client *client.Client) error {
}
// Returns a list of all namespaces in the cluster.
func GetNamespaceList(client *client.Client) (*NamespacesList, error) {
func GetNamespaceList(client *client.Client) (*NamespaceList, error) {
list, err := client.Namespaces().List(labels.Everything(), fields.Everything())
if err != nil {
return nil, err
}
namespaceList := &NamespacesList{}
namespaceList := &NamespaceList{}
for _, element := range list.Items {
namespaceList.Namespaces = append(namespaceList.Namespaces, element.ObjectMeta.Name)
......
......@@ -20,6 +20,8 @@ import (
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/labels"
"strconv"
"strings"
)
// List of Replica Sets in the cluster.
......@@ -56,47 +58,131 @@ type ReplicaSet struct {
CreationTime unversioned.Time `json:"creationTime"`
// Internal endpoints of all Kubernetes services have the same label selector as this Replica Set.
// Endpoint is DNS name merged with ports.
InternalEndpoints []string `json:"internalEndpoints"`
// External endpoints of all Kubernetes services have the same label selector as this Replica Set.
// Endpoint is external IP address name merged with ports.
ExternalEndpoints []string `json:"externalEndpoints"`
}
// Returns a list of all Replica Sets in the cluster.
func GetReplicaSetList(client *client.Client) (*ReplicaSetList, error) {
list, err := client.ReplicationControllers(api.NamespaceAll).
replicaSets, err := client.ReplicationControllers(api.NamespaceAll).
List(labels.Everything(), fields.Everything())
if err != nil {
return nil, err
}
services, err := client.Services(api.NamespaceAll).
List(labels.Everything(), fields.Everything())
if err != nil {
return nil, err
}
return getReplicaSetList(replicaSets.Items, services.Items), nil
}
// Returns a list of all Replica Set model objects in the cluster, based on all Kubernetes
// Replica Set and Service API objects.
// The function processes all Replica Sets API objects and finds matching Services for them.
func getReplicaSetList(
replicaSets []api.ReplicationController, services []api.Service) *ReplicaSetList {
replicaSetList := &ReplicaSetList{}
for _, replicaSet := range list.Items {
for _, replicaSet := range replicaSets {
var containerImages []string
for _, container := range replicaSet.Spec.Template.Spec.Containers {
containerImages = append(containerImages, container.Image)
}
matchingServices := getMatchingServices(services, &replicaSet)
var internalEndpoints []string
var externalEndpoints []string
for _, service := range matchingServices {
internalEndpoints = append(internalEndpoints,
getInternalEndpoint(service.Name, service.Namespace, service.Spec.Ports))
for _, externalIp := range service.Status.LoadBalancer.Ingress {
externalEndpoints = append(externalEndpoints,
getExternalEndpoint(externalIp.Hostname, service.Spec.Ports))
}
}
replicaSetList.ReplicaSets = append(replicaSetList.ReplicaSets, ReplicaSet{
Name: replicaSet.ObjectMeta.Name,
Namespace: replicaSet.ObjectMeta.Namespace,
// TODO(bryk): This field contains test value. Implement it.
Description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Nulla metus nibh, iaculis a consectetur vitae, imperdiet pellentesque turpis.",
Labels: replicaSet.ObjectMeta.Labels,
PodsRunning: replicaSet.Status.Replicas,
PodsPending: replicaSet.Spec.Replicas - replicaSet.Status.Replicas,
ContainerImages: containerImages,
CreationTime: replicaSet.ObjectMeta.CreationTimestamp,
// TODO(bryk): This field contains test value. Implement it.
InternalEndpoints: []string{"webapp"},
// TODO(bryk): This field contains test value. Implement it.
ExternalEndpoints: []string{"81.76.02.198:80"},
Name: replicaSet.ObjectMeta.Name,
Namespace: replicaSet.ObjectMeta.Namespace,
Description: replicaSet.Annotations[DescriptionAnnotationKey],
Labels: replicaSet.ObjectMeta.Labels,
PodsRunning: replicaSet.Status.Replicas,
PodsPending: replicaSet.Spec.Replicas - replicaSet.Status.Replicas,
ContainerImages: containerImages,
CreationTime: replicaSet.ObjectMeta.CreationTimestamp,
InternalEndpoints: internalEndpoints,
ExternalEndpoints: externalEndpoints,
})
}
return replicaSetList, nil
return replicaSetList
}
// Returns internal endpoint name for the given service properties, e.g.,
// "my-service.namespace 80/TCP" or "my-service 53/TCP,53/UDP".
func getInternalEndpoint(serviceName string, namespace string, ports []api.ServicePort) string {
name := serviceName
if namespace != api.NamespaceDefault {
name = name + "." + namespace
}
return name + getServicePortsName(ports)
}
// Returns external endpoint name for the given service properties.
func getExternalEndpoint(serviceIp string, ports []api.ServicePort) string {
return serviceIp + getServicePortsName(ports)
}
// Gets human readable name for the given service ports list.
func getServicePortsName(ports []api.ServicePort) string {
var portsString []string
for _, port := range ports {
portsString = append(portsString, strconv.Itoa(port.Port)+"/"+string(port.Protocol))
}
if len(portsString) > 0 {
return " " + strings.Join(portsString, ",")
} else {
return ""
}
}
// Returns all services that target the same Pods (or subset) as the given Replica Set.
func getMatchingServices(services []api.Service,
replicaSet *api.ReplicationController) []api.Service {
var matchingServices []api.Service
for _, service := range services {
if isServiceMatchingReplicaSet(service.Spec.Selector, replicaSet.Spec.Selector) {
matchingServices = append(matchingServices, service)
}
}
return matchingServices
}
// Returns true when a Service with the given selector targets the same Pods (or subset) that
// a Replica Set with the given selector.
func isServiceMatchingReplicaSet(serviceSelector map[string]string,
replicaSetSpecSelector map[string]string) bool {
// If service has no selectors, then assume it targets different Pods.
if len(serviceSelector) == 0 {
return false
}
for label, value := range serviceSelector {
if rsValue, ok := replicaSetSpecSelector[label]; !ok || rsValue != value {
return false
}
}
return true
}
......@@ -38,6 +38,7 @@ backendApi.PortMapping;
* containerImage: string,
* isExternal: boolean,
* name: string,
* description: string,
* portMappings: !Array<!backendApi.PortMapping>,
* replicas: number,
* namespace: string
......
......@@ -18,7 +18,7 @@ limitations under the License.
<md-whiteframe class="kd-deploy-whiteframe md-whiteframe-5dp" flex flex-gt-md>
<h3 class="md-headline">Deploy a Containerized App</h3>
<form ng-submit="ctrl.deploy()">
<md-input-container class="md-block">
<md-input-container>
<label>App name</label>
<input ng-model="ctrl.name" required>
</md-input-container>
......@@ -30,24 +30,24 @@ limitations under the License.
Upload a YAML or JSON file
</md-radio-button>
</md-radio-group>
<md-input-container class="md-block">
<md-input-container>
<label>Container image</label>
<input ng-model="ctrl.containerImage" required>
</md-input-container>
<md-input-container class="md-block">
<md-input-container>
<label>Number of pods</label>
<input ng-model="ctrl.replicas" type="number" required min="1">
</md-input-container>
<div layout="row" ng-repeat="portMapping in ctrl.portMappings">
<md-input-container class="md-block">
<md-input-container flex>
<label>Port</label>
<input ng-model="portMapping.port" type="number" min="0">
</md-input-container>
<md-input-container class="md-block">
<md-input-container flex>
<label>Target port</label>
<input ng-model="portMapping.targetPort" type="number" min="0">
</md-input-container>
<md-input-container class="md-block">
<md-input-container flex="none">
<label>Protocol</label>
<md-select ng-model="portMapping.protocol" required>
<md-option ng-repeat="protocol in ctrl.protocols" ng-value="protocol">
......@@ -56,7 +56,7 @@ limitations under the License.
</md-select>
</md-input-container>
</div>
<md-input-container class="md-block">
<md-input-container>
<label>Namespace</label>
<md-select ng-model="ctrl.namespace" required>
<md-option ng-repeat="namespace in ctrl.namespaces" ng-value="namespace">
......@@ -70,6 +70,10 @@ limitations under the License.
<md-switch ng-model="ctrl.isExternal" class="md-primary">
Expose service externally
</md-switch>
<md-input-container>
<label>Description (optional)</label>
<textarea ng-model="ctrl.description"></textarea>
</md-input-container>
<md-button class="md-raised md-primary" type="submit" ng-disabled="ctrl.isDeployDisabled()">
Deploy
</md-button>
......
......@@ -40,6 +40,9 @@ export default class DeployController {
/** @export {number} */
this.replicas = 1;
/** @export {string} */
this.description = '';
/**
* List of supported protocols.
* TODO(bryk): Do not hardcode the here, move to backend.
......@@ -94,6 +97,7 @@ export default class DeployController {
containerImage: this.containerImage,
isExternal: this.isExternal,
name: this.name,
description: this.description,
portMappings: this.portMappings.filter(this.isPortMappingEmpty_),
replicas: this.replicas,
namespace: this.namespace,
......
......@@ -15,17 +15,17 @@ limitations under the License.
-->
<div layout="row" layout-wrap layout-margin layout-align="center center">
<md-card ng-repeat="replicaSet in ctrl.replicaSets">
<md-card ng-repeat="replicaSet in ::ctrl.replicaSets">
<md-card-content class="kd-replicaset-card">
<div layout="row" layout-align="space-between center">
<div flex layout="column">
<a ng-href="{{ctrl.getReplicaSetDetailHref(replicaSet)}}" flex>
{{replicaSet.name}}
<a ng-href="{{::ctrl.getReplicaSetDetailHref(replicaSet)}}" flex>
{{::replicaSet.name}}
</a>
<div flex class="md-caption">
<span ng-repeat="(label, value) in replicaSet.labels"
<span ng-repeat="(label, value) in ::replicaSet.labels"
class="kd-replicaset-card-label">
{{label}}:{{value}}
{{::label}}:{{::value}}
</span>
</div>
</div>
......@@ -36,50 +36,56 @@ limitations under the License.
<div class="md-caption">
<div layout="row">
<span flex="60">
{{replicaSet.podsRunning}} pods running, {{replicaSet.podsPending}} pending
{{::replicaSet.podsRunning}} pods running, {{::replicaSet.podsPending}} pending
</span>
<a flex="40" href="#" class="kd-replicaset-card-logs">Logs</a>
</div>
<hr class="kd-replicaset-card-divider"></hr>
<div layout="row" layout-wrap>
<div ng-if="replicaSet.description" flex="100" layout="column"
<div ng-if="::replicaSet.description" flex="100" layout="column"
class="kd-replicaset-card-section">
<span flex>Description</span>
<div flex>
{{replicaSet.description}}
{{::replicaSet.description}}
</div>
</div>
<div flex="60" layout="column" class="kd-replicaset-card-section">
<span flex>Image</span>
<div flex>
<div ng-repeat="image in replicaSet.containerImages"
<div ng-repeat="image in ::replicaSet.containerImages track by $index"
class="kd-replicaset-card-section-image">
{{image}}
{{::image}}
</div>
</div>
</div>
<div flex="40" layout="column" class="kd-replicaset-card-section">
<span flex="initial">Creation time</span>
<span flex>{{replicaSet.creationTime}}</span>
<span flex>{{::replicaSet.creationTime}}</span>
</div>
<div flex="60" layout="column" class="kd-replicaset-card-section">
<span flex="initial">Internal Endpoint</span>
<div flex>
<span ng-repeat="endpoint in replicaSet.internalEndpoints">
{{endpoint}}
</span>
<div ng-repeat="endpoint in ::replicaSet.internalEndpoints track by $index">
{{::endpoint}}
</div>
<div ng-if="::!replicaSet.internalEndpoints.length">
none
</div>
</div>
</div>
<div flex="40" layout="column" class="kd-replicaset-card-section">
<span flex="initial">External Endpoint</span>
<div flex>
<span ng-repeat="endpoint in replicaSet.externalEndpoints">
{{endpoint}}
</span>
<div ng-repeat="endpoint in ::replicaSet.externalEndpoints track by $index">
<a ng-href="http://{{::endpoint}}" target="_blank">{{::endpoint}}</a>
</div>
<div ng-if="::!replicaSet.externalEndpoints.length">
none
</div>
</div>
</div>
</div>
......
......@@ -15,7 +15,6 @@
package main
import (
backend "app/backend"
client "k8s.io/kubernetes/pkg/client/unversioned"
"testing"
)
......@@ -34,14 +33,14 @@ func (FakeClientFactory) NewInCluster() (*client.Client, error) {
}
func TestCreateApiserverClient_inCluster(t *testing.T) {
client, _ := backend.CreateApiserverClient("", new(FakeClientFactory))
client, _ := CreateApiserverClient("", new(FakeClientFactory))
if client != fakeInClusterClient {
t.Fatal("Expected in cluster client to be created")
}
}
func TestCreateApiserverClient_remote(t *testing.T) {
client, _ := backend.CreateApiserverClient("http://foo:bar", new(FakeClientFactory))
client, _ := CreateApiserverClient("http://foo:bar", new(FakeClientFactory))
if client != fakeRemoteClient {
t.Fatal("Expected remote client to be created")
}
......
// 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 main
import (
"testing"
)
func TestFoo(t *testing.T) {
// TODO(bryk): Write tests here.
isServiceMatchingReplicaSet(nil, nil)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册