Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-installation testing framework #6278

Merged
merged 14 commits into from
May 14, 2024
15 changes: 9 additions & 6 deletions .github/workflows/kind.yml
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,8 @@ jobs:
path: log.tar.gz
retention-days: 30

run-post-installation-checks:
name: Test connectivity using 'antctl check' command
run-installation-checks:
name: Test installation using 'antctl check' command
needs: [ build-antrea-coverage-image ]
runs-on: [ ubuntu-latest ]
steps:
Expand Down Expand Up @@ -772,13 +772,16 @@ jobs:
- name: Create Kind Cluster
run: |
./ci/kind/kind-setup.sh create kind --ip-family dual
- name: Deploy Antrea
run: |
kubectl apply -f build/yamls/antrea.yml
- name: Build antctl binary
run: |
make antctl-linux
- name: Run antctl command
- name: Run Pre checks
run: |
/bin/antctl-linux check cluster
- name: Deploy Antrea
run: |
kubectl apply -f build/yamls/antrea.yml
- name: Run Post checks
run: |
./bin/antctl-linux check installation

Expand Down
7 changes: 7 additions & 0 deletions pkg/antctl/antctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

agentapis "antrea.io/antrea/pkg/agent/apis"
fallbackversion "antrea.io/antrea/pkg/antctl/fallback/version"
checkcluster "antrea.io/antrea/pkg/antctl/raw/check/cluster"
checkinstallation "antrea.io/antrea/pkg/antctl/raw/check/installation"
"antrea.io/antrea/pkg/antctl/raw/featuregates"
"antrea.io/antrea/pkg/antctl/raw/multicluster"
Expand Down Expand Up @@ -640,6 +641,12 @@ $ antctl get podmulticaststats pod -n namespace`,
supportController: false,
commandGroup: check,
},
{
cobraCommand: checkcluster.Command(),
supportAgent: false,
supportController: false,
commandGroup: check,
},
{
cobraCommand: supportbundle.Command,
supportAgent: true,
Expand Down
210 changes: 210 additions & 0 deletions pkg/antctl/raw/check/cluster/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright 2024 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cluster

import (
"context"
"fmt"
"os"
"time"

"github.com/fatih/color"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/utils/ptr"

"antrea.io/antrea/pkg/antctl/raw/check"
)

func Command() *cobra.Command {
o := newOptions()
command := &cobra.Command{
Use: "cluster",
Short: "Runs pre installation checks",
RunE: func(cmd *cobra.Command, args []string) error {
return Run(o)
},
}
command.Flags().StringVarP(&o.antreaNamespace, "namespace", "n", o.antreaNamespace, "Configure Namespace in which Antrea is running")
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
return command
}

type options struct {
antreaNamespace string
}

func newOptions() *options {
return &options{
antreaNamespace: "kube-system",
}
}

const (
antreaNamespace = "kube-system"
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
testNamespace = "antrea-test"
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
deploymentName = "check-cluster"
tnqn marked this conversation as resolved.
Show resolved Hide resolved
podReadyTimeout = 1 * time.Minute
)

type Test interface {
Run(ctx context.Context, testContext *testContext) error
}

var testsRegistry = make(map[string]Test)

func RegisterTest(name string, test Test) {
testsRegistry[name] = test
}

type testContext struct {
client kubernetes.Interface
config *rest.Config
clusterName string
antreaNamespace string
namespace string
}

func Run(o *options) error {
client, config, clusterName, err := check.NewClient()
if err != nil {
return fmt.Errorf("unable to create Kubernetes client: %s", err)
}
ctx := context.Background()
testContext := NewTestContext(client, config, clusterName, o)
if err := testContext.setup(ctx); err != nil {
return err
}
var numSuccess, numFailure int
for name, test := range testsRegistry {
testContext.Header("Running test: %s", name)
if err := test.Run(ctx, testContext); err != nil {
testContext.Fail("Test %s failed: %v", name, err)
numFailure++
} else {
testContext.Success("Test %s passed", name)
numSuccess++
}
}
testContext.Log("Test finished: %v tests succeeded, %v tests failed ", numSuccess, numFailure)
check.Teardown(ctx, testContext.client, testContext.clusterName, testContext.namespace)
if numFailure > 0 {
return fmt.Errorf("%v/%v tests failed", numFailure, len(testsRegistry))
}
return nil
}

func (t *testContext) setup(ctx context.Context) error {
t.Log("Creating Namespace %s for pre installation tests...", t.namespace)
_, err := t.client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: t.namespace}}, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("unable to create Namespace %s: %s", t.namespace, err)
}
deployment := check.NewDeployment(check.DeploymentParameters{
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
Name: deploymentName,
Image: "antrea/antrea-agent-ubuntu:latest",
tnqn marked this conversation as resolved.
Show resolved Hide resolved
Replicas: 1,
Command: []string{"sleep", "infinity"},
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
Labels: map[string]string{"app": "check-cluster"},
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
HostNetwork: true,
VolumeMounts: []corev1.VolumeMount{
{Name: "cni-conf", MountPath: "/etc/cni/net.d"},
{Name: "lib-modules", MountPath: "/lib/modules"},
},
Tolerations: []corev1.Toleration{
{
Key: "node-role.kubernetes.io/control-plane",
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
Operator: "Exists",
Effect: "NoSchedule",
},
{
Key: "node-role.kubernetes.io/master",
Operator: "Exists",
Effect: "NoSchedule",
},
{
Key: "node.kubernetes.io/not-ready",
Operator: "Exists",
Effect: "NoSchedule",
},
},
Volumes: []corev1.Volume{
{
Name: "cni-conf",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/etc/cni/net.d",
},
},
},
{
Name: "lib-modules",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/lib/modules",
Type: ptr.To(corev1.HostPathType("Directory")),
},
},
},
},
})

t.Log("Creating Deployment")
_, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, deployment, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("unable to create Deployment: %w", err)
}

t.Log("Waiting for Deployment to become ready")
check.WaitForDeploymentsReady(ctx, time.Second, podReadyTimeout, t.client, t.clusterName, t.namespace, deploymentName)
if err != nil {
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("error while waiting for Deployment to become ready: %w", err)
}
return nil
}

func NewTestContext(client kubernetes.Interface, config *rest.Config, clusterName string, o *options) *testContext {
return &testContext{
client: client,
config: config,
clusterName: clusterName,
antreaNamespace: o.antreaNamespace,
namespace: check.GenerateRandomNamespace(testNamespace),
}
}

func (t *testContext) Log(format string, a ...interface{}) {
fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+format+"\n", a...)
}

func (t *testContext) Success(format string, a ...interface{}) {
fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.GreenString(format, a...)+"\n")
}

func (t *testContext) Fail(format string, a ...interface{}) {
fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.RedString(format, a...)+"\n")
}

func (t *testContext) Warning(format string, a ...interface{}) {
fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+color.YellowString(format, a...)+"\n")
}

func (t *testContext) Header(format string, a ...interface{}) {
t.Log("-------------------------------------------------------------------------------------------")
t.Log(format, a...)
t.Log("-------------------------------------------------------------------------------------------")
}
61 changes: 61 additions & 0 deletions pkg/antctl/raw/check/cluster/test_checkcniexistence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2024 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cluster

import (
"context"
"fmt"
"sort"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"antrea.io/antrea/pkg/antctl/raw/check"
)

type checkCNIExistence struct{}

func init() {
RegisterTest("Check if another CNI is present", &checkCNIExistence{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name is very different from the ones in installation check: "pod-to-internet-connectivity", "pod-to-pod-internode-connectivity".
I think it should align with the latter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

}

func (t *checkCNIExistence) Run(ctx context.Context, testContext *testContext) error {
pods, err := testContext.client.CoreV1().Pods(testContext.namespace).List(ctx, metav1.ListOptions{LabelSelector: "name=check-cluster"})
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("failed to list Pods: %v", err)
}
command := []string{"ls", "-1", "/etc/cni/net.d"}
output, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, pods.Items[0].Name, "", command)
if err != nil {
testContext.Log("Failed to execute command in Pod: %s, error: %v", pods.Items[0].Name, err)
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
}
outputStr := strings.TrimSpace(output)
if outputStr == "" {
testContext.Log("No files present in /etc/cni/net.d in Pod: %s", pods.Items[0].Name)
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
} else {
files := strings.Split(outputStr, "\n")
sort.Strings(files)
if len(files) > 0 {
if files[0] < "10-antrea.conflist" {
testContext.Log("Another CNI configuration file with higher priority than Antrea's CNI configuration file found: %s", files[0])
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
} else if files[0] != "10-antrea.conflist" {
testContext.Log("Another CNI configuration file found: %s with Antrea having higher precedence", files[0])
} else {
testContext.Log("Antrea's CNI configuration file already present: %s", files[0])
}
}
}
return nil
}
51 changes: 51 additions & 0 deletions pkg/antctl/raw/check/cluster/test_checkcontrolplaneavailability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2024 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cluster

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
)

type checkControlPlaneAvailability struct{}

func init() {
RegisterTest("Check control plane Nodes availability", &checkControlPlaneAvailability{})
}

func (t *checkControlPlaneAvailability) Run(ctx context.Context, testContext *testContext) error {
controlPlaneNodes := sets.New[string]()
controlPlaneLabels := []string{"node-role.kubernetes.io/control-plane", "node-role.kubernetes.io/master"}
for _, label := range controlPlaneLabels {
nodes, err := testContext.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: label})
if err != nil {
return fmt.Errorf("failed to list Nodes with label %s: %w", label, err)
}
for idx := range nodes.Items {
controlPlaneNodes.Insert(nodes.Items[idx].Name)
}
}
if controlPlaneNodes.Len() == 0 {
testContext.Log("No control-plane or master Nodes were found; if installing Antrea in encap mode, some K8s functionalities (API aggregation, apiserver proxy, admission controllers) may be impacted.")
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
} else {
for _, node := range controlPlaneNodes.UnsortedList() {
testContext.Log("Nodes found : %s", node)
}
kanha-gupta marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}
Loading
Loading