You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2022/04/01 06:32:51 UTC

[skywalking-showcase] branch main updated: Add Satellite component and update the version of backend and ui (#18)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-showcase.git


The following commit(s) were added to refs/heads/main by this push:
     new 6146918  Add Satellite component and update the version of backend and ui (#18)
6146918 is described below

commit 614691863a55c458dd01ba8119dc5e25bf785095
Author: mrproliu <74...@qq.com>
AuthorDate: Fri Apr 1 14:32:47 2022 +0800

    Add Satellite component and update the version of backend and ui (#18)
---
 Makefile.in                                        |  6 +-
 deploy/platform/docker/Makefile                    |  5 ++
 deploy/platform/docker/docker-compose.agent.yaml   |  8 +--
 .../{Makefile => docker-compose.satellite.yaml}    | 31 +++++----
 deploy/platform/kubernetes/Makefile                |  5 ++
 .../kubernetes/feature-agent/resources.yaml        |  8 +--
 .../kubernetes/feature-cluster/permissions.yaml    | 10 +--
 .../kubernetes/feature-cluster/resources.yaml      |  6 +-
 .../permissions.yaml                               | 19 +++---
 .../kubernetes/feature-satellite/resources.yaml    | 75 ++++++++++++++++++++++
 .../feature-single-node/permissions.yaml           | 10 +--
 .../kubernetes/feature-single-node/resources.yaml  |  2 +-
 deploy/platform/kubernetes/features.mk             |  6 +-
 docs/readme.md                                     | 17 +++++
 14 files changed, 158 insertions(+), 50 deletions(-)

diff --git a/Makefile.in b/Makefile.in
index 68f79b9..4a419ad 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -26,8 +26,8 @@ TAG ?= $(shell git rev-parse --short HEAD)
 
 ES_IMAGE ?= docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.0
 
-SW_OAP_IMAGE ?= ghcr.io/apache/skywalking/oap:d3ea14d960ee52f66cc634307999feadea1b91e5
-SW_ROCKET_BOT_IMAGE ?= ghcr.io/apache/skywalking/ui:d3ea14d960ee52f66cc634307999feadea1b91e5
+SW_OAP_IMAGE ?= ghcr.io/apache/skywalking/oap:72e4b12dc3f05d697e9edfcce3f18483ef16c7c7
+SW_ROCKET_BOT_IMAGE ?= ghcr.io/apache/skywalking/ui:72e4b12dc3f05d697e9edfcce3f18483ef16c7c7
 SW_CLI_IMAGE ?= ghcr.io/apache/skywalking-cli/skywalking-cli:8c5725f2c4c47de6091748c1b4747f0d6047ad8e
 SW_EVENT_EXPORTER_IMAGE ?= ghcr.io/apache/skywalking-kubernetes-event-exporter/skywalking-kubernetes-event-exporter:8a012a3f968cb139f817189afb9b3748841bba22
 
@@ -35,6 +35,8 @@ SW_AGENT_JAVA_TAG ?= 8a0f79743cee6bc429218928f6afb296ed758ea4-java8
 SW_AGENT_NODEJS_BACKEND_VERSION ?= 2e7560518aff846befd4d6bc815fe5e38c704a11
 SW_AGENT_NODEJS_FRONTEND_VERSION ?= af0565a67d382b683c1dbd94c379b7080db61449
 
+SW_SATELLITE_IMAGE ?= ghcr.io/apache/skywalking-satellite/skywalking-satellite:ve82c277c943f832ceb28a6b80f483d5de2906036
+
 SWCK_OPERATOR_VERSION ?= 0.6.0
 CERT_MANAGER_VERSION ?= v1.3.1
 
diff --git a/deploy/platform/docker/Makefile b/deploy/platform/docker/Makefile
index f1197f3..fb83669 100644
--- a/deploy/platform/docker/Makefile
+++ b/deploy/platform/docker/Makefile
@@ -22,6 +22,11 @@ include Makefile.in
 features := $(subst $(comma), ,$(FEATURE_FLAGS))
 features := $(foreach f,$(features),-f docker-compose.$(f).yaml)
 
+BACKEND_SERVICE := oap
+ifneq (,$(findstring satellite,$(features)))
+	BACKEND_SERVICE := satellite
+endif
+
 .PHONY: deploy
 deploy:
 	docker-compose $(features) up -d
diff --git a/deploy/platform/docker/docker-compose.agent.yaml b/deploy/platform/docker/docker-compose.agent.yaml
index d9678a0..a31ba45 100644
--- a/deploy/platform/docker/docker-compose.agent.yaml
+++ b/deploy/platform/docker/docker-compose.agent.yaml
@@ -24,7 +24,7 @@ services:
     networks: [ sw ]
     environment:
       SW_AGENT_NAME: gateway
-      SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
+      SW_AGENT_COLLECTOR_BACKEND_SERVICES: ${BACKEND_SERVICE}:11800
     healthcheck:
       test: [ "CMD-SHELL", "curl http://localhost/actuator/health" ]
       interval: 30s
@@ -42,7 +42,7 @@ services:
     networks: [ sw ]
     environment:
       SW_AGENT_NAME: songs
-      SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
+      SW_AGENT_COLLECTOR_BACKEND_SERVICES: ${BACKEND_SERVICE}:11800
     healthcheck:
       test: [ "CMD-SHELL", "curl http://localhost/actuator/health" ]
       interval: 30s
@@ -58,7 +58,7 @@ services:
     networks: [ sw ]
     environment:
       SW_AGENT_NAME: recommendation
-      SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
+      SW_AGENT_COLLECTOR_BACKEND_SERVICES: ${BACKEND_SERVICE}:11800
     healthcheck:
       test: [ "CMD-SHELL", "curl http://localhost/health" ]
       interval: 30s
@@ -77,7 +77,7 @@ services:
     environment:
       SW_AGENT_NAME_SERVER: app
       REACT_APP_SW_AGENT_NAME_UI: ui
-      SW_AGENT_COLLECTOR_BACKEND_SERVICES: oap:11800
+      SW_AGENT_COLLECTOR_BACKEND_SERVICES: ${BACKEND_SERVICE}:11800
     healthcheck:
       test: [ "CMD-SHELL", "curl http://localhost/health" ]
       interval: 30s
diff --git a/deploy/platform/docker/Makefile b/deploy/platform/docker/docker-compose.satellite.yaml
similarity index 63%
copy from deploy/platform/docker/Makefile
copy to deploy/platform/docker/docker-compose.satellite.yaml
index f1197f3..1b34c93 100644
--- a/deploy/platform/docker/Makefile
+++ b/deploy/platform/docker/docker-compose.satellite.yaml
@@ -15,17 +15,24 @@
 # specific language governing permissions and limitations
 # under the License.
 #
+version: '2.1'
 
-include ../../../Makefile.in
-include Makefile.in
+services:
+  satellite:
+    image: ${SW_SATELLITE_IMAGE}
+    networks: [ sw ]
+    healthcheck:
+      test: [ "CMD-SHELL", "sh", "-c", "nc -zn 127.0.0.1 11800" ]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+    environment:
+      SATELLITE_GRPC_CLIENT_FINDER: static
+      SATELLITE_GRPC_CLIENT: oap:11800
+      SATELLITE_TELEMETRY_EXPORT_TYPE: metrics_service
+    depends_on:
+      oap:
+        condition: service_healthy
 
-features := $(subst $(comma), ,$(FEATURE_FLAGS))
-features := $(foreach f,$(features),-f docker-compose.$(f).yaml)
-
-.PHONY: deploy
-deploy:
-	docker-compose $(features) up -d
-
-.PHONY: undeploy
-undeploy:
-	docker-compose $(features) --log-level ERROR down
+networks:
+  sw:
\ No newline at end of file
diff --git a/deploy/platform/kubernetes/Makefile b/deploy/platform/kubernetes/Makefile
index 178ae19..a361a20 100644
--- a/deploy/platform/kubernetes/Makefile
+++ b/deploy/platform/kubernetes/Makefile
@@ -22,6 +22,11 @@ include Makefile.in
 features := $(subst $(comma), ,$(FEATURE_FLAGS))
 features := $(foreach f,$(features),feature-$(f))
 
+BACKEND_SERVICE := oap
+ifneq (,$(findstring satellite,$(features)))
+	BACKEND_SERVICE := satellite
+endif
+
 # Deploy
 deploy_features := $(foreach r,$(features),deploy.$(r))
 .PHONY: $(deploy_features)
diff --git a/deploy/platform/kubernetes/feature-agent/resources.yaml b/deploy/platform/kubernetes/feature-agent/resources.yaml
index 9c578ac..e49d3df 100644
--- a/deploy/platform/kubernetes/feature-agent/resources.yaml
+++ b/deploy/platform/kubernetes/feature-agent/resources.yaml
@@ -65,7 +65,7 @@ spec:
             - name: SW_AGENT_NAME
               value: agent::gateway
             - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
-              value: oap:11800
+              value: ${BACKEND_SERVICE}:11800
 
 ---
 apiVersion: v1
@@ -116,7 +116,7 @@ spec:
             - name: SW_AGENT_NAME
               value: agent::songs
             - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
-              value: oap:11800
+              value: ${BACKEND_SERVICE}:11800
 
 ---
 apiVersion: v1
@@ -160,7 +160,7 @@ spec:
             - name: SW_AGENT_NAME
               value: agent::recommendation
             - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
-              value: oap:11800
+              value: ${BACKEND_SERVICE}:11800
 
 ---
 apiVersion: v1
@@ -206,7 +206,7 @@ spec:
             - name: REACT_APP_SW_AGENT_NAME_UI
               value: agent::ui
             - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
-              value: oap:11800
+              value: ${BACKEND_SERVICE}:11800
 
 ---
 apiVersion: apps/v1
diff --git a/deploy/platform/kubernetes/feature-cluster/permissions.yaml b/deploy/platform/kubernetes/feature-cluster/permissions.yaml
index 7e94da8..02fda2d 100644
--- a/deploy/platform/kubernetes/feature-cluster/permissions.yaml
+++ b/deploy/platform/kubernetes/feature-cluster/permissions.yaml
@@ -19,13 +19,13 @@
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  name: skywalking-sa-cluster
+  name: skywalking-oap-sa-cluster
 
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
-  name: skywalking-sa-cluster-role
+  name: skywalking-oap-sa-cluster-role
 rules:
   - apiGroups: [ "" ]
     resources:
@@ -45,12 +45,12 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  name: skywalking-sa-cluster-role-binding
+  name: skywalking-oap-sa-cluster-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: skywalking-sa-cluster-role
+  name: skywalking-oap-sa-cluster-role
 subjects:
   - kind: ServiceAccount
-    name: skywalking-sa-cluster
+    name: skywalking-oap-sa-cluster
     namespace: ${NAMESPACE}
diff --git a/deploy/platform/kubernetes/feature-cluster/resources.yaml b/deploy/platform/kubernetes/feature-cluster/resources.yaml
index e07b318..d906b3c 100644
--- a/deploy/platform/kubernetes/feature-cluster/resources.yaml
+++ b/deploy/platform/kubernetes/feature-cluster/resources.yaml
@@ -135,7 +135,7 @@ spec:
       annotations:
         sidecar.istio.io/inject: "false"
     spec:
-      serviceAccountName: skywalking-sa-cluster
+      serviceAccountName: skywalking-oap-sa-cluster
       restartPolicy: Never
       initContainers:
         - name: wait-for-es
@@ -189,7 +189,7 @@ spec:
       annotations:
         sidecar.istio.io/inject: "false"
     spec:
-      serviceAccountName: skywalking-sa-cluster # @feature: cluster; set a service account with Pod "read" permission
+      serviceAccountName: skywalking-oap-sa-cluster # @feature: cluster; set a service account with Pod "read" permission
       initContainers:
         - name: wait-for-oap-init
           image: bitnami/kubectl:1.20.12
@@ -287,7 +287,7 @@ spec:
       annotations:
         sidecar.istio.io/inject: "false"
     spec:
-      serviceAccountName: skywalking-sa-cluster
+      serviceAccountName: skywalking-oap-sa-cluster
       containers:
         - name: rocket-bot
           image: ${SW_ROCKET_BOT_IMAGE}
diff --git a/deploy/platform/kubernetes/feature-single-node/permissions.yaml b/deploy/platform/kubernetes/feature-satellite/permissions.yaml
similarity index 67%
copy from deploy/platform/kubernetes/feature-single-node/permissions.yaml
copy to deploy/platform/kubernetes/feature-satellite/permissions.yaml
index e2206fb..43f90fc 100644
--- a/deploy/platform/kubernetes/feature-single-node/permissions.yaml
+++ b/deploy/platform/kubernetes/feature-satellite/permissions.yaml
@@ -19,33 +19,30 @@
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  name: skywalking-sa
+  name: skywalking-satellite-sa
+  namespace: ${NAMESPACE}
 
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
-  name: skywalking-sa-role
+  name: skywalking-satellite-sa-role
 rules:
   - apiGroups: [ "" ]
     resources:
-      - "pods" # @feature: als; OAP needs to read pods metadata to analyze the access logs
-      - "services" # @feature: als; OAP needs to read services metadata to analyze the access logs
-      - "endpoints" # @feature: als; OAP needs to read endpoints metadata to analyze the access logs
-      - "nodes" # @feature: als; OAP needs to read nodes metadata to analyze the access logs
-      - "configmaps"
+      - "pods"
     verbs: [ "get", "watch", "list" ]
 
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  name: skywalking-sa-role-binding
+  name: skywalking-satellite-sa-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: skywalking-sa-role
+  name: skywalking-satellite-sa-role
 subjects:
   - kind: ServiceAccount
-    name: skywalking-sa
-    namespace: ${NAMESPACE}
+    name: skywalking-satellite-sa
+    namespace: ${NAMESPACE}
\ No newline at end of file
diff --git a/deploy/platform/kubernetes/feature-satellite/resources.yaml b/deploy/platform/kubernetes/feature-satellite/resources.yaml
new file mode 100644
index 0000000..cad94d9
--- /dev/null
+++ b/deploy/platform/kubernetes/feature-satellite/resources.yaml
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+apiVersion: v1
+kind: Service
+metadata:
+  name: satellite
+spec:
+  selector:
+    app: satellite
+  ports:
+    - name: grpc
+      port: 11800
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: satellite-deployment
+  labels:
+    app: satellite
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: satellite
+  template:
+    metadata:
+      labels:
+        app: satellite
+      annotations:
+        sidecar.istio.io/inject: "false"
+    spec:
+      serviceAccountName: skywalking-satellite-sa
+      containers:
+        - name: satellite
+          image: ${SW_SATELLITE_IMAGE}
+          imagePullPolicy: Always
+          resources:
+            limits:
+              cpu: 500m
+              memory: "512Mi"
+            requests:
+              cpu: 500m
+              memory: "512Mi"
+          ports:
+            - name: grpc
+              containerPort: 11800
+          env:
+            - name: SATELLITE_GRPC_CLIENT_FINDER
+              value: kubernetes
+            - name: SATELLITE_GRPC_CLIENT_KUBERNETES_NAMESPACE
+              value: ${NAMESPACE}
+            - name: SATELLITE_GRPC_CLIENT_KUBERNETES_KIND
+              value: pod
+            - name: SATELLITE_GRPC_CLIENT_KUBERNETES_SELECTOR_LABEL
+              value: app=oap
+            - name: SATELLITE_GRPC_CLIENT_KUBERNETES_EXTRA_PORT
+              value: "11800"
+            - name: SATELLITE_TELEMETRY_EXPORT_TYPE
+              value: metrics_service
\ No newline at end of file
diff --git a/deploy/platform/kubernetes/feature-single-node/permissions.yaml b/deploy/platform/kubernetes/feature-single-node/permissions.yaml
index e2206fb..431c401 100644
--- a/deploy/platform/kubernetes/feature-single-node/permissions.yaml
+++ b/deploy/platform/kubernetes/feature-single-node/permissions.yaml
@@ -19,13 +19,13 @@
 apiVersion: v1
 kind: ServiceAccount
 metadata:
-  name: skywalking-sa
+  name: skywalking-oap-sa
 
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
-  name: skywalking-sa-role
+  name: skywalking-oap-sa-role
 rules:
   - apiGroups: [ "" ]
     resources:
@@ -40,12 +40,12 @@ rules:
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
-  name: skywalking-sa-role-binding
+  name: skywalking-oap-sa-role-binding
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: skywalking-sa-role
+  name: skywalking-oap-sa-role
 subjects:
   - kind: ServiceAccount
-    name: skywalking-sa
+    name: skywalking-oap-sa
     namespace: ${NAMESPACE}
diff --git a/deploy/platform/kubernetes/feature-single-node/resources.yaml b/deploy/platform/kubernetes/feature-single-node/resources.yaml
index 8e7c755..5b1fae9 100644
--- a/deploy/platform/kubernetes/feature-single-node/resources.yaml
+++ b/deploy/platform/kubernetes/feature-single-node/resources.yaml
@@ -135,7 +135,7 @@ spec:
       annotations:
         sidecar.istio.io/inject: "false"
     spec:
-      serviceAccountName: skywalking-sa # @feature: als; set a service account with Pods/Endpoints/Services/Nodes permissions to analyze Envoy access logs
+      serviceAccountName: skywalking-oap-sa # @feature: als; set a service account with Pods/Endpoints/Services/Nodes permissions to analyze Envoy access logs
       containers:
         - name: oap
           image: ${SW_OAP_IMAGE}
diff --git a/deploy/platform/kubernetes/features.mk b/deploy/platform/kubernetes/features.mk
index b2dc110..774dae8 100644
--- a/deploy/platform/kubernetes/features.mk
+++ b/deploy/platform/kubernetes/features.mk
@@ -39,8 +39,8 @@ istio:
 		--set 'meshConfig.defaultConfig.proxyStatsMatcher.inclusionRegexps[5]=.*upstream_rq_pending_active.*' \
 		--set 'meshConfig.defaultConfig.proxyStatsMatcher.inclusionRegexps[6]=.*lb_healthy_panic.*' \
 		--set 'meshConfig.defaultConfig.proxyStatsMatcher.inclusionRegexps[7]=.*upstream_cx_none_healthy.*' \
-		--set meshConfig.defaultConfig.envoyMetricsService.address=oap.$(NAMESPACE):11800 `# @feature: als; set MetricsService address to OAP so Envoy emits metrics to OAP` \
-		--set meshConfig.defaultConfig.envoyAccessLogService.address=oap.$(NAMESPACE):11800 `# @feature: als; set AccessLogService address to OAP so Envoy emits logs to OAP`
+		--set meshConfig.defaultConfig.envoyMetricsService.address=$(BACKEND_SERVICE).$(NAMESPACE):11800 `# @feature: als; set MetricsService address to Backend Service so Envoy emits metrics to Backend Service` \
+		--set meshConfig.defaultConfig.envoyAccessLogService.address=$(BACKEND_SERVICE).$(NAMESPACE):11800 `# @feature: als; set AccessLogService address to Backend Service so Envoy emits logs to Backend Service`
 
 .PHONY: namespace
 namespace:
@@ -98,7 +98,7 @@ deploy.feature-java-agent-injector: install-cert-manager
 	@curl -Ls https://archive.apache.org/dist/skywalking/swck/${SWCK_OPERATOR_VERSION}/skywalking-swck-${SWCK_OPERATOR_VERSION}-bin.tgz | tar -zxf - -O ./config/operator-bundle.yaml | kubectl apply -f -
 	@kubectl label namespace --overwrite $(NAMESPACE) swck-injection=enabled
 	# @feature: java-agent-injector; we can update the agent's backend address in a single-node cluster firstly so that we don't need to add the same backend env for every java agent
-	@kubectl get configmap skywalking-swck-java-agent-configmap -n skywalking-swck-system -oyaml | sed "s/127.0.0.1/$(NAMESPACE)-oap.$(NAMESPACE)/" | kubectl apply -f -
+	@kubectl get configmap skywalking-swck-java-agent-configmap -n skywalking-swck-system -oyaml | sed "s/127.0.0.1/$(NAMESPACE)-$(BACKEND_SERVICE).$(NAMESPACE)/" | kubectl apply -f -
 	$(MAKE) deploy FEATURE_FLAGS=agent AGENTLESS=false SHOW_TIPS=false
 
 # @feature: java-agent-injector; uninstall the swck operator and cert-manager
diff --git a/docs/readme.md b/docs/readme.md
index 144458c..c0c86e3 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -87,6 +87,7 @@ Currently, the features supported are:
 | `kubernetes-monitor` | Deploy OpenTelemetry and export Kubernetes monitoring metrics to SkyWalking for analysis and display on UI. | |
 | `istiod-monitor`     | Deploy OpenTelemetry and export Istio control plane metrics to SkyWalking for analysis and display on UI. | |
 | `event`       | Deploy tools to trigger events, and SkyWalking Kubernetes event exporter to export events into SkyWalking. | |
+| `satellite`   | Deploy SkyWalking Satellite to load balance the monitoring data.| |
 
 ### Kubernetes
 
@@ -126,3 +127,19 @@ make undeploy.docker
 # Redeploy
 make redeploy.docker # equivalent to make undeploy.docker deploy.docker
 ```
+
+## Traffic Flow
+
+After deploy the showcase, the business system would send monitoring traffic to the OAP node, and one agent/sidecar connect to one OAP node directly.
+
+### Satellite
+If the business traffic is unbalanced, it would cause the OAP node receive unbalanced monitoring data. So, you could add the Satellite component.
+After deploy the showcase with the satellite component, the monitoring traffic would send to the Satellite service, and satellite
+load balances the traffic to the OAP nodes.
+
+```mermaid
+%% please read this doc in our official website, otherwise the graph is not correctly rendered. 
+graph LR;
+  agent["business app(agent)"] --> satellite("satellite") --> oap("oap");
+  envoy["sidecar(envoy)"] --> satellite;
+```
\ No newline at end of file