From 732bb019a84105a475035cf638c31c332a2c102e Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Mon, 28 May 2018 12:50:43 -0400 Subject: [PATCH 1/4] Add support for kubernetes --- Gopkg.lock | 349 --------------------------------- Gopkg.toml | 83 -------- Makefile | 2 +- cmd/ltops/create.go | 36 +++- cmd/ltops/delete.go | 3 +- cmd/ltops/deploy.go | 20 +- cmd/ltops/load.go | 94 +++++++++ cmd/ltops/loadtest.go | 8 +- cmd/ltops/root.go | 1 + cmd/ltops/ssh.go | 74 +++---- cmd/ltops/status.go | 14 +- glide.yaml | 32 +++ kubernetes/cluster.go | 186 ++++++++++++++++++ kubernetes/cluster_connect.go | 28 +++ kubernetes/cluster_create.go | 97 +++++++++ kubernetes/cluster_deploy.go | 148 ++++++++++++++ kubernetes/cluster_loadtest.go | 109 ++++++++++ ltops/cluster.go | 7 + sshtools/kubetools.go | 36 ++++ terraform/cluster.go | 8 + terraform/cluster_create.go | 6 + terraform/cluster_deploy.go | 33 +--- terraform/cluster_load.go | 26 --- 23 files changed, 854 insertions(+), 546 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml create mode 100644 cmd/ltops/load.go create mode 100644 glide.yaml create mode 100644 kubernetes/cluster.go create mode 100644 kubernetes/cluster_connect.go create mode 100644 kubernetes/cluster_create.go create mode 100644 kubernetes/cluster_deploy.go create mode 100644 kubernetes/cluster_loadtest.go create mode 100644 sshtools/kubetools.go delete mode 100644 terraform/cluster_load.go diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index fb5b4998..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,349 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/VividCortex/ewma" - packages = ["."] - revision = "b24eb346a94c3ba12c1da1e564dbac1b498a77ce" - version = "v1.1.1" - -[[projects]] - branch = "master" - name = "github.com/corpix/uarand" - packages = ["."] - revision = "2b8494104d86337cdd41d0a49cbed8e4583c0ab4" - -[[projects]] - branch = "master" - name = "github.com/dimchansky/utfbom" - packages = ["."] - revision = "6c6132ff69f0f6c088739067407b5d32c52e1d0f" - -[[projects]] - branch = "master" - name = "github.com/dustin/go-humanize" - packages = ["."] - revision = "02af3965c54e8cacf948b97fef38925c4120652c" - -[[projects]] - name = "github.com/fsnotify/fsnotify" - packages = ["."] - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" - -[[projects]] - name = "github.com/go-ini/ini" - packages = ["."] - revision = "6529cf7c58879c08d927016dde4477f18a0634cb" - version = "v1.36.0" - -[[projects]] - name = "github.com/go-sql-driver/mysql" - packages = ["."] - revision = "a0583e0143b1624142adab07e0e97fe106d99561" - version = "v1.3" - -[[projects]] - branch = "master" - name = "github.com/gorilla/websocket" - packages = ["."] - revision = "21ab95fa12b9bdd8fecf5fa3586aad941cc98785" - -[[projects]] - branch = "master" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/printer", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token" - ] - revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" - -[[projects]] - branch = "master" - name = "github.com/icrowley/fake" - packages = ["."] - revision = "4178557ae428460c3780a381c824a1f3aceb6325" - -[[projects]] - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - branch = "master" - name = "github.com/jmoiron/sqlx" - packages = [ - ".", - "reflectx" - ] - revision = "2aeb6a910c2b94f2d5eb53d9895d80e27264ec41" - -[[projects]] - branch = "master" - name = "github.com/lib/pq" - packages = [ - ".", - "oid" - ] - revision = "d34b9ff171c21ad295489235aec8b6626023cd04" - -[[projects]] - name = "github.com/magiconair/properties" - packages = ["."] - revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" - version = "v1.7.6" - -[[projects]] - branch = "master" - name = "github.com/mattermost/html2text" - packages = ["."] - revision = "d47a5532a7bc36ad7b2b8ec3eebe24e975154f94" - -[[projects]] - name = "github.com/mattermost/mattermost-server" - packages = [ - "einterfaces", - "mlog", - "model", - "utils", - "utils/jsonutils", - "utils/markdown" - ] - revision = "fbbe1f7cefd52a27fd52893509b5365d275f9bee" - -[[projects]] - name = "github.com/minio/minio-go" - packages = [ - ".", - "pkg/credentials", - "pkg/encrypt", - "pkg/s3signer", - "pkg/s3utils", - "pkg/set" - ] - revision = "c6108c47ba5d86548404ebf9e51c5ca18769fe79" - version = "6.0.1" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" - -[[projects]] - branch = "master" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - revision = "00c29f56e2386353d58c599509e8dc3801b0d716" - -[[projects]] - name = "github.com/montanaflynn/stats" - packages = ["."] - revision = "eeaced052adbcfeea372c749c281099ed7fdaa38" - version = "0.2.0" - -[[projects]] - name = "github.com/nicksnyder/go-i18n" - packages = [ - "i18n", - "i18n/bundle", - "i18n/language", - "i18n/translation" - ] - revision = "0dc1626d56435e9d605a29875701721c54bc9bbd" - version = "v1.10.0" - -[[projects]] - name = "github.com/paulbellamy/ratecounter" - packages = ["."] - revision = "524851a93235ac051e3540563ed7909357fe24ab" - version = "v0.2.0" - -[[projects]] - name = "github.com/pborman/uuid" - packages = ["."] - revision = "e790cca94e6cc75c7064b1332e63811d4aae1a53" - version = "v1.1" - -[[projects]] - name = "github.com/pelletier/go-toml" - packages = ["."] - revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" - version = "v1.1.0" - -[[projects]] - name = "github.com/pkg/errors" - packages = ["."] - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - name = "github.com/sirupsen/logrus" - packages = ["."] - revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" - version = "v1.0.5" - -[[projects]] - name = "github.com/spf13/afero" - packages = [ - ".", - "mem" - ] - revision = "63644898a8da0bc22138abf860edaf5277b6102e" - version = "v1.1.0" - -[[projects]] - name = "github.com/spf13/cast" - packages = ["."] - revision = "8965335b8c7107321228e3e3702cab9832751bac" - version = "v1.2.0" - -[[projects]] - name = "github.com/spf13/cobra" - packages = ["."] - revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4" - version = "v0.0.2" - -[[projects]] - branch = "master" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" - -[[projects]] - name = "github.com/spf13/pflag" - packages = ["."] - revision = "583c0c0531f06d5278b7d917446061adc344b5cd" - version = "v1.0.1" - -[[projects]] - branch = "env-settings" - name = "github.com/spf13/viper" - packages = ["."] - revision = "4f5003aa93559718c866d86fbc795439079484f5" - source = "https://github.com/mattermost/viper" - -[[projects]] - name = "go.uber.org/atomic" - packages = ["."] - revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" - version = "v1.3.2" - -[[projects]] - name = "go.uber.org/multierr" - packages = ["."] - revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" - version = "v1.1.0" - -[[projects]] - name = "go.uber.org/zap" - packages = [ - ".", - "buffer", - "internal/bufferpool", - "internal/color", - "internal/exit", - "zapcore" - ] - revision = "eeedf312bc6c57391d84767a4cd413f02a917974" - version = "v1.8.0" - -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = [ - "argon2", - "bcrypt", - "blake2b", - "blowfish", - "curve25519", - "ed25519", - "ed25519/internal/edwards25519", - "internal/chacha20", - "poly1305", - "ssh", - "ssh/terminal" - ] - revision = "1f94bef427e370e425e55b172f3b5e300ef38304" - -[[projects]] - branch = "master" - name = "golang.org/x/net" - packages = [ - "html", - "html/atom", - "idna", - "lex/httplex" - ] - revision = "640f4622ab692b87c2f3a94265e6f579fe38263d" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = [ - "cpu", - "unix", - "windows" - ] - revision = "6f686a352de66814cdd080d970febae7767857a3" - -[[projects]] - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable" - ] - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - branch = "v3" - name = "gopkg.in/alexcesaro/quotedprintable.v3" - packages = ["."] - revision = "2caba252f4dc53eaf6b553000885530023f54623" - -[[projects]] - name = "gopkg.in/gomail.v2" - packages = ["."] - revision = "41f3572897373c5538c50a2402db15db079fa4fd" - version = "2.0.0" - -[[projects]] - name = "gopkg.in/natefinch/lumberjack.v2" - packages = ["."] - revision = "a96e63847dc3c67d17befa69c303767e2f84e54f" - version = "v2.1" - -[[projects]] - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "6f6bb2b816e591fed1612e6c95d8c7bbc5623850098aa906ab3c27f6cf95f56d" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 6db44b54..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,83 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - -[[constraint]] - name = "github.com/VividCortex/ewma" - version = "1.1.1" - -[[constraint]] - name = "github.com/go-sql-driver/mysql" - version = "1.3.0" - -[[constraint]] - branch = "master" - name = "github.com/icrowley/fake" - -[[constraint]] - branch = "master" - name = "github.com/jmoiron/sqlx" - -[[constraint]] - branch = "master" - name = "github.com/lib/pq" - -[[constraint]] - name = "github.com/mattermost/mattermost-server" - revision ="fbbe1f7cefd52a27fd52893509b5365d275f9bee" - -[[constraint]] - name = "github.com/montanaflynn/stats" - version = "0.2.0" - -[[constraint]] - name = "github.com/paulbellamy/ratecounter" - version = "0.2.0" - -[[constraint]] - name = "github.com/pkg/errors" - version = "0.8.0" - -[[constraint]] - name = "github.com/spf13/cobra" - version = "0.0.2" - -[[constraint]] - name = "github.com/spf13/pflag" - version = "1.0.1" - -[[constraint]] - name = "github.com/spf13/viper" - source = "https://github.com/mattermost/viper" - branch = "env-settings" - -[[constraint]] - branch = "master" - name = "golang.org/x/crypto" - -[prune] - go-tests = true - unused-packages = true diff --git a/Makefile b/Makefile index 4a6b5085..bae18685 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ DIST_PATH=$(DIST_ROOT)/$(DIST_FOLDER_NAME) all: install vendor: - dep ensure + glide install install: vendor $(GO) install ./cmd/loadtest diff --git a/cmd/ltops/create.go b/cmd/ltops/create.go index 8a84521f..935b48b1 100644 --- a/cmd/ltops/create.go +++ b/cmd/ltops/create.go @@ -3,8 +3,10 @@ package main import ( "path/filepath" + "github.com/mattermost/mattermost-load-test/kubernetes" "github.com/mattermost/mattermost-load-test/ltops" "github.com/mattermost/mattermost-load-test/terraform" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -22,6 +24,7 @@ func createClusterCmd(cmd *cobra.Command, args []string) error { config.DBInstanceType, _ = cmd.Flags().GetString("db-type") config.DBInstanceCount, _ = cmd.Flags().GetInt("db-count") config.LoadtestInstanceCount, _ = cmd.Flags().GetInt("loadtest-count") + clusterType, _ := cmd.Flags().GetString("type") workingDir, err := defaultWorkingDirectory() if err != nil { @@ -29,25 +32,42 @@ func createClusterCmd(cmd *cobra.Command, args []string) error { } config.WorkingDirectory = filepath.Join(workingDir, config.Name) - _, err = terraform.CreateCluster(&config) - if err != nil { - return err + if clusterType == kubernetes.CLUSTER_TYPE { + _, err = kubernetes.CreateCluster(&config) + if err != nil { + return err + } + } else if clusterType == terraform.CLUSTER_TYPE { + if len(config.AppInstanceType) == 0 { + return errors.New("required flag \"app-type\" not set") + } + if len(config.DBInstanceType) == 0 { + return errors.New("required flag \"db-type\" not set") + } + + _, err = terraform.CreateCluster(&config) + if err != nil { + return err + } + } else { + return errors.New("unrecognized type: " + clusterType) } return nil } func init() { - createCluster.Flags().StringP("name", "", "c", "a unique name for the cluster (required)") + createCluster.Flags().StringP("name", "c", "", "a unique name for the cluster (required)") createCluster.MarkFlagRequired("name") - createCluster.Flags().String("app-type", "", "the app instance type (required)") - createCluster.MarkFlagRequired("app-type") + createCluster.Flags().StringP("type", "t", "", "the type of cluster, terraform or kubernetes (required)") + createCluster.MarkFlagRequired("type") + + createCluster.Flags().String("app-type", "", "the app instance type (required for terraform)") createCluster.Flags().Int("app-count", 1, "the number of app instances") - createCluster.Flags().String("db-type", "", "the db instance type (required)") - createCluster.MarkFlagRequired("db-type") + createCluster.Flags().String("db-type", "", "the db instance type (required for terraform)") createCluster.Flags().Int("db-count", 1, "the number of db instances") diff --git a/cmd/ltops/delete.go b/cmd/ltops/delete.go index 9402d585..0791d1e5 100644 --- a/cmd/ltops/delete.go +++ b/cmd/ltops/delete.go @@ -3,7 +3,6 @@ package main import ( "path/filepath" - "github.com/mattermost/mattermost-load-test/terraform" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -20,7 +19,7 @@ var deleteCluster = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, name)) + cluster, err := LoadCluster(filepath.Join(workingDir, name)) if err != nil { return errors.Wrap(err, "Couldn't load cluster") } diff --git a/cmd/ltops/deploy.go b/cmd/ltops/deploy.go index 1f285d65..931f0436 100644 --- a/cmd/ltops/deploy.go +++ b/cmd/ltops/deploy.go @@ -3,9 +3,10 @@ package main import ( "path/filepath" - "github.com/mattermost/mattermost-load-test/terraform" "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/mattermost/mattermost-load-test/terraform" ) var deploy = &cobra.Command{ @@ -23,11 +24,20 @@ var deploy = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { return errors.Wrap(err, "Couldn't load cluster") } + if cluster.Type() == terraform.CLUSTER_TYPE { + if len(mattermostFile) == 0 { + return errors.New("required flag \"mattermost\" not set") + } + if len(loadtestsFile) == 0 { + return errors.New("required flag \"loadtests\" not set") + } + } + err = cluster.DeployMattermost(mattermostFile, licenseFile) if err != nil { return errors.Wrap(err, "Couldn't deploy mattermost") @@ -46,14 +56,12 @@ func init() { deploy.Flags().StringP("cluster", "c", "", "cluster name (required)") deploy.MarkFlagRequired("cluster") - deploy.Flags().StringP("mattermost", "m", "", "mattermost distribution to deploy. Can be local file or URL. (required)") - deploy.MarkFlagRequired("mattermost") + deploy.Flags().StringP("mattermost", "m", "", "mattermost distribution to deploy. Can be local file or URL. (required for terraform)") deploy.Flags().StringP("license", "l", "", "the license file to use (required)") deploy.MarkFlagRequired("license") - deploy.Flags().StringP("loadtests", "t", "", "the loadtests package to use (required)") - deploy.MarkFlagRequired("loadtests") + deploy.Flags().StringP("loadtests", "t", "", "the loadtests package to use (required for terraform)") rootCmd.AddCommand(deploy) } diff --git a/cmd/ltops/load.go b/cmd/ltops/load.go new file mode 100644 index 00000000..06460404 --- /dev/null +++ b/cmd/ltops/load.go @@ -0,0 +1,94 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-load-test/kubernetes" + "github.com/mattermost/mattermost-load-test/ltops" + "github.com/mattermost/mattermost-load-test/terraform" +) + +type ClusterJson struct { + Config *ltops.ClusterConfig + Bytes []byte +} + +func (c *ClusterJson) UnmarshalJSON(b []byte) error { + var data map[string]interface{} + err := json.Unmarshal(b, &data) + if err != nil { + return err + } + + config, ok := data["Config"] + if !ok { + return errors.New("missing cluster config") + } + + configBytes, err := json.Marshal(config) + if err != nil { + return err + } + + err = json.Unmarshal(configBytes, &c.Config) + if err != nil { + return err + } + + c.Bytes = b + + return nil +} + +func (c *ClusterJson) GetCluster() (ltops.Cluster, error) { + clusterType := c.Config.Type + + if clusterType == terraform.CLUSTER_TYPE { + var cluster *terraform.Cluster + err := json.Unmarshal(c.Bytes, &cluster) + if err != nil { + return nil, err + } + + return cluster, nil + } else if clusterType == kubernetes.CLUSTER_TYPE { + var cluster *kubernetes.Cluster + err := json.Unmarshal(c.Bytes, &cluster) + if err != nil { + return nil, err + } + + if len(cluster.Release()) > 0 { + err = cluster.Connect() + if err != nil { + return nil, err + } + } + + return cluster, nil + } + + return nil, errors.New("unrecognized cluster type: " + clusterType) +} + +const infoFilename = "clusterinfo.json" + +// Loads a cluster from a specific directory +func LoadCluster(dir string) (ltops.Cluster, error) { + path := filepath.Join(dir, infoFilename) + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrap(err, "unable to read cluster info") + } + + var cluster *ClusterJson + if err := json.Unmarshal(b, &cluster); err != nil { + return nil, errors.Wrap(err, "unable to unmarshal cluster info") + } + + return cluster.GetCluster() +} diff --git a/cmd/ltops/loadtest.go b/cmd/ltops/loadtest.go index 0afdea4b..0ac0e20a 100644 --- a/cmd/ltops/loadtest.go +++ b/cmd/ltops/loadtest.go @@ -3,14 +3,13 @@ package main import ( "path/filepath" - "github.com/mattermost/mattermost-load-test/terraform" "github.com/pkg/errors" "github.com/spf13/cobra" ) var loadTest = &cobra.Command{ Use: "loadtest -- [args...]", - Short: "Runs a mattermost-load-test command againt the given cluster", + Short: "Runs a mattermost-load-test command against the given cluster", RunE: func(cmd *cobra.Command, args []string) error { clusterName, _ := cmd.Flags().GetString("cluster") //config, _ := cmd.Flags().GetString("config") @@ -20,7 +19,7 @@ var loadTest = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { return errors.Wrap(err, "Couldn't load cluster") } @@ -33,7 +32,8 @@ func init() { loadTest.Flags().StringP("cluster", "c", "", "cluster name (required)") loadTest.MarkFlagRequired("cluster") - loadTest.Flags().StringP("config", "f", "", "a config file to use instead of the default (the ConnectionConfiguration section is mostly ignored)") + // TODO: Implement + //loadTest.Flags().StringP("config", "f", "", "a config file to use instead of the default (the ConnectionConfiguration section is mostly ignored)") rootCmd.AddCommand(loadTest) } diff --git a/cmd/ltops/root.go b/cmd/ltops/root.go index 08645e9a..5201dce3 100644 --- a/cmd/ltops/root.go +++ b/cmd/ltops/root.go @@ -11,6 +11,7 @@ var rootCmd = &cobra.Command{ Use: os.Args[0], SilenceErrors: true, SilenceUsage: true, + Long: "Use ltops to easily spin up and load test a cluster of Mattermost servers with all the trimmings. Currently supports AWS and Kubernetes. For AWS, you must have your aws-cli configured and terraform installed. For Kubernetes, you must have a Kubernetes cluster configured in your kubeconfig and helm installed. See https://github.com/mattermost/mattermost-load-test/blob/master/README.md for more information.", PersistentPreRun: func(cmd *cobra.Command, args []string) { if verbose, _ := cmd.Flags().GetBool("verbose"); verbose { logrus.SetLevel(logrus.DebugLevel) diff --git a/cmd/ltops/ssh.go b/cmd/ltops/ssh.go index 0eb2e71f..c7b62b5d 100644 --- a/cmd/ltops/ssh.go +++ b/cmd/ltops/ssh.go @@ -4,8 +4,9 @@ import ( "path/filepath" "strconv" + "github.com/mattermost/mattermost-load-test/kubernetes" + "github.com/mattermost/mattermost-load-test/ltops" "github.com/mattermost/mattermost-load-test/sshtools" - "github.com/mattermost/mattermost-load-test/terraform" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -24,7 +25,7 @@ var sshAppCommand = &cobra.Command{ clusterName := args[0] instanceNumber, err := strconv.Atoi(args[1]) if err != nil { - return errors.Wrap(err, "Instance number must be a number") + return errors.Wrap(err, "instance number must be a number") } workingDir, err := defaultWorkingDirectory() @@ -32,24 +33,21 @@ var sshAppCommand = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { - return errors.Wrap(err, "Couldn't load cluster") + return errors.Wrap(err, "couldn't load cluster") } addrs, err := cluster.GetAppInstancesAddrs() if err != nil { - return errors.Wrap(err, "Unable to get app instances.") + return errors.Wrap(err, "unable to get app instances.") } if len(addrs) <= instanceNumber { - return errors.New("Invalid instance number.") + return errors.New("invalid instance number.") } - addr := addrs[instanceNumber] - logrus.Info("Connecting to " + addr) - - return sshtools.SSHInteractiveTerminal(cluster.SSHKey(), addr) + return ssh(cluster, addrs[instanceNumber]) }, } @@ -69,24 +67,21 @@ var sshProxyCommand = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { - return errors.Wrap(err, "Couldn't load cluster") + return errors.Wrap(err, "couldn't load cluster") } addrs, err := cluster.GetProxyInstancesAddrs() if err != nil { - return errors.Wrap(err, "Unable to get proxy instances.") + return errors.Wrap(err, "unable to get proxy instances.") } if len(addrs) <= instanceNumber { - return errors.New("Invalid instance number.") + return errors.New("invalid instance number.") } - addr := addrs[instanceNumber] - logrus.Info("Connecting to " + addr) - - return sshtools.SSHInteractiveTerminal(cluster.SSHKey(), addr) + return ssh(cluster, addrs[instanceNumber]) }, } @@ -98,7 +93,7 @@ var sshLoadtestCommand = &cobra.Command{ clusterName := args[0] instanceNumber, err := strconv.Atoi(args[1]) if err != nil { - return errors.Wrap(err, "Instance number must be a number") + return errors.Wrap(err, "instance number must be a number") } workingDir, err := defaultWorkingDirectory() @@ -106,24 +101,21 @@ var sshLoadtestCommand = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { - return errors.Wrap(err, "Couldn't load cluster") + return errors.Wrap(err, "couldn't load cluster") } addrs, err := cluster.GetLoadtestInstancesAddrs() if err != nil { - return errors.Wrap(err, "Unable to get loadtest instances.") + return errors.Wrap(err, "unable to get loadtest instances.") } if len(addrs) <= instanceNumber { - return errors.New("Invalid instance number.") + return errors.New("invalid instance number.") } - addr := addrs[instanceNumber] - logrus.Info("Connecting to " + addr) - - return sshtools.SSHInteractiveTerminal(cluster.SSHKey(), addr) + return ssh(cluster, addrs[instanceNumber]) }, } @@ -139,22 +131,36 @@ var sshMetricsCommand = &cobra.Command{ return err } - cluster, err := terraform.LoadCluster(filepath.Join(workingDir, clusterName)) + cluster, err := LoadCluster(filepath.Join(workingDir, clusterName)) if err != nil { - return errors.Wrap(err, "Couldn't load cluster") + return errors.Wrap(err, "couldn't load cluster") } - addr, err := cluster.GetMetricsAddr() - if err != nil { - return errors.Wrap(err, "Could not get metrics server address.") + var addr string + if cluster.Type() == kubernetes.CLUSTER_TYPE { + addr, err = cluster.(*kubernetes.Cluster).GetMetricsPodName() + } else { + addr, err = cluster.GetMetricsAddr() } - logrus.Info("Connecting to " + addr) + if err != nil { + return errors.Wrap(err, "could not get metrics server address.") + } - return sshtools.SSHInteractiveTerminal(cluster.SSHKey(), addr) + return ssh(cluster, addr) }, } +func ssh(cluster ltops.Cluster, addr string) error { + logrus.Info("Connecting to " + addr) + + if cluster.Type() == kubernetes.CLUSTER_TYPE { + return sshtools.SSHInteractiveKubesPod(addr) + } + + return sshtools.SSHInteractiveTerminal(cluster.SSHKey(), addr) +} + func init() { sshCommand.AddCommand(sshAppCommand, sshLoadtestCommand, sshProxyCommand, sshMetricsCommand) diff --git a/cmd/ltops/status.go b/cmd/ltops/status.go index 15e6c157..b408b5f4 100644 --- a/cmd/ltops/status.go +++ b/cmd/ltops/status.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "path/filepath" + "github.com/mattermost/mattermost-load-test/kubernetes" "github.com/mattermost/mattermost-load-test/ltops" - "github.com/mattermost/mattermost-load-test/terraform" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -32,7 +32,7 @@ func statusCmd(cmd *cobra.Command, args []string) error { for _, file := range files { path := filepath.Join(workingDir, file.Name()) - if cluster, err := terraform.LoadCluster(path); err != nil { + if cluster, err := LoadCluster(path); err != nil { logrus.Error(errors.Wrap(err, "Unable to load cluster "+file.Name())) } else { printStatusForCluster(cluster) @@ -45,6 +45,7 @@ func statusCmd(cmd *cobra.Command, args []string) error { const statusFormatString = ` -------------------------------------- Name: %v +Type: %v%v SiteURL: %v Metrics: %v DBConnectionString: %v @@ -70,14 +71,21 @@ func printStatusForCluster(cluster ltops.Cluster) { } metrics, _ := cluster.GetMetricsAddr() + release := "" + if cluster.Type() == kubernetes.CLUSTER_TYPE { + release = "\nRelease: " + cluster.(*kubernetes.Cluster).Release() + } + fmt.Printf(statusFormatString, cluster.Name(), + cluster.Type(), + release, cluster.SiteURL(), metrics, dbConnectionString, rrConnnectionString, len(app), - cluster.Configuration().DBInstanceCount, + cluster.DBInstanceCount(), len(proxy), len(loadtest), ) diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 00000000..1aed624b --- /dev/null +++ b/glide.yaml @@ -0,0 +1,32 @@ +package: github.com/mattermost/mattermost-load-test +import: +- package: github.com/VividCortex/ewma +- package: github.com/gizak/termui +- package: github.com/go-sql-driver/mysql +- package: github.com/icrowley/fake +- package: github.com/jmoiron/sqlx +- package: github.com/lib/pq +- package: github.com/mattermost/mattermost-server + subpackages: + - model + - utils +- package: github.com/montanaflynn/stats +- package: github.com/op/go-logging +- package: github.com/paulbellamy/ratecounter +- package: github.com/pkg/errors +- package: github.com/sirupsen/logrus +- package: github.com/spf13/cobra +- package: github.com/spf13/pflag + version: v1.0.1 +- package: github.com/spf13/viper +- package: golang.org/x/crypto + subpackages: + - ssh + - ssh/terminal +- package: k8s.io/apimachinery + subpackages: + - pkg/apis/meta/v1 +- package: k8s.io/client-go + subpackages: + - kubernetes + - tools/clientcmd diff --git a/kubernetes/cluster.go b/kubernetes/cluster.go new file mode 100644 index 00000000..d256a3b4 --- /dev/null +++ b/kubernetes/cluster.go @@ -0,0 +1,186 @@ +package kubernetes + +import ( + "fmt" + "os" + "os/exec" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/mattermost/mattermost-load-test/ltops" +) + +type Cluster struct { + Config *ltops.ClusterConfig + ReleaseName string + Kubernetes *kubernetes.Clientset `json:"-"` +} + +func (c *Cluster) Name() string { + return c.Config.Name +} + +func (c *Cluster) Type() string { + return c.Config.Type +} + +func (c *Cluster) Release() string { + return c.ReleaseName +} + +func (c *Cluster) Configuration() *ltops.ClusterConfig { + return c.Config +} + +func (c *Cluster) SSHKey() []byte { + return []byte{} +} + +func (c *Cluster) SiteURL() string { + if len(c.Release()) == 0 { + return "" + } + + svc, err := c.Kubernetes.CoreV1().Services("default").Get(c.Release()+"-nginx-ingress-controller", metav1.GetOptions{}) + if err != nil { + return "" + } + + ingressInstances := svc.Status.LoadBalancer.Ingress + if len(ingressInstances) == 0 || ingressInstances[0].IP == "" { + return "pending" + } + + return ingressInstances[0].IP +} + +func (c *Cluster) GetAppInstancesAddrs() ([]string, error) { + if len(c.Release()) == 0 { + return []string{}, nil + } + + pods, err := c.Kubernetes.CoreV1().Pods("").List(metav1.ListOptions{LabelSelector: "app=" + c.Release() + "-mattermost-app"}) + if err != nil { + return nil, err + } + + podNames := make([]string, len(pods.Items)) + for i, p := range pods.Items { + podNames[i] = p.Name + } + + return podNames, nil +} + +func (c *Cluster) GetLoadtestInstancesAddrs() ([]string, error) { + if len(c.Release()) == 0 { + return []string{}, nil + } + + pods, err := c.Kubernetes.CoreV1().Pods("").List(metav1.ListOptions{LabelSelector: "app=mattermost-loadtest,release=" + c.Release()}) + if err != nil { + return nil, err + } + + podNames := make([]string, len(pods.Items)) + for i, p := range pods.Items { + podNames[i] = p.Name + } + + return podNames, nil +} + +func (c *Cluster) GetProxyInstancesAddrs() ([]string, error) { + if len(c.Release()) == 0 { + return []string{}, nil + } + + pods, err := c.Kubernetes.CoreV1().Pods("").List(metav1.ListOptions{LabelSelector: "release=" + c.Release() + ",app=nginx-ingress,component=controller"}) + if err != nil { + return nil, err + } + + podNames := make([]string, len(pods.Items)) + for i, p := range pods.Items { + podNames[i] = p.Name + } + + return podNames, nil +} + +func (c *Cluster) GetMetricsAddr() (string, error) { + if len(c.Release()) == 0 { + return "", nil + } + + svc, err := c.Kubernetes.CoreV1().Services("default").Get(c.Release()+"-grafana", metav1.GetOptions{}) + if err != nil { + return "", err + } + + ingressInstances := svc.Status.LoadBalancer.Ingress + if len(ingressInstances) == 0 || ingressInstances[0].IP == "" { + return "pending", nil + } + + return ingressInstances[0].IP, nil +} + +func (c *Cluster) GetMetricsPodName() (string, error) { + if len(c.Release()) == 0 { + return "", nil + } + + pods, err := c.Kubernetes.CoreV1().Pods("").List(metav1.ListOptions{LabelSelector: "app=" + c.Release() + "-grafana"}) + if err != nil { + return "", err + } + + if len(pods.Items) == 0 { + return "", errors.New("no grafana pods running") + } + + return pods.Items[0].Name, nil +} + +func (c *Cluster) DBConnectionString() string { + if len(c.Release()) == 0 { + return "" + } + return fmt.Sprintf("mmuser:passwd@tcp(%v-mysqlha-0.%v-mysqlha:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=20s&writeTimeout=20s&timeout=20s", c.Release(), c.Release()) +} + +func (c *Cluster) DBReaderConnectionStrings() []string { + if len(c.Release()) == 0 { + return []string{} + } + return []string{fmt.Sprintf("mmuser:passwd@tcp(%v-mysqlha-readonly:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=20s&writeTimeout=20s&timeout=20s", c.Release())} +} + +func (c *Cluster) DBInstanceCount() int { + if len(c.Release()) == 0 { + return 0 + } + + pods, err := c.Kubernetes.CoreV1().Pods("").List(metav1.ListOptions{LabelSelector: "app=" + c.Release() + "-mysqlha"}) + if err != nil { + return 0 + } + + return len(pods.Items) +} + +func (c *Cluster) Destroy() error { + log.Info("Destroying cluster...") + + cmd := exec.Command("helm", "delete", c.Release()) + out, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrap(err, "unable to delete release, error from helm: "+string(out)) + } + + return os.RemoveAll(c.Configuration().WorkingDirectory) +} diff --git a/kubernetes/cluster_connect.go b/kubernetes/cluster_connect.go new file mode 100644 index 00000000..54f88715 --- /dev/null +++ b/kubernetes/cluster_connect.go @@ -0,0 +1,28 @@ +package kubernetes + +import ( + "path/filepath" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +func (c *Cluster) Connect() error { + if c.Kubernetes != nil { + return nil + } + + kubeconfig := filepath.Join(homeDir(), ".kube", "config") + + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return err + } + + c.Kubernetes, err = kubernetes.NewForConfig(config) + if err != nil { + return err + } + + return nil +} diff --git a/kubernetes/cluster_create.go b/kubernetes/cluster_create.go new file mode 100644 index 00000000..aa3847d0 --- /dev/null +++ b/kubernetes/cluster_create.go @@ -0,0 +1,97 @@ +package kubernetes + +import ( + "encoding/json" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/mattermost/mattermost-load-test/ltops" +) + +const ( + CLUSTER_TYPE = "kubernetes" +) + +func CreateCluster(cfg *ltops.ClusterConfig) (ltops.Cluster, error) { + log.Info("checking kubernetes cluster...") + log.Info("note you must already have an existing kubernetes cluster configured in your kubeconfig") + + cmd := exec.Command("kubectl", "get", "nodes") + out, err := cmd.CombinedOutput() + if err != nil { + return nil, errors.New("error running 'kubectl get nodes', make sure your kubeconfig is correct. error from kubectl: " + string(out)) + } + + log.Info("kubectl working and cluster exists") + + cmd = exec.Command("helm", "ls") + err = cmd.Run() + if err != nil { + return nil, errors.Wrap(err, "unable to run helm") + } + + cmd = exec.Command("helm", "repo", "add", "incubator", "https://kubernetes-charts-incubator.storage.googleapis.com/") + err = cmd.Run() + if err != nil { + return nil, errors.Wrap(err, "unable to add incubator helm repo") + } + + cmd = exec.Command("helm", "repo", "add", "mattermost", "https://releases.mattermost.com/helm") + err = cmd.Run() + if err != nil { + return nil, errors.Wrap(err, "unable to add mattermost helm repo") + } + + log.Info("helm working and repos added") + + cfg.Type = CLUSTER_TYPE + + cluster := &Cluster{ + Config: cfg, + } + + err = saveCluster(cluster, cfg.WorkingDirectory) + if err != nil { + return nil, err + } + + log.Info("...done") + + return cluster, nil +} + +const infoFilename = "clusterinfo.json" + +func saveCluster(cluster *Cluster, dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + } else if err != nil { + return err + } + + b, err := json.Marshal(cluster) + if err != nil { + return errors.Wrap(err, "unable to marshal cluster") + } + + path := filepath.Join(dir, infoFilename) + if err := ioutil.WriteFile(path, b, 0600); err != nil { + return errors.Wrap(err, "unable to write cluster") + } + + return nil +} + +func homeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.Getenv("USERPROFILE") // windows +} diff --git a/kubernetes/cluster_deploy.go b/kubernetes/cluster_deploy.go new file mode 100644 index 00000000..bbc2b52e --- /dev/null +++ b/kubernetes/cluster_deploy.go @@ -0,0 +1,148 @@ +package kubernetes + +import ( + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" + + "github.com/mattermost/mattermost-load-test/ltops" +) + +type ChartConfig struct { + Global *GlobalConfig `yaml:"global"` + MySQLHA *MySQLHAConfig `yaml:"mysqlha"` + App *AppConfig `yaml:"mattermost-app"` + Loadtest *LoadtestConfig `yaml:"mattermost-loadtest"` +} + +type GlobalConfig struct { + SiteURL string `yaml:"siteUrl"` + MattermostLicense string `yaml:"mattermostLicense"` + Features *FeaturesConfig `yaml:"features"` +} + +type FeaturesConfig struct { + LoadTest *LoadTestFeature `yaml:"loadTest"` + Grafana *GrafanFeature `yaml:"grafana"` +} + +type LoadTestFeature struct { + Enabled bool `yaml:"enabled"` +} + +type GrafanFeature struct { + Enabled bool `yaml:"enabled"` +} + +type MySQLHAConfig struct { + Enabled bool `yaml:"enabled"` + Options *MySQLHAOptions `yaml:"mysqlha"` +} + +type MySQLHAOptions struct { + ReplicaCount int `yaml:"replicaCount"` +} + +type AppConfig struct { + ReplicaCount int `yaml:"replicaCount"` + Image *ImageSetting `yaml:"image"` +} + +type LoadtestConfig struct { + ReplicaCount int `yaml:"replicaCount"` +} + +type ImageSetting struct { + Tag string `yaml:"tag"` +} + +func (c *Cluster) DeployMattermost(mattermostFile string, licenceFileLocation string) error { + log.Info("installing mattermost helm chart...") + + if len(c.ReleaseName) > 0 { + log.Info("already installed as release '" + c.ReleaseName + "'") + return nil + } + + license, err := ltops.GetFileOrURL(licenceFileLocation) + if err != nil { + return err + } + + config := &ChartConfig{ + Global: &GlobalConfig{ + SiteURL: "http://localhost:8065", + MattermostLicense: string(license), + Features: &FeaturesConfig{ + &LoadTestFeature{Enabled: true}, + &GrafanFeature{Enabled: true}, + }, + }, + MySQLHA: &MySQLHAConfig{ + Enabled: true, + Options: &MySQLHAOptions{ + ReplicaCount: c.Configuration().DBInstanceCount, + }, + }, + App: &AppConfig{ + ReplicaCount: c.Configuration().AppInstanceCount, + Image: &ImageSetting{ + Tag: "4.9.2", + }, + }, + Loadtest: &LoadtestConfig{ + ReplicaCount: c.Configuration().LoadtestInstanceCount, + }, + } + + err = saveChartConfig(config, c.Config.WorkingDirectory) + if err != nil { + return err + } + + cmd := exec.Command("helm", "install", "-f", filepath.Join(c.Config.WorkingDirectory, chartFilename), "mattermost/mattermost-helm") + out, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrap(err, "unable to install mattermost chart, error from helm: "+string(out)) + } + + fields := strings.Fields(strings.Split(string(out), "\n")[0]) + c.ReleaseName = fields[1] + + log.Info("created release '" + c.ReleaseName + "'") + + err = saveCluster(c, c.Config.WorkingDirectory) + if err != nil { + return err + } + + log.Info("...done") + + return nil +} + +const chartFilename = "chartconfig.yaml" + +func saveChartConfig(config *ChartConfig, dir string) error { + b, err := yaml.Marshal(config) + if err != nil { + return errors.Wrap(err, "unable to marshal chart config") + } + + path := filepath.Join(dir, chartFilename) + if err := ioutil.WriteFile(path, b, 0600); err != nil { + return errors.Wrap(err, "unable to write chart config") + } + + return nil +} + +// Not applicable to kubernetes +func (c *Cluster) DeployLoadtests(loadtestsDistLocation string) error { + return nil +} diff --git a/kubernetes/cluster_loadtest.go b/kubernetes/cluster_loadtest.go new file mode 100644 index 00000000..fc328e18 --- /dev/null +++ b/kubernetes/cluster_loadtest.go @@ -0,0 +1,109 @@ +package kubernetes + +import ( + "io" + "os" + "os/exec" + "path/filepath" + "sync" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func (c *Cluster) loadtestPod(pod string, resultsOutput io.Writer) error { + commandOutputFile := filepath.Join(c.Configuration().WorkingDirectory, "results", "loadtest-out-"+pod+".txt") + if err := os.MkdirAll(filepath.Dir(commandOutputFile), 0700); err != nil { + return errors.Wrap(err, "unable to create results directory.") + } + outfile, err := os.Create(commandOutputFile) + if err != nil { + return errors.Wrap(err, "unable to create loadtest results file.") + } + + cmd := exec.Command("kubectl", "exec", pod, "./bin/loadtest", "all") + + if resultsOutput != nil { + cmd.Stdout = io.MultiWriter(outfile, resultsOutput) + } else { + cmd.Stdout = outfile + } + cmd.Stderr = outfile + + log.Info("Running loadtest on " + pod) + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (c *Cluster) bulkLoad(loadtestPod string, appPod string) error { + log.Info("Bulk importing data, this may take some time") + cmd := exec.Command("kubectl", "exec", loadtestPod, "./bin/loadtest", "genbulkload") + if err := cmd.Run(); err != nil { + return err + } + + // Unfortunately kubectl cp doesn't work directly between pods + cmd = exec.Command("kubectl", "cp", loadtestPod+":/mattermost-load-test/loadtestbulkload.json", c.Configuration().WorkingDirectory) + if err := cmd.Run(); err != nil { + return err + } + + cmd = exec.Command("kubectl", "cp", filepath.Join(c.Configuration().WorkingDirectory, "loadtestbulkload.json"), appPod+":/mattermost/") + if err := cmd.Run(); err != nil { + return err + } + + cmd = exec.Command("kubectl", "exec", appPod, "--", "./bin/platform", "import", "bulk", "--workers", "64", "--apply", "./loadtestbulkload.json") + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrap(err, "bulk import failed: "+string(out)) + } + + return nil +} + +func (c *Cluster) Loadtest(resultsOutput io.Writer) error { + loadtestPods, err := c.GetLoadtestInstancesAddrs() + if err != nil || len(loadtestPods) <= 0 { + return errors.Wrap(err, "unable to get loadtest pods") + } + + appPods, err := c.GetAppInstancesAddrs() + if err != nil || len(appPods) <= 0 { + return errors.Wrap(err, "unable to get app pods") + } + + err = c.bulkLoad(loadtestPods[0], appPods[0]) + if err != nil { + return err + } + + var wg sync.WaitGroup + wg.Add(len(loadtestPods)) + + for i, pod := range loadtestPods { + pod := pod + go func() { + var err error + if i == 0 { + err = c.loadtestPod(pod, resultsOutput) + } else { + err = c.loadtestPod(pod, nil) + } + if err != nil { + log.Error(err) + } + wg.Done() + }() + // Give some time between instances just to avoid any races + time.Sleep(time.Second * 10) + } + + log.Info("Wating for loadtests to complete. See: " + filepath.Join(c.Configuration().WorkingDirectory, "results") + " for results.") + wg.Wait() + + return nil +} diff --git a/ltops/cluster.go b/ltops/cluster.go index 57e6269e..87cdbf64 100644 --- a/ltops/cluster.go +++ b/ltops/cluster.go @@ -4,6 +4,7 @@ import "io" type ClusterConfig struct { Name string + Type string AppInstanceType string AppInstanceCount int DBInstanceType string @@ -17,6 +18,9 @@ type Cluster interface { // Returns the name of the cluster Name() string + // Returns the type of the cluster + Type() string + // Returns the current configuration of the cluster Configuration() *ClusterConfig @@ -44,6 +48,9 @@ type Cluster interface { // Returns a list of all the read-replica database connection strings DBReaderConnectionStrings() []string + // Returns a count of DB instances + DBInstanceCount() int + // Deploys a mattermost package to the cluster. mattermostFile can be disk file or URL. DeployMattermost(mattermostFile string, licenceFile string) error diff --git a/sshtools/kubetools.go b/sshtools/kubetools.go new file mode 100644 index 00000000..d2f4b6c7 --- /dev/null +++ b/sshtools/kubetools.go @@ -0,0 +1,36 @@ +package sshtools + +import ( + "os" + "os/exec" +) + +// SSHInteractiveKubesPod will create an interactive shell for the given pod. +func SSHInteractiveKubesPod(podName string) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + + pa := os.ProcAttr{ + Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, + Dir: cwd, + } + + kubectl, err := exec.LookPath("kubectl") + if err != nil { + return err + } + + proc, err := os.StartProcess(kubectl, []string{"kubectl", "exec", "-it", podName, "--", "/bin/bash"}, &pa) + if err != nil { + return err + } + + _, err = proc.Wait() + if err != nil { + return err + } + + return nil +} diff --git a/terraform/cluster.go b/terraform/cluster.go index d4236959..141024da 100644 --- a/terraform/cluster.go +++ b/terraform/cluster.go @@ -19,6 +19,10 @@ func (c *Cluster) Name() string { return c.Config.Name } +func (c *Cluster) Type() string { + return c.Config.Type +} + func (c *Cluster) Configuration() *ltops.ClusterConfig { return c.Config } @@ -94,6 +98,10 @@ func (c *Cluster) DBReaderConnectionStrings() []string { return []string{"mmuser:" + c.DBPassword + "@tcp(" + databaseEndpoint + ":3306)/mattermost?charset=utf8mb4,utf8&readTimeout=20s&writeTimeout=20s&timeout=20s"} } +func (c *Cluster) DBInstanceCount() int { + return c.Config.DBInstanceCount +} + func (c *Cluster) Destroy() error { logrus.Info("Destroying cluster...") if err := c.Env.destroy(); err != nil { diff --git a/terraform/cluster_create.go b/terraform/cluster_create.go index e88c94ba..4dfd7259 100644 --- a/terraform/cluster_create.go +++ b/terraform/cluster_create.go @@ -10,6 +10,10 @@ import ( "github.com/sirupsen/logrus" ) +const ( + CLUSTER_TYPE = "terraform" +) + func CreateCluster(cfg *ltops.ClusterConfig) (ltops.Cluster, error) { dbPassword, err := generatePassword() if err != nil { @@ -33,6 +37,8 @@ func CreateCluster(cfg *ltops.ClusterConfig) (ltops.Cluster, error) { return nil, errors.Wrap(err, "Unable to run apply for create cluster") } + cfg.Type = CLUSTER_TYPE + cluster := &Cluster{ Config: cfg, SSHPrivateKeyPEM: sshPrivateKeyPEM, diff --git a/terraform/cluster_deploy.go b/terraform/cluster_deploy.go index c4b617ca..db23e838 100644 --- a/terraform/cluster_deploy.go +++ b/terraform/cluster_deploy.go @@ -5,9 +5,7 @@ import ( "encoding/json" "fmt" "io" - "net/http" "net/url" - "os" "strconv" "strings" "sync" @@ -21,31 +19,6 @@ import ( "golang.org/x/crypto/ssh" ) -func getFileOrURL(fileOrUrl string) ([]byte, error) { - buffer := bytes.NewBuffer(nil) - if strings.HasPrefix(fileOrUrl, "http") { - response, err := http.Get(fileOrUrl) - if err != nil { - return nil, errors.Wrap(err, "Can't get file at URL: "+fileOrUrl) - } - defer response.Body.Close() - - io.Copy(buffer, response.Body) - - return buffer.Bytes(), nil - } else { - f, err := os.Open(fileOrUrl) - if err != nil { - return nil, errors.Wrap(err, "unable to open file "+fileOrUrl) - } - defer f.Close() - - io.Copy(buffer, f) - - return buffer.Bytes(), nil - } -} - func (c *Cluster) DeployMattermost(mattermostDistLocation string, licenceFileLocation string) error { appInstanceAddrs, err := c.GetAppInstancesAddrs() if err != nil || len(appInstanceAddrs) <= 0 { @@ -57,12 +30,12 @@ func (c *Cluster) DeployMattermost(mattermostDistLocation string, licenceFileLoc return errors.Wrap(err, "Unable to get app instance addresses") } - mattermostDist, err := getFileOrURL(mattermostDistLocation) + mattermostDist, err := ltops.GetFileOrURL(mattermostDistLocation) if err != nil { return err } - licenseFile, err := getFileOrURL(licenceFileLocation) + licenseFile, err := ltops.GetFileOrURL(licenceFileLocation) if err != nil { return err } @@ -102,7 +75,7 @@ func (c *Cluster) DeployLoadtests(loadtestsDistLocation string) error { return errors.Wrap(err, "Unable to get loadtest instance addresses") } - loadtestsDist, err := getFileOrURL(loadtestsDistLocation) + loadtestsDist, err := ltops.GetFileOrURL(loadtestsDistLocation) if err != nil { return err } diff --git a/terraform/cluster_load.go b/terraform/cluster_load.go deleted file mode 100644 index fba4c99f..00000000 --- a/terraform/cluster_load.go +++ /dev/null @@ -1,26 +0,0 @@ -package terraform - -import ( - "encoding/json" - "io/ioutil" - "path/filepath" - - "github.com/mattermost/mattermost-load-test/ltops" - "github.com/pkg/errors" -) - -// Loads a cluster from a specific directory -func LoadCluster(dir string) (ltops.Cluster, error) { - path := filepath.Join(dir, infoFilename) - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, errors.Wrap(err, "unable to read cluster info") - } - - var cluster *Cluster - if err := json.Unmarshal(b, &cluster); err != nil { - return nil, errors.Wrap(err, "unable to unmarshal cluster info") - } - - return cluster, nil -} From 59cf53dde9e3ea104b06364db1435aae7e64141d Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 1 Jun 2018 13:18:53 -0400 Subject: [PATCH 2/4] Update README --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b67338ca..1ec424ac 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A set of tools for testing/proving Mattermost servers under load. ## Loadtesting with the ltops (load test ops) tool -The ltops tool allows you to easily spin up and loadtest a cluster of Mattermost servers with all the trimmings. Currently it supports AWS with support for other cloud platforms and Kubernetes planned in the future. It is powered by [Terraform](https://www.terraform.io/) +The ltops tool allows you to easily spin up and load test a cluster of Mattermost servers with all the trimmings. Currently it supports AWS via [Terraform](https://www.terraform.io/) and Kubernetes. ### Installation @@ -22,11 +22,50 @@ make install Type `ltops` to check tool is installed properly. For help with any command, use `ltops help ` +### Kubernetes + +If you want to run the load test on a Kubernetes cluster, you need to install kubectl and helm. + +Install kubectl: https://kubernetes.io/docs/tasks/tools/install-kubectl/ + +Type `kubectl` to check the tool is installed properly. + +Install helm: https://docs.helm.sh/using_helm/#installing-helm + +Type `helm` to check the tool is installed properly. + +#### Configuration + +You need to have an existing Kubernetes cluster configured. To check if you have one set up and configured run: `kubectl cluster-info`. + +To set up a Kubernetes cluster, use one of the following guides: +* AWS - https://github.com/kubernetes/kops/blob/master/docs/aws.md +* Azure - https://github.com/Azure/acs-engine/blob/master/docs/kubernetes/deploy.md +* Google Cloud Engine - https://kubernetes.io/docs/getting-started-guides/gce/ + +See https://kubernetes.io/docs/setup/pick-right-solution/ for more options. + +#### Set up a load test with Kubernetes + +1. Set up a cluster: +``` +ltops create --name myloadtestcluster --type kubernetes --app-count 1 --db-count 1 --loadtest-count 1 +``` + +2. Deploy and configure the helm chart: +``` +ltops deploy -c myloadtestcluster --license ~/mylicence.mattermost-license +``` + +### Terraform + +If you want to run load test clusters on AWS, you need to install terraform. + Install Terraform: https://www.terraform.io/intro/getting-started/install.html Type `terraform` to check tool is installed properly. -### Configure for AWS +#### Configure for AWS Fill in your `~/.aws/credentials` file. The ltops tool will use the profile named `ltops`. You can add a profile using the aws CLI: ``` @@ -35,11 +74,11 @@ aws configure --profile ltops More info on setting up the credentials file here: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html -### Running a loadtest +#### Set up a load test with Terraform 1. Create a cluster: ``` -ltops create --name myloadtestcluster --app-count 1 --db-count 1 --loadtest-count 1 --app-type m4.large --db-type db.r4.large +ltops create --name myloadtestcluster --type terraform --app-count 1 --db-count 1 --loadtest-count 1 --app-type m4.large --db-type db.r4.large ``` 2. Deploy Mattermost, configure proxy, loadtest. Note that the options support local files and URLs. @@ -47,12 +86,16 @@ ltops create --name myloadtestcluster --app-count 1 --db-count 1 --loadtest-coun ltops deploy -c myloadtestcluster -m https://releases.mattermost.com/4.9.2/mattermost-4.9.2-linux-amd64.tar.gz -l ~/mylicence.mattermost-license -t https://releases.mattermost.com/mattermost-load-test/mattermost-load-test.tar.gz ``` -3. Run loadtests +### Run a load test + +Now that you have a cluster set up in either AWS or Kubernetes, do the following to run a load test: + +1. Run load tests: ``` ltops loadtest -c myloadtestcluster ``` -4. Logs, including loadtest results will show up in ~/.mattermost-load-test-ops/myloadtestcluster/results +2. Logs, including loadtest results will show up in ~/.mattermost-load-test-ops/myloadtestcluster/results To generate a textual summary: ``` @@ -74,13 +117,12 @@ To generate a markdown summary comparing the results with a previous results fil ltparse results --file ~/.mattermost-load-test-ops/myloadtestcluster/results --display markdown --baseline /path/to/other/results ``` -5. Delete cluster when done -``` -ltops delete myloadtestcluster -``` - ### SSH into machines +For AWS, this will actually SSH into the EC2 instances. + +For Kubernetes, this will open an interactive shell to pods in the cluster. + SSH into app server 0: ``` ltops ssh app myloadtestcluster 0 @@ -102,6 +144,11 @@ ltops ssh loadtest myloadtestcluster 0 ltops status ``` +### Destroy a cluster + +``` +ltops delete myloadtestcluster +``` ## Using the loadtest agent directly From e58fe2aa95d666af78cc1b95ec66be3018ce1610 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 1 Jun 2018 13:41:10 -0400 Subject: [PATCH 3/4] Commit glide.lock --- .gitignore | 3 - glide.lock | 346 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 glide.lock diff --git a/.gitignore b/.gitignore index 497dc643..5d76173f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,9 +40,6 @@ bin/* !bin/echo_example.sh !bin/run_example.sh -# We just want to always use the latest -glide.lock - # Ignore dist directory dist diff --git a/glide.lock b/glide.lock new file mode 100644 index 00000000..d01decb6 --- /dev/null +++ b/glide.lock @@ -0,0 +1,346 @@ +hash: fde5c0b51a20d5f2a51088b413c5a8fc5ac5c4c68e1be1569f1ebc98f50fae79 +updated: 2018-05-28T14:57:09.967872826-04:00 +imports: +- name: github.com/corpix/uarand + version: 2b8494104d86337cdd41d0a49cbed8e4583c0ab4 +- name: github.com/dimchansky/utfbom + version: 6c6132ff69f0f6c088739067407b5d32c52e1d0f +- name: github.com/fsnotify/fsnotify + version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 +- name: github.com/ghodss/yaml + version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee +- name: github.com/gizak/termui + version: e9a3a6b3d6472ce875254d734f36335c15ebd4e6 +- name: github.com/go-ini/ini + version: 06f5f3d67269ccec1fe5fe4134ba6e982984f7f5 +- name: github.com/go-sql-driver/mysql + version: 64db0f7ebe171b596aa9b26f39a79f7413a3b617 +- name: github.com/gogo/protobuf + version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 + subpackages: + - proto + - sortkeys +- name: github.com/golang/glog + version: 44145f04b68cf362d9c4df2182967c2275eaefed +- name: github.com/golang/protobuf + version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 + subpackages: + - proto + - ptypes + - ptypes/any + - ptypes/duration + - ptypes/timestamp +- name: github.com/google/btree + version: 7d79101e329e5a3adf994758c578dab82b90c017 +- name: github.com/google/gofuzz + version: 44d81051d367757e1c7c6a5a86423ece9afcf63c +- name: github.com/googleapis/gnostic + version: 0c5108395e2debce0d731cf0287ddf7242066aba + subpackages: + - OpenAPIv2 + - compiler + - extensions +- name: github.com/gorilla/websocket + version: 21ab95fa12b9bdd8fecf5fa3586aad941cc98785 +- name: github.com/gregjones/httpcache + version: 787624de3eb7bd915c329cba748687a3b22666a6 + subpackages: + - diskcache +- name: github.com/hashicorp/hcl + version: ef8a98b0bbce4a65b5aa4c368430a80ddc533168 + subpackages: + - hcl/printer + - hcl/ast + - hcl/parser + - hcl/token + - json/parser + - hcl/scanner + - hcl/strconv + - json/scanner + - json/token +- name: github.com/icrowley/fake + version: 4178557ae428460c3780a381c824a1f3aceb6325 +- name: github.com/imdario/mergo + version: 6633656539c1639d9d78127b7d47c622b5d7b6dc +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/jmoiron/sqlx + version: 2aeb6a910c2b94f2d5eb53d9895d80e27264ec41 + subpackages: + - reflectx +- name: github.com/json-iterator/go + version: 2ddf6d758266fcb080a4f9e054b9f292c85e6798 +- name: github.com/lib/pq + version: 90697d60dd844d5ef6ff15135d0203f65d2f53b8 + subpackages: + - oid +- name: github.com/magiconair/properties + version: c2353362d570a7bfa228149c62842019201cfb71 +- name: github.com/maruel/panicparse + version: 4417700b5a8d33fbb4416bb04359d20329f60008 + subpackages: + - stack +- name: github.com/mattermost/html2text + version: d47a5532a7bc36ad7b2b8ec3eebe24e975154f94 +- name: github.com/mattermost/mattermost-server + version: 792dae23ca2b23650b34f15b1aa7ad63907a82c4 + subpackages: + - model + - utils + - utils/jsonutils + - utils/markdown + - einterfaces + - mlog +- name: github.com/mattn/go-runewidth + version: ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb +- name: github.com/minio/minio-go + version: 6f8ee6677a042d53d9251ecf7b1c7431c23d2416 + subpackages: + - pkg/credentials + - pkg/encrypt + - pkg/s3signer + - pkg/s3utils + - pkg/set +- name: github.com/mitchellh/go-homedir + version: 3864e76763d94a6df2f9960b16a20a33da9f9a66 +- name: github.com/mitchellh/go-wordwrap + version: ad45545899c7b13c020ea92b2072220eefad42b8 +- name: github.com/mitchellh/mapstructure + version: bb74f1db0675b241733089d5a1faa5dd8b0ef57b +- name: github.com/modern-go/concurrent + version: bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94 +- name: github.com/modern-go/reflect2 + version: 05fbef0ca5da472bbf96c9322b84a53edc03c9fd +- name: github.com/montanaflynn/stats + version: 1bf9dbcd8cbe1fdb75add3785b1d4a9a646269ab +- name: github.com/nicksnyder/go-i18n + version: e8d278b298d8267920e2ca98438d71b971e8e367 + subpackages: + - i18n + - i18n/bundle + - i18n/language + - i18n/translation +- name: github.com/nsf/termbox-go + version: 21a4d435a86280a2927985fd6296de56cbce453e +- name: github.com/op/go-logging + version: 970db520ece77730c7e4724c61121037378659d9 +- name: github.com/paulbellamy/ratecounter + version: a803f0e4f07116687bb30965e8f7d0c32981b63c +- name: github.com/pborman/uuid + version: c65b2f87fee37d1c7854c9164a450713c28d50cd +- name: github.com/pelletier/go-toml + version: 66540cf1fcd2c3aee6f6787dfa32a6ae9a870f12 +- name: github.com/peterbourgon/diskv + version: 5f041e8faa004a95c88a202771f4cc3e991971e6 +- name: github.com/pkg/errors + version: 816c9085562cd7ee03e7f8188a1cfd942858cded +- name: github.com/sirupsen/logrus + version: ea8897e79973357ba785ac2533559a6297e83c44 +- name: github.com/spf13/afero + version: 63644898a8da0bc22138abf860edaf5277b6102e + subpackages: + - mem +- name: github.com/spf13/cast + version: 8965335b8c7107321228e3e3702cab9832751bac +- name: github.com/spf13/cobra + version: ef82de70bb3f60c65fb8eebacbb2d122ef517385 +- name: github.com/spf13/jwalterweatherman + version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394 +- name: github.com/spf13/pflag + version: 583c0c0531f06d5278b7d917446061adc344b5cd +- name: github.com/spf13/viper + version: 15738813a09db5c8e5b60a19d67d3f9bd38da3a4 +- name: github.com/VividCortex/ewma + version: 43880d236f695d39c62cf7aa4ebd4508c258e6c0 +- name: go.uber.org/atomic + version: 1ea20fb1cbb1cc08cbd0d913a96dead89aa18289 +- name: go.uber.org/multierr + version: 3c4937480c32f4c13a875a1829af76c98ca3d40a +- name: go.uber.org/zap + version: 2dc8d10634f473f4dc678e48391a9dc8cf6ae6f9 + subpackages: + - zapcore + - internal/bufferpool + - buffer + - internal/color + - internal/exit +- name: golang.org/x/crypto + version: 49796115aa4b964c318aad4f3084fdb41e9aa067 + subpackages: + - ssh + - ssh/terminal + - bcrypt + - curve25519 + - ed25519 + - internal/chacha20 + - poly1305 + - blowfish + - argon2 + - ed25519/internal/edwards25519 + - blake2b +- name: golang.org/x/net + version: 1c05540f6879653db88113bc4a2b70aec4bd491f + subpackages: + - html + - html/atom + - http2 + - http2/hpack + - idna + - lex/httplex + - websocket + - http/httpguts + - context +- name: golang.org/x/sys + version: 95c6576299259db960f6c5b9b69ea52422860fce + subpackages: + - unix + - windows +- name: golang.org/x/text + version: b19bf474d317b857955b12035d2c5acb57ce8b01 + subpackages: + - secure/bidirule + - transform + - unicode/bidi + - unicode/norm +- name: golang.org/x/time + version: f51c12702a4d776e4c1fa9b0fabab841babae631 + subpackages: + - rate +- name: google.golang.org/appengine + version: b1f26356af11148e710935ed1ac8a7f5702c7612 + subpackages: + - cloudsql +- name: gopkg.in/alexcesaro/quotedprintable.v3 + version: 2caba252f4dc53eaf6b553000885530023f54623 +- name: gopkg.in/gomail.v2 + version: 81ebce5c23dfd25c6c67194b37d3dd3f338c98b1 +- name: gopkg.in/inf.v0 + version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 +- name: gopkg.in/natefinch/lumberjack.v2 + version: a96e63847dc3c67d17befa69c303767e2f84e54f +- name: gopkg.in/yaml.v2 + version: 670d4cfef0544295bc27a114dbac37980d83185a +- name: k8s.io/api + version: 184e700b32b7f1b532b9fce8dd8c1f412d297c4b + subpackages: + - admissionregistration/v1alpha1 + - admissionregistration/v1beta1 + - apps/v1 + - apps/v1beta1 + - apps/v1beta2 + - authentication/v1 + - authentication/v1beta1 + - authorization/v1 + - authorization/v1beta1 + - autoscaling/v1 + - autoscaling/v2beta1 + - batch/v1 + - batch/v1beta1 + - batch/v2alpha1 + - certificates/v1beta1 + - core/v1 + - events/v1beta1 + - extensions/v1beta1 + - imagepolicy/v1alpha1 + - networking/v1 + - policy/v1beta1 + - rbac/v1 + - rbac/v1alpha1 + - rbac/v1beta1 + - scheduling/v1alpha1 + - scheduling/v1beta1 + - settings/v1alpha1 + - storage/v1 + - storage/v1alpha1 + - storage/v1beta1 +- name: k8s.io/apimachinery + version: e226be1f5ed46b19fedc971f410f84de5b3dab8a + subpackages: + - pkg/apis/meta/v1 + - pkg/api/resource + - pkg/conversion + - pkg/fields + - pkg/labels + - pkg/runtime + - pkg/runtime/schema + - pkg/selection + - pkg/types + - pkg/util/intstr + - pkg/watch + - pkg/util/errors + - pkg/util/validation + - third_party/forked/golang/reflect + - pkg/util/sets + - pkg/conversion/queryparams + - pkg/util/json + - pkg/util/runtime + - pkg/util/net + - pkg/util/wait + - pkg/api/errors + - pkg/runtime/serializer + - pkg/version + - pkg/api/meta + - pkg/runtime/serializer/streaming + - pkg/util/clock + - pkg/util/validation/field + - pkg/runtime/serializer/json + - pkg/runtime/serializer/versioning + - pkg/runtime/serializer/protobuf + - pkg/runtime/serializer/recognizer + - pkg/apis/meta/v1beta1 + - pkg/util/framer + - pkg/util/yaml +- name: k8s.io/client-go + version: 9126b42c46c66dfbaafc366451e6e8a0f3789dd2 + subpackages: + - kubernetes + - tools/clientcmd + - discovery + - kubernetes/typed/admissionregistration/v1alpha1 + - kubernetes/typed/admissionregistration/v1beta1 + - kubernetes/typed/apps/v1 + - kubernetes/typed/apps/v1beta1 + - kubernetes/typed/apps/v1beta2 + - kubernetes/typed/authentication/v1 + - kubernetes/typed/authentication/v1beta1 + - kubernetes/typed/authorization/v1 + - kubernetes/typed/authorization/v1beta1 + - kubernetes/typed/autoscaling/v1 + - kubernetes/typed/autoscaling/v2beta1 + - kubernetes/typed/batch/v1 + - kubernetes/typed/batch/v1beta1 + - kubernetes/typed/batch/v2alpha1 + - kubernetes/typed/certificates/v1beta1 + - kubernetes/typed/core/v1 + - kubernetes/typed/events/v1beta1 + - kubernetes/typed/extensions/v1beta1 + - kubernetes/typed/networking/v1 + - kubernetes/typed/policy/v1beta1 + - kubernetes/typed/rbac/v1 + - kubernetes/typed/rbac/v1alpha1 + - kubernetes/typed/rbac/v1beta1 + - kubernetes/typed/scheduling/v1alpha1 + - kubernetes/typed/scheduling/v1beta1 + - kubernetes/typed/settings/v1alpha1 + - kubernetes/typed/storage/v1 + - kubernetes/typed/storage/v1alpha1 + - kubernetes/typed/storage/v1beta1 + - rest + - util/flowcontrol + - tools/auth + - tools/clientcmd/api + - tools/clientcmd/api/latest + - util/homedir + - kubernetes/scheme + - tools/reference + - pkg/version + - plugin/pkg/client/auth/exec + - rest/watch + - tools/metrics + - transport + - util/cert + - util/integer + - tools/clientcmd/api/v1 + - pkg/apis/clientauthentication + - pkg/apis/clientauthentication/v1alpha1 +testImports: [] From 8a0e0627f9b4bb53311a31fedf81d1a2e6083870 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 1 Jun 2018 13:49:59 -0400 Subject: [PATCH 4/4] Fix gitignore ignoring ltops package --- .gitignore | 1 - ltops/utils.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 ltops/utils.go diff --git a/.gitignore b/.gitignore index 5d76173f..654c1a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,3 @@ loadtestconfig.json /vendor /terraformenv -/ltops diff --git a/ltops/utils.go b/ltops/utils.go new file mode 100644 index 00000000..046c4fff --- /dev/null +++ b/ltops/utils.go @@ -0,0 +1,36 @@ +package ltops + +import ( + "bytes" + "io" + "net/http" + "os" + "strings" + + "github.com/pkg/errors" +) + +func GetFileOrURL(fileOrUrl string) ([]byte, error) { + buffer := bytes.NewBuffer(nil) + if strings.HasPrefix(fileOrUrl, "http") { + response, err := http.Get(fileOrUrl) + if err != nil { + return nil, errors.Wrap(err, "Can't get file at URL: "+fileOrUrl) + } + defer response.Body.Close() + + io.Copy(buffer, response.Body) + + return buffer.Bytes(), nil + } else { + f, err := os.Open(fileOrUrl) + if err != nil { + return nil, errors.Wrap(err, "unable to open file "+fileOrUrl) + } + defer f.Close() + + io.Copy(buffer, f) + + return buffer.Bytes(), nil + } +}