Skip to content

Commit

Permalink
Merge pull request #14 from containerum/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
MargoTuleninova committed Sep 3, 2018
2 parents 735ab6e + e763ce8 commit c857d23
Show file tree
Hide file tree
Showing 53 changed files with 2,293 additions and 1,580 deletions.
1 change: 0 additions & 1 deletion .dockerignore
@@ -1,2 +1 @@
.git
.idea
5 changes: 3 additions & 2 deletions .gitlab-ci.yml
Expand Up @@ -9,6 +9,7 @@ variables:
NAMESPACE: hosting
SECRET: gitlab-registry
REPOSITORY: registry.containerum.net/ch/kube-api
PROJECT: kube

.docker-login: &docker-login
before_script:
Expand Down Expand Up @@ -57,7 +58,7 @@ dev-deploy:
name: develop
script:
- cd charts/kube
- helm upgrade --install --namespace=${NAMESPACE} ${CI_PROJECT_NAME} --set image.tag=${CI_COMMIT_SHA:0:8} --set image.secret=${SECRET} --set image.repository=${REPOSITORY} --set service.externalIP=192.168.88.210 --values values.yaml .
- helm upgrade --install --namespace=${NAMESPACE} ${PROJECT} --set image.tag=${CI_COMMIT_SHA:0:8} --set image.secret=${SECRET} --set image.repository=${REPOSITORY} --set service.externalIP=88.99.247.59 --values values.yaml .
only:
- develop

Expand Down Expand Up @@ -141,7 +142,7 @@ prod-deploy:
url: https://web.containerum.io
script:
- cd charts/kube
- helm upgrade --install --namespace=${NAMESPACE} ${CI_PROJECT_NAME} --set image.tag=${CI_COMMIT_TAG} --set image.secret=${SECRET} --set image.repository=${REPOSITORY} --set service.externalIP=${PRODUCTION_IP} --set env.CH_KUBE_API_DEBUG="false" --set env.CH_KUBE_API_TEXTLOG="false" --values values.yaml .
- helm upgrade --install --namespace=${NAMESPACE} ${PROJECT} --set image.tag=${CI_COMMIT_TAG} --set image.secret=${SECRET} --set image.repository=${REPOSITORY} --set service.externalIP=${PRODUCTION_IP} --set env.CH_KUBE_API_DEBUG="false" --set env.CH_KUBE_API_TEXTLOG="false" --values values.yaml .
only:
- tags
when: manual
16 changes: 10 additions & 6 deletions Dockerfile
@@ -1,13 +1,17 @@
FROM golang:1.10-alpine as builder
WORKDIR /go/src/git.containerum.net/ch/kube-api
RUN apk add --update make git
WORKDIR src/git.containerum.net/ch/kube-api
COPY . .
WORKDIR cmd/kube-api
RUN CGO_ENABLED=0 go build -v -ldflags="-w -s -extldflags '-static'" -tags="jsoniter" -o /bin/kube-api
RUN VERSION=$(git describe --abbrev=0 --tags) make build-for-docker

FROM alpine:3.7
COPY --from=builder /bin/kube-api /

VOLUME ["/cfg"]

COPY --from=builder /tmp/kube /
ENV CH_KUBE_API_DEBUG="true" \
CH_KUBE_API_TEXTLOG="true"
VOLUME ["/cfg"]

EXPOSE 1212
CMD ["/kube-api"]

CMD ["/kube"]
36 changes: 18 additions & 18 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Gopkg.toml
Expand Up @@ -67,11 +67,11 @@ required = ["github.com/containerum/kube-client"]

[[constraint]]
name = "github.com/containerum/utils"
version = "1.0.0"
version = "1.0.7"

[[constraint]]
name = "github.com/containerum/kube-client"
version = "^0.23.23"
version = "^0.23.27"

[[constraint]]
name = "k8s.io/kubernetes"
Expand Down
29 changes: 11 additions & 18 deletions Makefile
@@ -1,26 +1,21 @@
.PHONY: build test clean release single_release
.PHONY: build build-for-docker test clean release single_release

CMD_DIR:=cmd/kube-api
CLI_DIR:=cmd/kube-api
#get current package, assuming it`s in GOPATH sources
PACKAGE := $(shell go list -f '{{.ImportPath}}' ./$(CLI_DIR))
PACKAGE := $(PACKAGE:%/$(CLI_DIR)=%)

COMMIT_HASH=$(shell git rev-parse --short HEAD 2>/dev/null)
BUILD_DATE=$(shell date +%FT%T%Z)
LATEST_TAG=$(shell git describe --tags $(shell git rev-list --tags --max-count=1))

VERSION?=$(LATEST_TAG:v%=%)

# make directory and store path to variable
BUILDS_DIR:=$(PWD)/build
EXECUTABLE:=kube-api
LDFLAGS=-X '$(PACKAGE)/$(CLI_DIR).VERSION=v$(VERSION)'
EXECUTABLE:=kube
LDFLAGS=-X 'main.version=$(VERSION)' -w -s -extldflags '-static'

# go has build artifacts caching so soruce tracking not needed
build:
@echo "Building kube-api for current OS/architecture"
@go build -v -ldflags="$(LDFLAGS)" -o $(BUILDS_DIR)/$(EXECUTABLE) ./$(CMD_DIR)
@echo "Building mail-templater for current OS/architecture"
@echo $(LDFLAGS)
@CGO_ENABLED=0 go build -v -ldflags="$(LDFLAGS)" -tags="jsoniter" -o $(BUILDS_DIR)/$(EXECUTABLE) ./$(CMD_DIR)

build-for-docker:
@echo $(LDFLAGS)
@CGO_ENABLED=0 go build -v -ldflags="$(LDFLAGS)" -tags="jsoniter" -o /tmp/$(EXECUTABLE) ./$(CMD_DIR)

test:
@echo "Running tests"
Expand Down Expand Up @@ -69,7 +64,5 @@ single_release:
$(call build_release,$(OS),$(ARCH))

dev:
$(eval VERSION=$(LATEST_TAG:v%=%)+dev)
@echo building $(VERSION)
@echo $(PACKAGE)
go build -v --tags="dev" --ldflags="$(DEV_LDFLAGS)" ./$(CMD_DIR)
go build -v --tags="dev" --ldflags="$(LDFLAGS)" ./$(CMD_DIR)
5 changes: 4 additions & 1 deletion cmd/kube-api/main.go
Expand Up @@ -12,9 +12,12 @@ import (
//go:generate swagger validate ../../swagger.json
//go:generate protoc --go_out=../../proto -I../../proto exec.proto

var version string

func main() {
app := cli.NewApp()
app.Name = "ch-kube-api-server"
app.Name = "kube"
app.Version = version
app.Usage = "Kube api server for Container Hosting"
app.Flags = flags

Expand Down
10 changes: 9 additions & 1 deletion cmd/kube-api/server.go
Expand Up @@ -11,6 +11,7 @@ import (

"git.containerum.net/ch/kube-api/pkg/kubernetes"
"git.containerum.net/ch/kube-api/pkg/router"
"github.com/containerum/kube-client/pkg/model"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
Expand All @@ -26,7 +27,14 @@ func initServer(c *cli.Context) error {

kube := kubernetes.Kube{}
go exitOnErr(kube.RegisterClient(c.String("kubeconf")))
app := router.CreateRouter(&kube, c.Bool("cors"))

status := model.ServiceStatus{
Name: c.App.Name,
Version: c.App.Version,
StatusOK: true,
}

app := router.CreateRouter(&kube, &status, c.Bool("cors"))

srv := &http.Server{
Addr: ":" + c.String("port"),
Expand Down
19 changes: 19 additions & 0 deletions config
@@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNE1ERXhOVEE1TURRek5Gb1hEVEk0TURFeE16QTVNRFF6TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTkd2Cnl5RDlPcFB2RzRWSkZ3N2RWWVlqbVV2Z0VzQ09jNW4rcWFvVTZVa0NDRHZtRkxNK05KL1FWaFFuOXdDQWQxWDIKUjBna2doWHdUbFBtVHJhbkxqT0dMM3FFY0F5cElKaEVVaVBjeStJNSs0NTVrblJyUW9RNCtuR29FUGJodkdHSQo2VUovNWRjZ3l4cmQ3ZHJyeUc3bUJMQlpBME5LUFlDRldNY05HOFF4dEU3QU5SaExVR0MwVjQ5cUVCQWJQdCtiClE4WCtKRE5UT2U2dTRGcnhXS1kvR1lIOHpPVi85SDZBOFNsSFkySFgrL0dIaXpUNlJ6eWRETmpnVkxXeHU2YmwKclhSNTk5QmVWcUtXK3d3VFBrZHhSOUJBLzVrdUZrbGR0U3NvaWhEL0NKT3FEWDN3a0FoNFJ3Nm1YOXZXZzhQZwpMVURHYm0xcFhDZklIV0ZwMWRzQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFNajhWMGZIUGczbDkyakZuV2dQd1pLd282eHMKRytDa05EeERVQlg4cm5aTHpZM0lrNk1JOEZiNkZlTzhKbXlXUXB3L3hiNEpGc0J0eVZFVHVCNVFwc0k5OWhvRwo2MkRoa3NVekdYa1MzYkZBVTROVFhjTHZjRXo3eGxReGVvU0xCTndjRUFVV2V5dG1DWmYxSDBma1pWSG9BNGlQCmlFa0Z4SFA0NitkWjVRMlpadi9SSllEd0JBVURQN2NEMlIvSE1icFVHNjlZSXAyWVNTQnVJbTh0T2dYbm84TnEKV25ZYkxuQVNKMUZLQ1dCYnYvSE0zZktpS21vZmNWV0JKN3RPcjE0SEprak44YzQxVC9nRU9hZEVQRlVtdENRdQpCSDk4YlJUazlMbWJUVXMva1BlZVVValhMNi9ycFpocjVXeWJ0SUdBc1hZcFhpYmZNd3VoeHo5T2VTcz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://kubernetes:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJSHA5cXExSUF6Qzh3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBeE1UVXdPVEEwTXpSYUZ3MHhPVEF4TVRVd09UQTBNelZhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXA3V2xEUkp4LzUvMzJQckQKWndZUUljRE1sYVJGNi9wK1hmR3hSaGV0aFViNW4vVXFOdG5XandaUi9vRENsc2Zhakx0dzBwa0htTTBwL3Y0TwpGQ2haeXZHcXFFYUF3NXREN0hpb1dOeXIyQTcwSXhMOUJLN29uMGdBcWh0d3NKL1dpbkZsTE9LWFJ1SEQrYU0rCnpRWTV4WWJhVUpRSTVwOXRJZGN2dk1ueXFRNWxQRnNyOTQxVjBJa1pmWTdRa0NxL1VyOUxJV3JlbU8vT1Q4eXYKYVFSeXk2YjMwR29XM1lUMUlBa0NWTmtNVjdsYStWNTNRY2ZJclV4c3FyUy9MUTFWQ2F1NU5HY1UvaVRYZjBpQworK2E2aTFyMXZ5cTVmRUJNTzVhTklieWdvWFJ4ekV0eDJFcnBDdnRYeCtOZ0wwaGRjRXhuSzF4aHhNc2VtQjZkCng1dnFYd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFYjYvUk82TUltaVNOVE91RGxBS0tmZXhBUEtWdGlYNngvVwpOZHU1TUtyWU9udE5ZWTF3Z0llNUx0UHNpdXQwSmdvUHVjR0J1R2lDbkJGcTN6b2NhOUZqTXNBc3lyaWZ3WGNnCk85bHNVYVBXOFY1bTlhaEJqYzBtVEtla2o2eWtaM0FwaERKbUQvdW1reC85L1dRSlRQblNpUmdEUUVUcHZtb20KK0dlbk1zMmdKMXRJbThzTERQZUpFQk4wcTd0Y0s2cEM4M1NoYkk1OXhORzgyOU5MNUtzR09tR2psV0FpUnRnagp5a2R0RWFLUy85NlkwZThOK0pJVkNWZXZRYVlLeUdLbkNFTnFxQm1XV2IzaFlHaEhFN29keExhbUhnQjJJY0c5CnpXNEs1Z1lmMm5uMEhlY2NHR3lzakxPUjJDMEZNc0Y1THlSUERsYkRBUkw3VzUwRVIraz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBcDdXbERSSngvNS8zMlByRFp3WVFJY0RNbGFSRjYvcCtYZkd4UmhldGhVYjVuL1VxCk50bldqd1pSL29EQ2xzZmFqTHR3MHBrSG1NMHAvdjRPRkNoWnl2R3FxRWFBdzV0RDdIaW9XTnlyMkE3MEl4TDkKQks3b24wZ0FxaHR3c0ovV2luRmxMT0tYUnVIRCthTSt6UVk1eFliYVVKUUk1cDl0SWRjdnZNbnlxUTVsUEZzcgo5NDFWMElrWmZZN1FrQ3EvVXI5TElXcmVtTy9PVDh5dmFRUnl5NmIzMEdvVzNZVDFJQWtDVk5rTVY3bGErVjUzClFjZklyVXhzcXJTL0xRMVZDYXU1TkdjVS9pVFhmMGlDKythNmkxcjF2eXE1ZkVCTU81YU5JYnlnb1hSeHpFdHgKMkVycEN2dFh4K05nTDBoZGNFeG5LMXhoeE1zZW1CNmR4NXZxWHdJREFRQUJBb0lCQUZJRmNIZDRNZkRIRjd3eQp5Q1kydk9VRkdnbUZrZnpiUWJwSVhsN2RiYXZsRG1qQXlxaVEyWEtSc01BNzBPOVZSdm5WSWxTZEt5MzhDVElBCnBub05VZjBEUEdlWU01bDI4ajFRdFdUZ1o5ci9WVDlpSmdTSVZwU3Y3a05Ec0ZWeGxzZVd0ZGpCNlNKdGdCOHoKUzUrU0QrY2ZFTjZmeDhxaTVnaks2VG4zdW9vYkNYNHpxRE5Ic3hpcTRFNGxsZFFQWnM2K2o0a0Vyd3BRUzNPOApLUzZsaDg5cnM1VEtIWm54cVJmbmVPZlBqdUpJQjZqTjhKREMyejZWd3NCUVNVdHI1VFBWNnFjUGFpRUFlc3Q5CnVBdDlHZmZwYnBQY3JUelBicGZSSDNXclIreDVscmtxcENHWWd6Tm1FZ1Y5cHB1NEpHTkFmMmdjWTJkQ2s2SWcKNTZJTVhxRUNnWUVBeVhUMlE1Tit6bWROK1pacTNUUFY2MWIvTEg5UWhBTUUzQ29nS2xzQklOZldmeVNPYnpOVwpQaEQ1OUN4WjZKWk8rNlNPb2k4Qkc5RngxY0c1NlcwUDV5NVlrZnpwdjZzSDJDZDBvSlZSb3hWWkFpWkNqU2pwClRqT1QwZ05FWFhNODByakpiMjlsMkk1QURteW5xeGwzc2t2RnRPbTJxY0NQWGR4UnZ0bmFqd1VDZ1lFQTFSMmwKRW9IRTd2dmJYbEkvVEdNSDE1dThUTFNwdklPMmNPcTdsNGUrMWZSTUpuODJoelI5MjdEVzRsVEY0aFpuOWZXNAp1UkMwUGJ0YXlWYnJ2aHhYcFVIQjViY2pPdTZselA0OXUyb1daK1h0TW9aUnNGcm4ranhIcmtEcmZJZUFCYXZqCnFoSkhSRFkrZER2MWE3aXlTUGlYS1FqWVpIRnl0bUIwMXZ0bHFSTUNnWUF0eFY4MlYwcVdScTVXcUxBNjljNEMKbkU5NW9xdCtjdGtsMUlSZVd6TFViWFdVaVkrN0FDVDJFUkZaUXNKaXI5UGVKTE1yY2hhRVpYL0JtelJNTHJWUQpSeDFrZmR2MTZqdnNCbnJmQ1UzMUNhS2hkR1BtZ29jUGQzUjhZTFdscmNzQjdXNEczWDU4UjBXU2dXbE8zSWh2CmYvdTVjT2hKaFRDTm1NTVI0NWxlcVFLQmdGalJSd2VFZjcrZHhWbjA2eDU1c3ozL1VLV2pNWTQ0VW1PUzRHV1IKbEUzMHgzdlB6cmpnZm9kOWxzQ3kxZG1ZdnZENTNHZDhkN0cya0dxU3lDTHVDdlQ3aDhTbDQ0L2UvOGlFT05RUApyWkRWZVZweU1qOU9VNW10WDY1V3JnMnpKdjYyR1dhNndXckROSm9hbHZpeSs5Si9qL2Z4RU1ZanBjd3VQT09QCjJGbHpBb0dBWWdON2dicVJ0V3Q5bkR4YlRJdytQNWozR29ORlN4dHljWDA2cW5CVnl5K3YyaW82aE84SW13dkIKQkZEdmpmL1ZFZjVZMkdaNTlVVDRBNktDTmM3Yi93RkFmU2tzWmtjbEt2YlpEZU8wUmx2WHRzTUdjYjE4YmxBdwpvcDJyeWVJbUtxSm5KZHBwdUlQOWxOdG1ERjM3VmkzYlNzM2VmT01uSkdnd1NEQkV6cXM9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
35 changes: 35 additions & 0 deletions pkg/kubernetes/namespace.go
Expand Up @@ -3,6 +3,7 @@ package kubernetes
import (
log "github.com/sirupsen/logrus"
api_core "k8s.io/api/core/v1"
api_resource "k8s.io/apimachinery/pkg/api/resource"
api_meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -74,6 +75,40 @@ func (k *Kube) CreateNamespaceQuota(nsName string, quota *api_core.ResourceQuota
return quotaAfter, nil
}

//CreateNamespaceQuota creates namespace quota
func (k *Kube) CreateLimitRange(nsName string) error {
_, err := k.CoreV1().LimitRanges(nsName).Create(&api_core.LimitRange{
TypeMeta: api_meta.TypeMeta{
Kind: "LimitRange",
APIVersion: "v1",
},
ObjectMeta: api_meta.ObjectMeta{
Name: "limitrange",
Namespace: nsName,
},
Spec: api_core.LimitRangeSpec{
Limits: []api_core.LimitRangeItem{
{
Type: api_core.LimitTypeContainer,
Default: api_core.ResourceList{
api_core.ResourceCPU: api_resource.MustParse("200m"),
api_core.ResourceMemory: api_resource.MustParse("256Mi"),
},
DefaultRequest: api_core.ResourceList{
api_core.ResourceCPU: api_resource.MustParse("100m"),
api_core.ResourceMemory: api_resource.MustParse("128Mi"),
},
},
},
},
})
if err != nil {
log.WithField("Namespace", nsName).Error(err)
return err
}
return nil
}

//UpdateNamespaceQuota updates namespace quota
func (k *Kube) UpdateNamespaceQuota(nsName string, quota *api_core.ResourceQuota) (*api_core.ResourceQuota, error) {
quotaAfter, err := k.CoreV1().ResourceQuotas(nsName).Update(quota)
Expand Down
5 changes: 5 additions & 0 deletions pkg/router/handlers/namespace.go
Expand Up @@ -176,6 +176,11 @@ func CreateNamespace(ctx *gin.Context) {
return
}

if err := kube.CreateLimitRange(ns.ID); err != nil {
gonic.Gonic(model.ParseKubernetesResourceError(err, kubeerrors.ErrUnableCreateResource()), ctx)
return
}

ret, err := model.ParseKubeResourceQuota(quotaCreated)
if err != nil {
ctx.Error(err)
Expand Down
7 changes: 0 additions & 7 deletions pkg/router/midlleware/access.go
Expand Up @@ -35,13 +35,6 @@ const (
RoleAdmin = "admin"
)

func IsAdmin(ctx *gin.Context) {
if role := GetHeader(ctx, headers.UserRoleXHeader); role != RoleAdmin {
gonic.Gonic(kubeerrors.ErrAdminRequired(), ctx)
return
}
}

func ReadAccess(ctx *gin.Context) {
CheckAccess(ctx, readLevels)
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/router/midlleware/access_test.go
Expand Up @@ -9,6 +9,8 @@ import (
headers "github.com/containerum/utils/httputil"
"github.com/gin-gonic/gin"

"git.containerum.net/ch/kube-api/pkg/kubeerrors"
"github.com/containerum/utils/httputil"
. "github.com/smartystreets/goconvey/convey"
)

Expand Down Expand Up @@ -39,7 +41,7 @@ func TestContainsAccess(t *testing.T) {
func TestIsAdmin(t *testing.T) {
e := gin.New()
r := gofight.New()
e.GET("/test", IsAdmin, func(c *gin.Context) {
e.GET("/test", httputil.RequireAdminRole(kubeerrors.ErrAdminRequired), func(c *gin.Context) {
c.AbortWithStatus(http.StatusOK)
})
Convey("Test IsAdmin Middleware", t, func() {
Expand All @@ -52,7 +54,7 @@ func TestIsAdmin(t *testing.T) {
Convey("Check wrong User-Role", func() {
r.GET("/test").
SetHeader(gofight.H{
headers.UserRoleXHeader: "useradmin",
httputil.UserRoleXHeader: "useradmin",
}).
Run(e, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
So(r.Code, ShouldEqual, http.StatusForbidden)
Expand All @@ -61,7 +63,7 @@ func TestIsAdmin(t *testing.T) {
Convey("Check user User-Role", func() {
r.GET("/test").
SetHeader(gofight.H{
headers.UserRoleXHeader: RoleUser,
httputil.UserRoleXHeader: RoleUser,
}).
Run(e, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
So(r.Code, ShouldEqual, http.StatusForbidden)
Expand Down

0 comments on commit c857d23

Please sign in to comment.