diff --git a/pkg/apis/elasticsearch/v1/elasticsearch_config.go b/pkg/apis/elasticsearch/v1/elasticsearch_config.go index f3ff4e7953..e252dd6d72 100644 --- a/pkg/apis/elasticsearch/v1/elasticsearch_config.go +++ b/pkg/apis/elasticsearch/v1/elasticsearch_config.go @@ -5,16 +5,18 @@ package v1 import ( + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/go-ucfg" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" ) const ( - NodeData = "node.data" - NodeIngest = "node.ingest" - NodeMaster = "node.master" - NodeML = "node.ml" + NodeData = "node.data" + NodeIngest = "node.ingest" + NodeMaster = "node.master" + NodeML = "node.ml" + NodeTransform = "node.transform" ) // ClusterSettings is the cluster node in elasticsearch.yml. @@ -24,10 +26,11 @@ type ClusterSettings struct { // Node is the node section in elasticsearch.yml. type Node struct { - Master bool `config:"master"` - Data bool `config:"data"` - Ingest bool `config:"ingest"` - ML bool `config:"ml"` + Master bool `config:"master"` + Data bool `config:"data"` + Ingest bool `config:"ingest"` + ML bool `config:"ml"` + Transform bool `config:"transform"` // available as of 7.7.0 } // ElasticsearchSettings is a typed subset of elasticsearch.yml for purposes of the operator. @@ -37,18 +40,26 @@ type ElasticsearchSettings struct { } // DefaultCfg is an instance of ElasticsearchSettings with defaults set as they are in Elasticsearch. -var DefaultCfg = ElasticsearchSettings{ - Node: Node{ - Master: true, - Data: true, - Ingest: true, - ML: true, - }, +func DefaultCfg(ver version.Version) ElasticsearchSettings { + settings := ElasticsearchSettings{ + Node: Node{ + Master: true, + Data: true, + Ingest: true, + ML: true, + Transform: true, + }, + } + if !ver.IsSameOrAfter(version.From(7, 7, 0)) { + // this setting did not exist before 7.7.0 expressed here by setting it to false this allows us to keep working with just one model + settings.Node.Transform = false + } + return settings } // Unpack unpacks Config into a typed subset. -func UnpackConfig(c *commonv1.Config) (ElasticsearchSettings, error) { - esSettings := DefaultCfg // defensive copy +func UnpackConfig(c *commonv1.Config, ver version.Version) (ElasticsearchSettings, error) { + esSettings := DefaultCfg(ver) if c == nil { // make this nil safe to allow a ptr value to work around Json serialization issues return esSettings, nil diff --git a/pkg/apis/elasticsearch/v1/elasticsearch_config_test.go b/pkg/apis/elasticsearch/v1/elasticsearch_config_test.go index 4005f54759..76ec6af8f9 100644 --- a/pkg/apis/elasticsearch/v1/elasticsearch_config_test.go +++ b/pkg/apis/elasticsearch/v1/elasticsearch_config_test.go @@ -8,13 +8,15 @@ import ( "testing" commonv1 "github.com/elastic/cloud-on-k8s/pkg/apis/common/v1" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/go-test/deep" "github.com/stretchr/testify/require" ) func TestConfig_RoleDefaults(t *testing.T) { type args struct { - c2 commonv1.Config + c2 commonv1.Config + ver version.Version } tests := []struct { name string @@ -61,12 +63,36 @@ func TestConfig_RoleDefaults(t *testing.T) { }, want: false, }, + { + name: "version specific default differences 1", + c: commonv1.Config{ + Data: map[string]interface{}{ + NodeTransform: true, + }, + }, + args: args{ + ver: version.From(7, 5, 0), + }, + want: false, + }, + { + name: "version specific default differences 2", + c: commonv1.Config{ + Data: map[string]interface{}{ + NodeTransform: true, + }, + }, + args: args{ + ver: version.From(7, 7, 0), + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c1, err := UnpackConfig(&tt.c) + c1, err := UnpackConfig(&tt.c, tt.args.ver) require.NoError(t, err) - c2, err := UnpackConfig(&tt.args.c2) + c2, err := UnpackConfig(&tt.args.c2, tt.args.ver) require.NoError(t, err) if got := c1.Node == c2.Node; got != tt.want { t.Errorf("Config.EqualRoles() = %v, want %v", got, tt.want) @@ -148,6 +174,7 @@ func TestConfig_DeepCopy(t *testing.T) { } func TestConfig_Unpack(t *testing.T) { + ver := version.From(7, 7, 0) tests := []struct { name string args *commonv1.Config @@ -169,10 +196,11 @@ func TestConfig_Unpack(t *testing.T) { }, want: ElasticsearchSettings{ Node: Node{ - Master: false, - Data: true, - Ingest: true, - ML: true, + Master: false, + Data: true, + Ingest: true, + ML: true, + Transform: true, }, Cluster: ClusterSettings{ InitialMasterNodes: []string{"a", "b"}, @@ -183,13 +211,13 @@ func TestConfig_Unpack(t *testing.T) { { name: "Unpack is nil safe", args: nil, - want: DefaultCfg, + want: DefaultCfg(ver), wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := UnpackConfig(tt.args) + got, err := UnpackConfig(tt.args, ver) if (err != nil) != tt.wantErr { t.Errorf("Config.Unpack() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/apis/elasticsearch/v1/validations.go b/pkg/apis/elasticsearch/v1/validations.go index 37ee11cd19..6e8ec0826f 100644 --- a/pkg/apis/elasticsearch/v1/validations.go +++ b/pkg/apis/elasticsearch/v1/validations.go @@ -94,8 +94,12 @@ func supportedVersion(es *Elasticsearch) field.ErrorList { func hasMaster(es *Elasticsearch) field.ErrorList { var errs field.ErrorList var hasMaster bool + v, err := version.Parse(es.Spec.Version) + if err != nil { + errs = append(errs, field.Invalid(field.NewPath("spec").Child("version"), es.Spec.Version, parseVersionErrMsg)) + } for i, t := range es.Spec.NodeSets { - cfg, err := UnpackConfig(t.Config) + cfg, err := UnpackConfig(t.Config, *v) if err != nil { errs = append(errs, field.Invalid(field.NewPath("spec").Child("nodeSets").Index(i), t.Config, cfgInvalidMsg)) } diff --git a/pkg/controller/elasticsearch/label/label.go b/pkg/controller/elasticsearch/label/label.go index 852212b85f..9eaea4dd3e 100644 --- a/pkg/controller/elasticsearch/label/label.go +++ b/pkg/controller/elasticsearch/label/label.go @@ -40,6 +40,8 @@ const ( NodeTypesIngestLabelName common.TrueFalseLabel = "elasticsearch.k8s.elastic.co/node-ingest" // NodeTypesMLLabelName is a label set to true on nodes with the ml role NodeTypesMLLabelName common.TrueFalseLabel = "elasticsearch.k8s.elastic.co/node-ml" + // NodeTypesTransformLabelName is a label set to true on nodes with the transform role + NodeTypesTransformLabelName common.TrueFalseLabel = "elasticsearch.k8s.elastic.co/node-transform" HTTPSchemeLabelName = "elasticsearch.k8s.elastic.co/http-scheme" @@ -112,21 +114,25 @@ func NewLabels(es types.NamespacedName) map[string]string { func NewPodLabels( es types.NamespacedName, ssetName string, - version version.Version, + ver version.Version, nodeRoles esv1.Node, configHash string, scheme string, -) (map[string]string, error) { +) map[string]string { // cluster name based labels labels := NewLabels(es) // version label - labels[VersionLabelName] = version.String() + labels[VersionLabelName] = ver.String() // node types labels NodeTypesMasterLabelName.Set(nodeRoles.Master, labels) NodeTypesDataLabelName.Set(nodeRoles.Data, labels) NodeTypesIngestLabelName.Set(nodeRoles.Ingest, labels) NodeTypesMLLabelName.Set(nodeRoles.ML, labels) + // transform nodes were only added in 7.7.0 so we should not annotate previous versions with them + if ver.IsSameOrAfter(version.From(7, 7, 0)) { + NodeTypesTransformLabelName.Set(nodeRoles.Transform, labels) + } // config hash label, to rotate pods on config changes labels[ConfigHashLabelName] = configHash @@ -138,7 +144,7 @@ func NewPodLabels( labels[k] = v } - return labels, nil + return labels } // NewConfigLabels returns labels to apply for an Elasticsearch Config secret. diff --git a/pkg/controller/elasticsearch/label/label_test.go b/pkg/controller/elasticsearch/label/label_test.go index 77c8e6ccac..3091d0a537 100644 --- a/pkg/controller/elasticsearch/label/label_test.go +++ b/pkg/controller/elasticsearch/label/label_test.go @@ -8,7 +8,10 @@ import ( "reflect" "testing" + v1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" + "github.com/elastic/cloud-on-k8s/pkg/controller/common" "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" + "github.com/go-test/deep" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -168,3 +171,92 @@ func TestMinVersion(t *testing.T) { }) } } + +func TestNewPodLabels(t *testing.T) { + type args struct { + es types.NamespacedName + ssetName string + ver version.Version + nodeRoles v1.Node + configHash string + scheme string + } + nameFixture := types.NamespacedName{ + Namespace: "ns", + Name: "name", + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + { + name: "labels pre-7.7", + args: args{ + es: nameFixture, + ssetName: "sset", + ver: version.From(7, 1, 0), + nodeRoles: v1.Node{ + Master: false, + Data: false, + Ingest: false, + ML: false, + Transform: false, + }, + configHash: "hash", + scheme: "https", + }, + want: map[string]string{ + ClusterNameLabelName: "name", + common.TypeLabelName: "elasticsearch", + VersionLabelName: "7.1.0", + string(NodeTypesMasterLabelName): "false", + string(NodeTypesDataLabelName): "false", + string(NodeTypesIngestLabelName): "false", + string(NodeTypesMLLabelName): "false", + ConfigHashLabelName: "hash", + HTTPSchemeLabelName: "https", + StatefulSetNameLabelName: "sset", + }, + wantErr: false, + }, + { + name: "labels post-7.7", + args: args{ + es: nameFixture, + ssetName: "sset", + ver: version.From(7, 7, 0), + nodeRoles: v1.Node{ + Master: false, + Data: true, + Ingest: false, + ML: false, + Transform: true, + }, + configHash: "hash", + scheme: "https", + }, + want: map[string]string{ + ClusterNameLabelName: "name", + common.TypeLabelName: "elasticsearch", + VersionLabelName: "7.7.0", + string(NodeTypesMasterLabelName): "false", + string(NodeTypesDataLabelName): "true", + string(NodeTypesIngestLabelName): "false", + string(NodeTypesMLLabelName): "false", + string(NodeTypesTransformLabelName): "true", + ConfigHashLabelName: "hash", + HTTPSchemeLabelName: "https", + StatefulSetNameLabelName: "sset", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewPodLabels(tt.args.es, tt.args.ssetName, tt.args.ver, tt.args.nodeRoles, tt.args.configHash, tt.args.scheme) + require.Nil(t, deep.Equal(got, tt.want)) + }) + } +} diff --git a/pkg/controller/elasticsearch/nodespec/podspec.go b/pkg/controller/elasticsearch/nodespec/podspec.go index c09023e821..52e6c1227d 100644 --- a/pkg/controller/elasticsearch/nodespec/podspec.go +++ b/pkg/controller/elasticsearch/nodespec/podspec.go @@ -90,28 +90,25 @@ func buildLabels( nodeSet esv1.NodeSet, keystoreResources *keystore.Resources, ) (map[string]string, error) { - // label with a hash of the config to rotate the pod on config changes - unpackedCfg, err := cfg.Unpack() + // label with version + ver, err := version.Parse(es.Spec.Version) if err != nil { return nil, err } - nodeRoles := unpackedCfg.Node - cfgHash := hash.HashObject(cfg) - // label with version - ver, err := version.Parse(es.Spec.Version) + // label with a hash of the config to rotate the pod on config changes + unpackedCfg, err := cfg.Unpack(*ver) if err != nil { return nil, err } + nodeRoles := unpackedCfg.Node + cfgHash := hash.HashObject(cfg) - podLabels, err := label.NewPodLabels( + podLabels := label.NewPodLabels( k8s.ExtractNamespacedName(&es), esv1.StatefulSet(es.Name, nodeSet.Name), *ver, nodeRoles, cfgHash, es.Spec.HTTP.Protocol(), ) - if err != nil { - return nil, err - } if keystoreResources != nil { // label with a checksum of the secure settings to rotate the pod on secure settings change diff --git a/pkg/controller/elasticsearch/settings/canonical_config.go b/pkg/controller/elasticsearch/settings/canonical_config.go index a17c70c9f2..0958ecd05e 100644 --- a/pkg/controller/elasticsearch/settings/canonical_config.go +++ b/pkg/controller/elasticsearch/settings/canonical_config.go @@ -7,6 +7,7 @@ package settings import ( esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" common "github.com/elastic/cloud-on-k8s/pkg/controller/common/settings" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" ) // CanonicalConfig contains configuration for Elasticsearch ("elasticsearch.yml"), @@ -20,8 +21,8 @@ func NewCanonicalConfig() CanonicalConfig { } // Unpack returns a typed subset of Elasticsearch settings. -func (c CanonicalConfig) Unpack() (esv1.ElasticsearchSettings, error) { - cfg := esv1.DefaultCfg +func (c CanonicalConfig) Unpack(ver version.Version) (esv1.ElasticsearchSettings, error) { + cfg := esv1.DefaultCfg(ver) err := c.CanonicalConfig.Unpack(&cfg) return cfg, err } diff --git a/test/e2e/test/elasticsearch/checks_es.go b/test/e2e/test/elasticsearch/checks_es.go index a47c5d73bc..d50cea41c7 100644 --- a/test/e2e/test/elasticsearch/checks_es.go +++ b/test/e2e/test/elasticsearch/checks_es.go @@ -11,6 +11,7 @@ import ( "strings" esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/client" "github.com/elastic/cloud-on-k8s/test/e2e/test" "k8s.io/apimachinery/pkg/api/resource" @@ -114,6 +115,11 @@ func (e *esClusterChecks) CheckESNodesTopology(es esv1.Elasticsearch) test.Step ) } + v, err := version.Parse(es.Spec.Version) + if err != nil { + return err + } + // flatten the topology var expectedTopology []esv1.NodeSet for _, node := range es.Spec.NodeSets { @@ -148,7 +154,7 @@ func (e *esClusterChecks) CheckESNodesTopology(es esv1.Elasticsearch) test.Step nodeRoles := rolesToConfig(node.Roles) nodeStats := nodesStats.Nodes[nodeID] for i, topoElem := range expectedTopology { - cfg, err := esv1.UnpackConfig(topoElem.Config) + cfg, err := esv1.UnpackConfig(topoElem.Config, *v) if err != nil { return err } @@ -190,6 +196,8 @@ func rolesToConfig(roles []string) esv1.Node { node.Data = true case "ingest": node.Ingest = true + case "transform": + node.Transform = true } } return node diff --git a/test/e2e/test/elasticsearch/settings.go b/test/e2e/test/elasticsearch/settings.go index c55e7f6028..55353c91ea 100644 --- a/test/e2e/test/elasticsearch/settings.go +++ b/test/e2e/test/elasticsearch/settings.go @@ -7,22 +7,24 @@ package elasticsearch import ( esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" common "github.com/elastic/cloud-on-k8s/pkg/controller/common/settings" + "github.com/elastic/cloud-on-k8s/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/settings" ) func MustNumDataNodes(es esv1.Elasticsearch) int { var numNodes int + ver := version.MustParse(es.Spec.Version) for _, n := range es.Spec.NodeSets { - if isDataNode(n) { + if isDataNode(n, ver) { numNodes += int(n.Count) } } return numNodes } -func isDataNode(node esv1.NodeSet) bool { +func isDataNode(node esv1.NodeSet, ver version.Version) bool { if node.Config == nil { - return esv1.DefaultCfg.Node.Data // if not specified use the default + return esv1.DefaultCfg(ver).Node.Data // if not specified use the default } config, err := common.NewCanonicalConfigFrom(node.Config.Data) if err != nil { @@ -30,7 +32,7 @@ func isDataNode(node esv1.NodeSet) bool { } nodeCfg, err := settings.CanonicalConfig{ CanonicalConfig: config, - }.Unpack() + }.Unpack(ver) if err != nil { panic(err) }