mirror of https://github.com/helm/helm
Merge f87510863e
into 84bb77f183
commit
e830c549de
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors 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 e2e_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestE2e(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "E2e Suite")
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors 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 e2e
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
)
|
||||
|
||||
var _ = Describe("Basic Suite", func() {
|
||||
var helm HelmManager
|
||||
var namespace *v1.Namespace
|
||||
var clientset kubernetes.Interface
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
clientset, err = KubeClient()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
By("Creating namespace and initializing test framework")
|
||||
namespaceObj := &v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "e2e-helm-",
|
||||
},
|
||||
}
|
||||
namespace, err = clientset.Core().Namespaces().Create(namespaceObj)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
helm = &BinaryHelmManager{
|
||||
Namespace: namespace.Name,
|
||||
Clientset: clientset,
|
||||
HelmBin: helmBinPath,
|
||||
TillerHost: tillerHost,
|
||||
UseCanary: true,
|
||||
UseServiceAccount: true,
|
||||
}
|
||||
if !localTiller {
|
||||
Expect(helm.InstallTiller()).NotTo(HaveOccurred())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("Removing namespace")
|
||||
DeleteNS(clientset, namespace)
|
||||
})
|
||||
|
||||
It("Should be possible to create/delete/upgrade/rollback and check status of wordpress chart", func() {
|
||||
chartName := "stable/wordpress"
|
||||
By("Install chart stable/wordpress")
|
||||
releaseName, err := helm.Install(chartName, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
By("Check status of release " + releaseName)
|
||||
Expect(helm.Status(releaseName)).NotTo(HaveOccurred())
|
||||
By("Upgrading release " + releaseName)
|
||||
Expect(helm.Upgrade(chartName, releaseName, map[string]string{"image": "bitnami/wordpress:4.7.3-r1"})).NotTo(HaveOccurred())
|
||||
By("Rolling back release " + releaseName + "to a first revision")
|
||||
Expect(helm.Rollback(releaseName, 1)).NotTo(HaveOccurred())
|
||||
By("Deleting release " + releaseName)
|
||||
Expect(helm.Delete(releaseName)).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
@ -0,0 +1,280 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors 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 e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// HelmManager provides functionality to install client/server helm and use it
|
||||
type HelmManager interface {
|
||||
// InstallTiller will bootstrap tiller pod in k8s
|
||||
InstallTiller() error
|
||||
// DeleteTiller removes tiller pod from k8s
|
||||
DeleteTiller(removeHelmHome bool) error
|
||||
// Install chart, returns releaseName and error
|
||||
Install(chartName string, values map[string]string) (string, error)
|
||||
// Status verifies state of installed release
|
||||
Status(releaseName string) error
|
||||
// Delete release
|
||||
Delete(releaseName string) error
|
||||
// Upgrade release
|
||||
Upgrade(chartName, releaseName string, values map[string]string) error
|
||||
// Rollback release
|
||||
Rollback(releaseName string, revision int) error
|
||||
}
|
||||
|
||||
// BinaryHelmManager uses helm binary to work with helm server
|
||||
type BinaryHelmManager struct {
|
||||
Clientset kubernetes.Interface
|
||||
Namespace string
|
||||
HelmBin string
|
||||
TillerHost string
|
||||
UseCanary bool
|
||||
UseServiceAccount bool
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) InstallTiller() error {
|
||||
arg := make([]string, 0, 5)
|
||||
var err error
|
||||
arg = append(arg, "init", "--tiller-namespace", m.Namespace)
|
||||
if m.UseCanary {
|
||||
arg = append(arg, "--canary-image")
|
||||
}
|
||||
if m.UseServiceAccount {
|
||||
arg = append(arg, "--service-account", "tiller")
|
||||
if err = m.InstallServiceAccounts(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = m.executeUsingHelm(arg...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
By("Waiting for tiller pod")
|
||||
waitTillerPod(m.Clientset, m.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) DeleteTiller(removeHelmHome bool) error {
|
||||
arg := []string{}
|
||||
arg = append(arg, "reset", "--tiller-namespace", m.Namespace, "--force")
|
||||
if removeHelmHome {
|
||||
arg = append(arg, "--remove-helm-home")
|
||||
}
|
||||
_, err := m.executeUsingHelm(arg...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) Install(chartName string, values map[string]string) (string, error) {
|
||||
stdout, err := m.executeCommandWithValues(chartName, "install", values)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return getNameFromHelmOutput(stdout), nil
|
||||
}
|
||||
|
||||
// Status reports nil if release is considered to be succesfull
|
||||
func (m *BinaryHelmManager) Status(releaseName string) error {
|
||||
stdout, err := m.executeUsingHelm("status", releaseName, "--tiller-namespace", m.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := getStatusFromHelmOutput(stdout)
|
||||
if status == "DEPLOYED" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Expected status is DEPLOYED. But got %v for release %v.", status, releaseName)
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) Delete(releaseName string) error {
|
||||
_, err := m.executeUsingHelm("delete", releaseName, "--tiller-namespace", m.Namespace)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) Upgrade(chartName, releaseName string, values map[string]string) error {
|
||||
arg := make([]string, 0, 9)
|
||||
arg = append(arg, "upgrade", releaseName, chartName)
|
||||
if len(values) > 0 {
|
||||
arg = append(arg, "--set", prepareArgsFromValues(values))
|
||||
}
|
||||
_, err := m.executeUsingHelmInNamespace(arg...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) Rollback(releaseName string, revision int) error {
|
||||
arg := make([]string, 0, 6)
|
||||
arg = append(arg, "rollback", releaseName, strconv.Itoa(revision), "--tiller-namespace", m.Namespace)
|
||||
_, err := m.executeUsingHelm(arg...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) executeUsingHelmInNamespace(arg ...string) (string, error) {
|
||||
arg = append(arg, "--namespace", m.Namespace, "--tiller-namespace", m.Namespace)
|
||||
return m.executeUsingHelm(arg...)
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) executeUsingHelm(arg ...string) (string, error) {
|
||||
if m.TillerHost != "" {
|
||||
arg = append(arg, "--host", m.TillerHost)
|
||||
}
|
||||
return m.executeUsingBinary(m.HelmBin, arg...)
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) executeUsingBinary(binary string, arg ...string) (string, error) {
|
||||
cmd := exec.Command(binary, arg...)
|
||||
Logf("Running command %+v\n", cmd.Args)
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *exec.ExitError:
|
||||
stderr := err.(*exec.ExitError)
|
||||
Logf("Command %+v, Err %s\n", cmd.Args, stderr.Stderr)
|
||||
case *exec.Error:
|
||||
Logf("Command %+v, Err %s\n", cmd.Args, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) executeCommandWithValues(releaseName, command string, values map[string]string) (string, error) {
|
||||
arg := make([]string, 0, 8)
|
||||
arg = append(arg, command, releaseName)
|
||||
if len(values) > 0 {
|
||||
vals := prepareArgsFromValues(values)
|
||||
arg = append(arg, "--set", vals)
|
||||
}
|
||||
return m.executeUsingHelmInNamespace(arg...)
|
||||
}
|
||||
|
||||
func (m *BinaryHelmManager) InstallServiceAccounts() error {
|
||||
objects := strings.Replace(serviceAccountTemplate, "TILLER_NAMESPACE", m.Namespace, -1)
|
||||
|
||||
f, err := ioutil.TempFile("", m.Namespace)
|
||||
if err != nil {
|
||||
Logf("Failed creating tempfile: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
f.WriteString(objects)
|
||||
f.Sync()
|
||||
|
||||
_, err = m.executeUsingBinary("kubectl", "create", "-f", f.Name())
|
||||
return err
|
||||
}
|
||||
|
||||
func regexpKeyFromStructuredOutput(key, output string) string {
|
||||
r := regexp.MustCompile(fmt.Sprintf("%v:[[:space:]]*(.*)", key))
|
||||
// key will be captured in group with index 1
|
||||
result := r.FindStringSubmatch(output)
|
||||
if len(result) < 2 {
|
||||
return ""
|
||||
}
|
||||
return result[1]
|
||||
}
|
||||
|
||||
func getNameFromHelmOutput(output string) string {
|
||||
return regexpKeyFromStructuredOutput("NAME", output)
|
||||
}
|
||||
|
||||
func getStatusFromHelmOutput(output string) string {
|
||||
return regexpKeyFromStructuredOutput("STATUS", output)
|
||||
}
|
||||
|
||||
func waitTillerPod(clientset kubernetes.Interface, namespace string) {
|
||||
Eventually(func() bool {
|
||||
pods, err := clientset.Core().Pods(namespace).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
if !strings.Contains(pod.Name, "tiller") {
|
||||
continue
|
||||
}
|
||||
Logf("Found tiller pod. Phase %v\n", pod.Status.Phase)
|
||||
if pod.Status.Phase != v1.PodRunning {
|
||||
return false
|
||||
}
|
||||
for _, cond := range pod.Status.Conditions {
|
||||
if cond.Type != v1.PodReady {
|
||||
continue
|
||||
}
|
||||
return cond.Status == v1.ConditionTrue
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, 2*time.Minute, 5*time.Second).Should(BeTrue(), "tiller pod is not running in namespace "+namespace)
|
||||
}
|
||||
|
||||
func prepareArgsFromValues(values map[string]string) string {
|
||||
var b bytes.Buffer
|
||||
for key, val := range values {
|
||||
b.WriteString(key)
|
||||
b.WriteString("=")
|
||||
b.WriteString(val)
|
||||
b.WriteString(",")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var serviceAccountTemplate = `
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: tiller
|
||||
namespace: TILLER_NAMESPACE
|
||||
---
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: tiller-manager
|
||||
namespace: TILLER_NAMESPACE
|
||||
rules:
|
||||
- apiGroups: ["", "extensions", "apps", "*"]
|
||||
resources: ["*"]
|
||||
verbs: ["*"]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: tiller-binding
|
||||
namespace: TILLER_NAMESPACE
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: tiller
|
||||
namespace: TILLER_NAMESPACE
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: tiller-manager
|
||||
apiGroup: rbac.authorization.k8s.io`
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors 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 e2e
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/api/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var url string
|
||||
var tillerHost string
|
||||
var helmBinPath string
|
||||
var localTiller bool
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&url, "cluster-url", "http://127.0.0.1:8080", "apiserver address to use with restclient")
|
||||
flag.StringVar(&tillerHost, "tiller-host", "", "tiller address")
|
||||
flag.StringVar(&helmBinPath, "helm-bin", "helm", "helm binary to test")
|
||||
flag.BoolVar(&localTiller, "local-tiller", false, "wait for tiller pod")
|
||||
}
|
||||
|
||||
func LoadConfig() *rest.Config {
|
||||
config, err := clientcmd.BuildConfigFromFlags(url, "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return config
|
||||
}
|
||||
|
||||
func KubeClient() (*kubernetes.Clientset, error) {
|
||||
config := LoadConfig()
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return clientset, nil
|
||||
}
|
||||
|
||||
func DeleteNS(clientset kubernetes.Interface, namespace *v1.Namespace) {
|
||||
defer GinkgoRecover()
|
||||
err := clientset.Core().Namespaces().Delete(namespace.Name, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
func Logf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(GinkgoWriter, format, a...)
|
||||
}
|
||||
|
||||
func WaitForPod(clientset kubernetes.Interface, namespace string, name string, phase v1.PodPhase) *v1.Pod {
|
||||
defer GinkgoRecover()
|
||||
var podUpdated *v1.Pod
|
||||
Eventually(func() error {
|
||||
podUpdated, err := clientset.Core().Pods(namespace).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if phase != "" && podUpdated.Status.Phase != phase {
|
||||
return fmt.Errorf("pod %v is not %v phase: %v", podUpdated.Name, phase, podUpdated.Status.Phase)
|
||||
}
|
||||
return nil
|
||||
}, 1*time.Minute, 3*time.Second).Should(BeNil())
|
||||
return podUpdated
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors 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.
|
||||
|
||||
scripts/portforward.sh 8080&
|
||||
|
||||
wget https://cdn.rawgit.com/Mirantis/kubeadm-dind-cluster/master/fixed/dind-cluster-v1.7.sh
|
||||
chmod +x dind-cluster-v1.7.sh
|
||||
RUN_ON_BTRFS_ANYWAY=trololo bash -x ./dind-cluster-v1.7.sh up
|
||||
export PATH="$HOME/.kubeadm-dind-cluster:$PATH"
|
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors 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.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
set -o xtrace
|
||||
|
||||
IMAGE_REPO=${IMAGE_REPO:-gcr.io/kubernetes-helm/tiller}
|
||||
IMAGE_TAG=${IMAGE_TAG:-canary}
|
||||
TMP_IMAGE_PATH=${TMP_IMAGE_PATH:-/tmp/image.tar}
|
||||
NODE_PATTERN=${NODE_PATTERN:-"kube-node-"}
|
||||
|
||||
|
||||
function import-image {
|
||||
docker save ${IMAGE_REPO}:${IMAGE_TAG} -o "${TMP_IMAGE_PATH}"
|
||||
|
||||
for node in `docker ps --format "{{.Names}}" | grep ${NODE_PATTERN}`;
|
||||
do
|
||||
docker cp "${TMP_IMAGE_PATH}" $node:/image.tar
|
||||
docker exec -ti "$node" docker load -i /image.tar
|
||||
done
|
||||
|
||||
set +o xtrace
|
||||
echo "Finished copying docker image to dind nodes"
|
||||
}
|
||||
|
||||
import-image
|
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors 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.
|
||||
|
||||
# Portforward hack for CircleCI remote docker
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
set -o errtrace
|
||||
|
||||
if [[ ${1:-} = start ]]; then
|
||||
docker run -d -it \
|
||||
--name portforward --net=host \
|
||||
--entrypoint /bin/sh \
|
||||
bobrik/socat -c "while true; do sleep 1000; done"
|
||||
elif [[ ${1} ]]; then
|
||||
socat "TCP-LISTEN:${1},reuseaddr,fork" \
|
||||
EXEC:"'docker exec -i portforward socat STDIO TCP-CONNECT:localhost:${1}'"
|
||||
else
|
||||
echo "Must specify either start or the port number" >&2
|
||||
exit 1
|
||||
fi
|
Loading…
Reference in new issue