internal/version.go (118 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package internal
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/client-go/kubernetes"
)
// fallbackMaxVersion if we cannot detect version assume a max version to turn on all features
var fallbackMaxVersion = version.MustParseSemantic("999.9999.9999")
// logVersion log the given version or unknown if we only have the fallback.
func logVersion(v *version.Version) {
s := v.String()
if v == fallbackMaxVersion {
s = "unknown"
}
logger.Printf("ECK version is %s\n", s)
}
// detectECKVersion tries to detect the ECK version by inspecting the ECK operator stateful set or deployment.
func detectECKVersion(c *kubernetes.Clientset, namespace, userSpecifiedVersion string) *version.Version {
if userSpecifiedVersion != "" {
parsed, err := version.ParseSemantic(userSpecifiedVersion)
if err != nil {
logger.Println(err.Error())
return fallbackMaxVersion
}
return parsed
}
statefulSet, err := findOperatorStatefulSet(c, namespace)
if err != nil {
logger.Println(err.Error())
return fallbackMaxVersion
}
// we were not able to find a StatefulSet we might be dealing with an ECK operator deployed via OLM
if statefulSet == nil {
return extractVersionFromDeployment(c, namespace)
}
// since version 1.3 ECK uses standard labels
versionLabel := statefulSet.Labels["app.kubernetes.io/version"]
parsed, err := version.ParseSemantic(versionLabel)
if err == nil {
return parsed
}
return extractVersionFromContainers(statefulSet.Spec.Template.Spec.Containers)
}
func findOperatorStatefulSet(c *kubernetes.Clientset, namespace string) (*appsv1.StatefulSet, error) {
// we use the control-plane label since ECK version 1.0
ssets, err := c.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: "control-plane=elastic-operator"})
if err != nil {
return nil, err
}
// there is the possibility that users have deployed multiple ECK operators into the same namespace which we are
// ignoring here by assuming exactly one
if len(ssets.Items) == 1 {
return &ssets.Items[0], nil
}
// when deployed via Helm we don't have the control-plan label
ssets, err = c.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=Helm"})
if err != nil {
return nil, err
}
for _, set := range ssets.Items {
// unfortunately the chart name also encodes the version which is what we are trying to find out
// that's why we are doing a substring match here
if chart, ok := set.Labels["helm.sh/chart"]; ok && strings.Contains(chart, "eck-operator") {
return &set, nil
}
}
return nil, nil
}
// extractVersionFromContainers tries to find the operator container in the list of containers to extract version information.
func extractVersionFromContainers(containers []corev1.Container) *version.Version {
// try to parse the Docker image tag for older versions of ECK
for _, container := range containers {
// likely but not certain that this is the operator container
if strings.Contains(container.Image, "eck-operator") {
parsed, err := extractVersionFromDockerImage(container.Image)
if err != nil {
logger.Print(err.Error())
return fallbackMaxVersion
}
return parsed
}
}
return fallbackMaxVersion
}
// extractVersionFromDeployment tries to extract version information from a deployment as it is typically used in ECK installations via OLM.
func extractVersionFromDeployment(c *kubernetes.Clientset, namespace string) *version.Version {
deployment, err := c.AppsV1().Deployments(namespace).Get(context.Background(), "elastic-operator", metav1.GetOptions{})
if err != nil {
logger.Println(fmt.Errorf("operator statefulset not found, checking for OLM deployment but failed: %w", err).Error())
return fallbackMaxVersion
}
v, err := extractVersionFromOLMMetadata(deployment.Labels)
if err != nil {
logger.Println("ECK operator not found in OLM metadata checking container image as last resort")
return extractVersionFromContainers(deployment.Spec.Template.Spec.Containers)
}
return v
}
func extractVersionFromOLMMetadata(labels map[string]string) (*version.Version, error) {
if val, ok := labels["olm.owner"]; ok {
if _, v, found := strings.Cut(val, "."); found {
return version.ParseSemantic(v)
}
}
return nil, errors.New("no OLM metadata found")
}
// extractVersionFromDockerImage parses the version tag out of the given Docker image identifier.
func extractVersionFromDockerImage(image string) (*version.Version, error) {
regex := regexp.MustCompile(":([^@]+)")
matches := regex.FindAllStringSubmatch(image, -1)
if len(matches) == 1 && len(matches[0]) == 2 {
return version.ParseSemantic(matches[0][1])
}
return fallbackMaxVersion, nil
}
// maxVersion returns the maximum of the given versions.
func maxVersion(versions []*version.Version) *version.Version {
if len(versions) == 0 {
return fallbackMaxVersion
}
res := versions[0]
for _, v := range versions[1:] {
if res.LessThan(v) {
res = v
}
}
return res
}