Skip to content

Commit

Permalink
Add Mattermost config get/set functionality
Browse files Browse the repository at this point in the history
This initial implementation is done by executing the Mattermost
CLI in one of the running containers of the cluster installation.

In the future, we should move to a direct API approach and should
use Kubernetes jobs. That said, this change lays the groundwork
for running arbitrary commands in the Mattermost pods.
  • Loading branch information
gabrieljackson committed Aug 6, 2019
1 parent d12ca03 commit 442f1eb
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 53 deletions.
30 changes: 20 additions & 10 deletions cmd/cloud/cluster_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,29 @@ import (
func init() {
clusterInstallationCmd.PersistentFlags().String("server", "http://localhost:8075", "The provisioning server whose API will be queried.")

clusterInstallationGetCmd.Flags().String("cluster_installation", "", "The id of the cluster installation to be fetched.")
clusterInstallationGetCmd.MarkFlagRequired("cluster_installation")
clusterInstallationGetCmd.Flags().String("cluster-installation", "", "The id of the cluster installation to be fetched.")
clusterInstallationGetCmd.MarkFlagRequired("cluster-installation")

clusterInstallationListCmd.Flags().String("cluster", "", "The cluster by which to filter cluster installations.")
clusterInstallationListCmd.Flags().String("installation", "", "The installation by which to filter cluster installations.")
clusterInstallationListCmd.Flags().Int("page", 0, "The page of cluster installations to fetch, starting at 0.")
clusterInstallationListCmd.Flags().Int("per-page", 100, "The number of cluster installations to fetch per page.")
clusterInstallationListCmd.Flags().Bool("include-deleted", false, "Whether to include deleted cluster installations.")

clusterInstallationConfigGetCmd.Flags().String("installation", "", "The id of the installation.")
clusterInstallationConfigGetCmd.MarkFlagRequired("installation")
clusterInstallationConfigCmd.PersistentFlags().String("cluster-installation", "", "The id of the cluster installation.")
clusterInstallationConfigCmd.MarkFlagRequired("cluster-installation")

clusterInstallationConfigSetCmd.Flags().String("installation", "", "The id of the installation.")
clusterInstallationConfigSetCmd.Flags().String("key", "", "The configuration key to update (e.g. ServiceSettings.SiteURL).")
clusterInstallationConfigSetCmd.Flags().String("value", "", "The value to write to the config.")
clusterInstallationConfigSetCmd.MarkFlagRequired("key")
clusterInstallationConfigSetCmd.MarkFlagRequired("value")

clusterInstallationCmd.AddCommand(clusterInstallationGetCmd)
clusterInstallationCmd.AddCommand(clusterInstallationListCmd)
clusterInstallationCmd.AddCommand(clusterInstallationConfigCmd)

clusterInstallationConfigCmd.AddCommand(clusterInstallationConfigGetCmd)
clusterInstallationConfigCmd.AddCommand(clusterInstallationConfigSetCmd)
}

var clusterInstallationCmd = &cobra.Command{
Expand All @@ -47,7 +50,7 @@ var clusterInstallationGetCmd = &cobra.Command{
serverAddress, _ := command.Flags().GetString("server")
client := model.NewClient(serverAddress)

clusterInstallationID, _ := command.Flags().GetString("cluster_installation")
clusterInstallationID, _ := command.Flags().GetString("cluster-installation")
clusterInstallation, err := client.GetClusterInstallation(clusterInstallationID)
if err != nil {
return errors.Wrap(err, "failed to query cluster installation")
Expand Down Expand Up @@ -100,6 +103,11 @@ var clusterInstallationListCmd = &cobra.Command{
},
}

var clusterInstallationConfigCmd = &cobra.Command{
Use: "config",
Short: "Manipulate a particular cluster installation's config.",
}

var clusterInstallationConfigGetCmd = &cobra.Command{
Use: "get",
Short: "Get a particular cluster installation's config.",
Expand All @@ -109,7 +117,7 @@ var clusterInstallationConfigGetCmd = &cobra.Command{
serverAddress, _ := command.Flags().GetString("server")
client := model.NewClient(serverAddress)

clusterInstallationID, _ := command.Flags().GetString("cluster_installation")
clusterInstallationID, _ := command.Flags().GetString("cluster-installation")
clusterInstallationConfig, err := client.GetClusterInstallationConfig(clusterInstallationID)
if err != nil {
return errors.Wrap(err, "failed to query cluster installation config")
Expand All @@ -136,16 +144,18 @@ var clusterInstallationConfigSetCmd = &cobra.Command{
serverAddress, _ := command.Flags().GetString("server")
client := model.NewClient(serverAddress)

clusterInstallationID, _ := command.Flags().GetString("cluster_installation")
clusterInstallationID, _ := command.Flags().GetString("cluster-installation")
key, _ := command.Flags().GetString("key")
value, _ := command.Flags().GetString("value")

config := make(map[string]interface{})
keyParts := strings.Split(key, ".")
configRef := config
for i, keyPart := range keyParts {
if i < len(keyParts) {
configRef[keyPart] = make(map[string]interface{})
if i < len(keyParts)-1 {
value := make(map[string]interface{})
configRef[keyPart] = value
configRef = value
} else {
configRef[keyPart] = value
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/cloud/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,10 @@ var serverCmd = &cobra.Command{
router := mux.NewRouter()

api.Register(router, &api.Context{
Store: sqlStore,
Supervisor: supervisor,
Logger: logger,
Store: sqlStore,
Supervisor: supervisor,
Provisioner: kopsProvisioner,
Logger: logger,
})

listen, _ := command.Flags().GetString("listen")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ require (
k8s.io/apimachinery v0.0.0-20190425132440-17f84483f500
k8s.io/client-go v0.0.0-20190425172711-65184652c889
k8s.io/kube-openapi v0.0.0-20190425185145-07b897206552 // indirect
k8s.io/kubernetes v1.14.2
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 // indirect
)
9 changes: 5 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
Expand Down Expand Up @@ -104,6 +106,7 @@ github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:Fecb
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down Expand Up @@ -142,10 +145,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattermost/mattermost-operator v0.0.0-20190611211530-f6f1f8912464 h1:zLgqnvvyg2hgAwOV4j9TmPBjbYyfoKASIAqZogAZsjs=
github.com/mattermost/mattermost-operator v0.0.0-20190611211530-f6f1f8912464/go.mod h1:Rinpkr6ordw53vU23itYjIaypS117QJwCGKr3YiriNI=
github.com/mattermost/mattermost-operator v0.0.0-20190708223312-f6904f9f4903 h1:Rb93w5lelKUVC0jt+c+dWpLj7X5bM7Y3VwmqO2SMlh0=
github.com/mattermost/mattermost-operator v0.0.0-20190708223312-f6904f9f4903/go.mod h1:Rinpkr6ordw53vU23itYjIaypS117QJwCGKr3YiriNI=
github.com/mattermost/mattermost-operator v0.0.0-20190710123602-6a20f0845534 h1:OJkXg5TKCPMq7vM+F8pG+4qm276UzqpaSbEvfWct6EM=
github.com/mattermost/mattermost-operator v0.0.0-20190710123602-6a20f0845534/go.mod h1:Rinpkr6ordw53vU23itYjIaypS117QJwCGKr3YiriNI=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
Expand Down Expand Up @@ -305,6 +304,8 @@ k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/kube-openapi v0.0.0-20190425185145-07b897206552 h1:g4LfmaPWHOlwAB8RuUEuV8AWk1iFl81RjccZSm2NZsY=
k8s.io/kube-openapi v0.0.0-20190425185145-07b897206552/go.mod h1:CLkc+4zFUoGjPZyFIWC3FUADCMC+x6rpxJ/G64tX+Y0=
k8s.io/kubernetes v1.14.2 h1:Gdq2hPpttbaJBoClIanCE6WSu4IZReA54yhkZtvPUOo=
k8s.io/kubernetes v1.14.2/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 h1:8r+l4bNWjRlsFYlQJnKJ2p7s1YQPj4XyXiJVqDHRx7c=
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
Expand Down
91 changes: 86 additions & 5 deletions internal/api/cluster_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,27 @@ func handleGetClusterInstallationConfig(c *Context, w http.ResponseWriter, r *ht
return
}

// TODO: fetch the config for output
w.WriteHeader(http.StatusNotImplemented)
cluster, err := c.Store.GetCluster(clusterInstallation.ClusterID)
if err != nil {
c.Logger.WithError(err).Error("failed to query cluster")
w.WriteHeader(http.StatusInternalServerError)
return
}
if cluster == nil {
c.Logger.Errorf("failed to find cluster %s associated with cluster installations", clusterInstallation.ClusterID)
w.WriteHeader(http.StatusInternalServerError)
return
}

output, err := c.Provisioner.ExecMattermostCLI(cluster, clusterInstallation, "config", "show", "--json")
if err != nil {
c.Logger.WithError(err).Error("failed to execute mattermost cli")
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Write(output)
}

// handleSetClusterInstallationConfig responds to PUT /api/cluster_installation/{cluster_installation}/config, merging the given config into the given cluster installation.
Expand All @@ -104,7 +123,7 @@ func handleSetClusterInstallationConfig(c *Context, w http.ResponseWriter, r *ht
clusterInstallationID := vars["cluster_installation"]
c.Logger = c.Logger.WithField("cluster_installation", clusterInstallationID)

_, err := model.NewClusterInstallationConfigRequestFromReader(r.Body)
clusterInstallationConfigRequest, err := model.NewClusterInstallationConfigRequestFromReader(r.Body)
if err != nil {
c.Logger.WithError(err).Error("failed to decode request")
w.WriteHeader(http.StatusBadRequest)
Expand All @@ -122,6 +141,68 @@ func handleSetClusterInstallationConfig(c *Context, w http.ResponseWriter, r *ht
return
}

// TODO: write the config
w.WriteHeader(http.StatusNotImplemented)
cluster, err := c.Store.GetCluster(clusterInstallation.ClusterID)
if err != nil {
c.Logger.WithError(err).Error("failed to query cluster")
w.WriteHeader(http.StatusInternalServerError)
return
}
if cluster == nil {
w.WriteHeader(http.StatusNotFound)
return
}

var applyConfig func(parentKey string, value map[string]interface{}) error

// applyConfig takes the decomposed configuration, walks the resulting map, and invokes
// something akin to:
// mattermost config set <ParentKey1.ParentKey2.LeafKey> <value>
//
// Ideally, this would be replaced by simply using the API and passing in the config struct
// directly, but at the moment that requires authentication.
applyConfig = func(parentKey string, parentValue map[string]interface{}) error {
if parentKey != "" {
parentKey = parentKey + "."
}

for key, value := range parentValue {
fullKey := parentKey + key

valueMap, ok := value.(map[string]interface{})
if ok {
err = applyConfig(fullKey, valueMap)
if err != nil {
return err
}

continue
}

valueStr, ok := value.(string)
if ok {
_, err := c.Provisioner.ExecMattermostCLI(cluster, clusterInstallation, "config", "set", fullKey, valueStr)
if err != nil {
c.Logger.WithError(err).Errorf("failed to set key %s to value %s", fullKey, valueStr)
return err
}

c.Logger.Infof("Successfully set config key %s to value %s", fullKey, valueStr)
continue
}

c.Logger.WithError(err).Errorf("unable to set key %s with value %t", fullKey, value)
return err
}

return nil
}

err = applyConfig("", clusterInstallationConfigRequest)
if err != nil {
c.Logger.WithError(err).Error("failed to set the config")
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
}
50 changes: 26 additions & 24 deletions internal/api/cluster_installation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,20 +255,25 @@ func TestGetClusterInstallationConfig(t *testing.T) {

router := mux.NewRouter()
api.Register(router, &api.Context{
Store: sqlStore,
Supervisor: &mockSupervisor{},
Logger: logger,
Store: sqlStore,
Supervisor: &mockSupervisor{},
Provisioner: &mockProvisioner{},
Logger: logger,
})
ts := httptest.NewServer(router)
defer ts.Close()

client := model.NewClient(ts.URL)

cluster := &model.Cluster{}
err := sqlStore.CreateCluster(cluster)
require.NoError(t, err)

clusterInstallation1 := &model.ClusterInstallation{
ClusterID: model.NewID(),
ClusterID: cluster.ID,
InstallationID: model.NewID(),
}
err := sqlStore.CreateClusterInstallation(clusterInstallation1)
err = sqlStore.CreateClusterInstallation(clusterInstallation1)
require.NoError(t, err)

t.Run("unknown cluster installation", func(t *testing.T) {
Expand All @@ -277,14 +282,10 @@ func TestGetClusterInstallationConfig(t *testing.T) {
require.Nil(t, config)
})

t.Run("not yet implemented", func(t *testing.T) {
clusterInstallation1.State = model.ClusterInstallationStateStable
err = sqlStore.UpdateClusterInstallation(clusterInstallation1)
require.NoError(t, err)

t.Run("success", func(t *testing.T) {
config, err := client.GetClusterInstallationConfig(clusterInstallation1.ID)
require.EqualError(t, err, "failed with status code 501")
require.Nil(t, config)
require.NoError(t, err)
require.Contains(t, config, "ServiceSettings")
})
}

Expand All @@ -294,20 +295,25 @@ func TestSetClusterInstallationConfig(t *testing.T) {

router := mux.NewRouter()
api.Register(router, &api.Context{
Store: sqlStore,
Supervisor: &mockSupervisor{},
Logger: logger,
Store: sqlStore,
Supervisor: &mockSupervisor{},
Provisioner: &mockProvisioner{},
Logger: logger,
})
ts := httptest.NewServer(router)
defer ts.Close()

client := model.NewClient(ts.URL)

cluster := &model.Cluster{}
err := sqlStore.CreateCluster(cluster)
require.NoError(t, err)

clusterInstallation1 := &model.ClusterInstallation{
ClusterID: model.NewID(),
ClusterID: cluster.ID,
InstallationID: model.NewID(),
}
err := sqlStore.CreateClusterInstallation(clusterInstallation1)
err = sqlStore.CreateClusterInstallation(clusterInstallation1)
require.NoError(t, err)

config := map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": "test.example.com"}}
Expand All @@ -332,15 +338,11 @@ func TestSetClusterInstallationConfig(t *testing.T) {

resp, err := http.DefaultClient.Do(httpRequest)
require.NoError(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
require.Equal(t, http.StatusOK, resp.StatusCode)
})

t.Run("not yet implemented", func(t *testing.T) {
clusterInstallation1.State = model.ClusterInstallationStateStable
err = sqlStore.UpdateClusterInstallation(clusterInstallation1)
require.NoError(t, err)

t.Run("success", func(t *testing.T) {
err := client.SetClusterInstallationConfig(clusterInstallation1.ID, config)
require.EqualError(t, err, "failed with status code 501")
require.NoError(t, err)
})
}
9 changes: 9 additions & 0 deletions internal/api/common_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package api_test

import "github.com/mattermost/mattermost-cloud/model"

type mockSupervisor struct {
}

func (s *mockSupervisor) Do() error {
return nil
}

type mockProvisioner struct {
}

func (s *mockProvisioner) ExecMattermostCLI(*model.Cluster, *model.ClusterInstallation, ...string) ([]byte, error) {
return []byte(`{"ServiceSettings":{"SiteURL":"http://test.example.com"}}`), nil
}

func sToP(s string) *string {
return &s
}
Loading

0 comments on commit 442f1eb

Please sign in to comment.