Skip to content

Commit

Permalink
cnf ran: ztp test migration preliminaries
Browse files Browse the repository at this point in the history
This is the first PR for migrating the ZTP test suite and includes the ztp directory with the helper and tsparams packages. Additionally, some helpers and parameters are moved from the TALM suite into the ran directory so they may be shared with the ZTP tests.
  • Loading branch information
klaskosk committed Jun 27, 2024
1 parent 0401912 commit 5fcf539
Show file tree
Hide file tree
Showing 25 changed files with 1,119 additions and 290 deletions.
15 changes: 15 additions & 0 deletions tests/cnf/ran/internal/cluster/cluster.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cluster

import (
"context"
"fmt"
"os/exec"
"strings"
"time"

Expand All @@ -12,6 +14,19 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ExecLocalCommand runs the provided command with the provided args locally, cancelling execution if it exceeds
// timeout.
func ExecLocalCommand(timeout time.Duration, command string, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()

glog.V(ranparam.LogLevel).Infof("Locally executing command '%s' with args '%v'", command, args)

output, err := exec.CommandContext(ctx, command, args...).Output()

return string(output), err
}

// ExecCmd executes a command on the provided client on each node matching nodeSelector, retrying on internal errors
// retries times with a 10 second delay between retries, and ignores the stdout.
func ExecCmd(client *clients.Settings, retries uint, nodeSelector, command string) error {
Expand Down
1 change: 1 addition & 0 deletions tests/cnf/ran/internal/ranconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type RANConfig struct {
OcpUpgradeUpstreamURL string `yaml:"ocpUpgradeUpstreamUrl" envconfig:"ECO_CNF_RAN_OCP_UPGRADE_UPSTREAM_URL"`
PtpOperatorNamespace string `yaml:"ptpOperatorNamespace" envconfig:"ECO_CNF_RAN_PTP_OPERATOR_NAMESPACE"`
TalmPreCachePolicies []string `yaml:"talmPreCachePolicies" envconfig:"ECO_CNF_RAN_TALM_PRECACHE_POLICIES"`
ZtpSiteGenerateImage string `yaml:"ztpSiteGenerateImage" envconfig:"ECO_CNF_RAN_ZTP_SITE_GENERATE_IMAGE"`
}

// NewRANConfig returns an instance of RANConfig.
Expand Down
1 change: 1 addition & 0 deletions tests/cnf/ran/internal/ranconfig/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ ptpOperatorNamespace: "openshift-ptp"
talmPreCachePolicies:
- "common-config-policy"
- "common-subscriptions-policy"
ztpSiteGenerateImage: "registry-proxy.engineering.redhat.com/rh-osbs/openshift4-ztp-site-generate"
...
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package helper
package ranhelper

import (
"github.com/golang/glog"
"github.com/openshift-kni/eco-goinfra/pkg/pod"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/ranparam"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

// IsPodHealthy returns true if a given pod is healthy, otherwise false.
Expand Down Expand Up @@ -41,6 +43,25 @@ func DoesContainerExistInPod(pod *pod.Builder, containerName string) bool {
return false
}

// UnmarshalRaw converts raw bytes for a K8s CR into the actual type.
func UnmarshalRaw[T any](raw []byte) (*T, error) {
untyped := &unstructured.Unstructured{}
err := untyped.UnmarshalJSON(raw)

if err != nil {
return nil, err
}

var typed T
err = runtime.DefaultUnstructuredConverter.FromUnstructured(untyped.UnstructuredContent(), &typed)

if err != nil {
return nil, err
}

return &typed, nil
}

// isPodInCondition returns true if a given pod is in expected condition, otherwise false.
func isPodInCondition(pod *pod.Builder, condition v1.PodConditionType) bool {
for _, c := range pod.Object.Status.Conditions {
Expand Down
211 changes: 211 additions & 0 deletions tests/cnf/ran/internal/ranhelper/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package ranhelper

import (
"errors"
"fmt"
"math"
"os"
"strconv"
"strings"

"github.com/golang/glog"
"github.com/openshift-kni/eco-goinfra/pkg/clients"
"github.com/openshift-kni/eco-goinfra/pkg/deployment"
"github.com/openshift-kni/eco-goinfra/pkg/olm"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/raninittools"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/ranparam"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
)

// InitializeSpokeNames initializes the name of spoke 1 and, if present, spoke 2.
func InitializeSpokeNames() error {
var err error

// Spoke 1 is required to be present.
ranparam.Spoke1Name, err = getClusterName(os.Getenv("KUBECONFIG"))
if err != nil {
return err
}

// Spoke 2 is optional depending on the test.
if raninittools.RANConfig.Spoke2Kubeconfig != "" {
ranparam.Spoke2Name, err = getClusterName(raninittools.RANConfig.Spoke2Kubeconfig)
if err != nil {
return err
}
}

return nil
}

// InitializeTalmVersion initializes the version of the TALM operator from the hub cluster.
func InitializeTalmVersion() error {
var err error

ranparam.TalmVersion, err = getOperatorVersionFromCsv(
raninittools.HubAPIClient, ranparam.TalmOperatorHubNamespace, ranparam.OpenshiftOperatorNamespace)

return err
}

// InitializeVersions initializes the versions of ACM, TALM, and ZTP from the hub cluster.
func InitializeVersions() error {
var err error

err = InitializeTalmVersion()
if err != nil {
return err
}

ranparam.AcmVersion, err = getOperatorVersionFromCsv(
raninittools.HubAPIClient, ranparam.AcmOperatorName, ranparam.AcmOperatorNamespace)
if err != nil {
return err
}

ranparam.ZtpVersion, err = getZtpVersionFromArgoCd(
raninittools.HubAPIClient, ranparam.OpenshiftGitopsRepoServer, ranparam.OpenshiftGitops)

return err
}

// IsVersionStringInRange checks if a version string is between a specified min and max value, inclusive. All the string
// inputs to this function should be dot separated positive intergers, e.g. "1.0.0" or "4.10". Each string input must be
// at least two dot separarted integers but may also be 3 or more, though only the first two are compared.
func IsVersionStringInRange(version, minimum, maximum string) (bool, error) {
versionValid, versionDigits := validateInputString(version)
minimumValid, minimumDigits := validateInputString(minimum)
maximumValid, maximumDigits := validateInputString(maximum)

if !minimumValid {
// Only accept invalid empty strings
if minimum != "" {
return false, fmt.Errorf("invalid minimum provided: '%s'", minimum)
}

// Assume the minimum digits are [0,0] for later comparison
minimumDigits = []int{0, 0}
}

if !maximumValid {
// Only accept invalid empty strings
if maximum != "" {
return false, fmt.Errorf("invalid maximum provided: '%s'", maximum)
}

// Assume the maximum digits are [math.MaxInt, math.MaxInt] for later comparison
maximumDigits = []int{math.MaxInt, math.MaxInt}
}

// If the version was not valid then we need to check the min and max
if !versionValid {
// If no min or max was defined then return true
if !minimumValid && !maximumValid {
return true, nil
}

// Otherwise return whether the input maximum was an empty string or not
return maximum == "", nil
}

// Otherwise the versions were valid so compare the digits
for i := 0; i < 2; i++ {
// The version bit should be between the minimum and maximum
if versionDigits[i] < minimumDigits[i] || versionDigits[i] > maximumDigits[i] {
return false, nil
}
}

// At the end if we never returned then all the digits were in valid range
return true, nil
}

// validateInputString validates that a string is at least two dot separated nonnegative integers.
func validateInputString(input string) (bool, []int) {
versionSplits := strings.Split(input, ".")

if len(versionSplits) < 2 {
return false, []int{}
}

digits := []int{}

for i := 0; i < 2; i++ {
digit, err := strconv.Atoi(versionSplits[i])
if err != nil || digit < 0 {
return false, []int{}
}

digits = append(digits, digit)
}

return true, digits
}

// getClusterName extracts the cluster name from provided kubeconfig, assuming there's one cluster in the kubeconfig.
func getClusterName(kubeconfigPath string) (string, error) {
rawConfig, _ := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&clientcmd.ConfigOverrides{
CurrentContext: "",
}).RawConfig()

for _, cluster := range rawConfig.Clusters {
// Get a cluster name by parsing it from the server hostname. Expects the url to start with
// `https://api.cluster-name.` so splitting by `.` gives the cluster name.
splits := strings.Split(cluster.Server, ".")
clusterName := splits[1]

glog.V(ranparam.LogLevel).Infof("cluster name %s found for kubeconfig at %s", clusterName, kubeconfigPath)

return clusterName, nil
}

return "", fmt.Errorf("could not get cluster name for kubeconfig at %s", kubeconfigPath)
}

// getOperatorVersionFromCsv returns operator version from csv, or an empty string if no CSV for the provided operator
// is found.
func getOperatorVersionFromCsv(client *clients.Settings, operatorName, operatorNamespace string) (string, error) {
csv, err := olm.ListClusterServiceVersion(client, operatorNamespace, metav1.ListOptions{})
if err != nil {
return "", err
}

for _, csv := range csv {
if strings.Contains(csv.Object.Name, operatorName) {
return csv.Object.Spec.Version.String(), nil
}
}

return "", fmt.Errorf("could not find version for operator %s in namespace %s", operatorName, operatorNamespace)
}

// getZtpVersionFromArgoCd is used to fetch the version of the ztp-site-generate init container.
func getZtpVersionFromArgoCd(client *clients.Settings, name, namespace string) (string, error) {
ztpDeployment, err := deployment.Pull(client, name, namespace)
if err != nil {
return "", err
}

for _, container := range ztpDeployment.Definition.Spec.Template.Spec.InitContainers {
// Match both the `ztp-site-generator` and `ztp-site-generate` images since which one matches is version
// dependent.
if strings.Contains(container.Image, "ztp-site-gen") {
colonSplit := strings.Split(container.Image, ":")
ztpVersion := colonSplit[len(colonSplit)-1]

if ztpVersion == "latest" {
glog.V(ranparam.LogLevel).Info("ztp-site-generate version tag was 'latest', returning empty version")

return "", nil
}

// The format here will be like vX.Y.Z so we need to remove the v at the start.
return ztpVersion[1:], nil
}
}

return "", errors.New("unable to identify ZTP version")
}
16 changes: 11 additions & 5 deletions tests/cnf/ran/internal/ranparam/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import "github.com/golang/glog"

const (
// Label represents the label for the ran test cases.
Label string = "ran"
Label = "ran"
// AcmOperatorName operator name of ACM.
AcmOperatorName string = "advanced-cluster-management"
AcmOperatorName = "advanced-cluster-management"
// AcmOperatorNamespace ACM's namespace.
AcmOperatorNamespace string = "rhacm"
AcmOperatorNamespace = "rhacm"
// TalmOperatorHubNamespace TALM namespace.
TalmOperatorHubNamespace = "topology-aware-lifecycle-manager"
// TalmContainerName is the name of the container in the talm pod.
TalmContainerName = "manager"
// OpenshiftOperatorNamespace is the namespace where operators are.
OpenshiftOperatorNamespace = "openshift-operators"
// OpenshiftGitops name.
OpenshiftGitops string = "openshift-gitops"
OpenshiftGitops = "openshift-gitops"
// OpenshiftGitopsRepoServer ocp git repo server.
OpenshiftGitopsRepoServer string = "openshift-gitops-repo-server"
OpenshiftGitopsRepoServer = "openshift-gitops-repo-server"
// PtpContainerName is the name of the container in the PTP daemon pod.
PtpContainerName = "linuxptp-daemon-container"
// PtpDaemonsetLabelSelector is the label selector to find the PTP daemon pod.
Expand Down
18 changes: 16 additions & 2 deletions tests/cnf/ran/internal/ranparam/ranvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,19 @@ package ranparam

import "github.com/openshift-kni/eco-gotests/tests/cnf/internal/cnfparams"

// Labels represents the range of labels that can be used for test cases selection.
var Labels = []string{cnfparams.Label, Label}
var (
// Labels represents the range of labels that can be used for test cases selection.
Labels = []string{cnfparams.Label, Label}

// Spoke1Name is the name of the first spoke cluster.
Spoke1Name string
// Spoke2Name is the name of the second spoke cluster.
Spoke2Name string

// AcmVersion is the version of the ACM operator.
AcmVersion string
// TalmVersion is the version of the TALM operator.
TalmVersion string
// ZtpVersion is the version of the ZTP from ArgoCD.
ZtpVersion string
)
4 changes: 2 additions & 2 deletions tests/cnf/ran/talm/internal/helper/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/openshift-kni/eco-goinfra/pkg/nodes"
"github.com/openshift-kni/eco-goinfra/pkg/ocm"
"github.com/openshift-kni/eco-goinfra/pkg/pod"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/helper"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/ranhelper"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/internal/raninittools"
"github.com/openshift-kni/eco-gotests/tests/cnf/ran/talm/internal/tsparams"
v1 "github.com/openshift/api/config/v1"
Expand Down Expand Up @@ -170,7 +170,7 @@ func waitForAllPodsHealthy(client *clients.Settings, namespaces []string, timeou
}

for _, namespacePod := range namespacePods {
healthy := helper.IsPodHealthy(namespacePod)
healthy := ranhelper.IsPodHealthy(namespacePod)

// Ignore failed pod with restart policy never. This could happen in image pruner or installer pods that
// will never restart. For those pods, instead of restarting the same pod, a new pod will be created
Expand Down
Loading

0 comments on commit 5fcf539

Please sign in to comment.