diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml index b6873ea2ae2..845d5e3a9ca 100644 --- a/.github/workflows/kind.yml +++ b/.github/workflows/kind.yml @@ -741,6 +741,48 @@ jobs: path: log.tar.gz retention-days: 30 + run-post-installation-checks: + name: Test connectivity using 'antctl check' command + needs: [ build-antrea-coverage-image ] + runs-on: [ ubuntu-latest ] + steps: + - name: Free disk space + run: | + sudo apt-get clean + df -h + - uses: actions/checkout@v4 + with: + show-progress: false + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Download Antrea image from previous job + uses: actions/download-artifact@v4 + with: + name: antrea-ubuntu-cov + - name: Load Antrea image + run: | + docker load -i antrea-ubuntu.tar + - name: Install Kind + run: | + KIND_VERSION=$(head -n1 ./ci/kind/version) + curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-$(uname)-amd64 + chmod +x ./kind + sudo mv kind /usr/local/bin + - name: Create Kind Cluster + run: | + kind create cluster --config ci/kind/config-3nodes.yml + - name: Load Docker images and deploy Antrea + run: | + kind load docker-image antrea/antrea-controller-ubuntu-coverage:latest antrea/antrea-agent-ubuntu-coverage:latest + kubectl apply -f build/yamls/antrea.yml + - name: Build antctl binary + run: | + make antctl-linux + - name: Run antctl command + run: | + ./bin/antctl-linux check installation + validate-prometheus-metrics-doc: name: Validate metrics in Prometheus document match running deployment's needs: build-antrea-coverage-image diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index c5244adcc75..2addc2d44e9 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -19,6 +19,7 @@ import ( agentapis "antrea.io/antrea/pkg/agent/apis" fallbackversion "antrea.io/antrea/pkg/antctl/fallback/version" + checkinstallation "antrea.io/antrea/pkg/antctl/raw/check/installation" "antrea.io/antrea/pkg/antctl/raw/featuregates" "antrea.io/antrea/pkg/antctl/raw/multicluster" "antrea.io/antrea/pkg/antctl/raw/proxy" @@ -633,6 +634,12 @@ $ antctl get podmulticaststats pod -n namespace`, }, }, rawCommands: []rawCommand{ + { + cobraCommand: checkinstallation.Command(), + supportAgent: false, + supportController: false, + commandGroup: check, + }, { cobraCommand: supportbundle.Command, supportAgent: true, diff --git a/pkg/antctl/command_definition.go b/pkg/antctl/command_definition.go index 2382f320808..ff463548295 100644 --- a/pkg/antctl/command_definition.go +++ b/pkg/antctl/command_definition.go @@ -70,6 +70,7 @@ const ( query mc upgrade + check ) var groupCommands = map[commandGroup]*cobra.Command{ @@ -93,6 +94,10 @@ var groupCommands = map[commandGroup]*cobra.Command{ Short: "Sub-commands for upgrade operations", Long: "Sub-commands for upgrade operations", }, + check: { + Use: "check", + Short: "Performs pre and post installation checks", + }, } type endpointResponder interface { diff --git a/pkg/antctl/command_list.go b/pkg/antctl/command_list.go index e60f381e435..d780d9e697f 100644 --- a/pkg/antctl/command_list.go +++ b/pkg/antctl/command_list.go @@ -64,7 +64,8 @@ func (cl *commandList) applyToRootCommand(root *cobra.Command, client AntctlClie (runtime.Mode == runtime.ModeController && cmd.supportController) || (runtime.Mode == runtime.ModeFlowAggregator && cmd.supportFlowAggregator) || (!runtime.InPod && cmd.commandGroup == mc) || - (!runtime.InPod && cmd.commandGroup == upgrade) { + (!runtime.InPod && cmd.commandGroup == upgrade) || + (!runtime.InPod && cmd.commandGroup == check) { if groupCommand, ok := groupCommands[cmd.commandGroup]; ok { groupCommand.AddCommand(cmd.cobraCommand) } else { diff --git a/pkg/antctl/raw/check/installation/command.go b/pkg/antctl/raw/check/installation/command.go new file mode 100644 index 00000000000..405f7b13f04 --- /dev/null +++ b/pkg/antctl/raw/check/installation/command.go @@ -0,0 +1,404 @@ +// 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 installation + +import ( + "context" + "crypto/rand" + "fmt" + "net" + "os" + "time" + + "github.com/spf13/cobra" + 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/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + "antrea.io/antrea/pkg/antctl/raw/check" +) + +func Command() *cobra.Command { + o := newOptions() + command := &cobra.Command{ + Use: "installation", + Short: "Runs post 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") + return command +} + +type options struct { + antreaNamespace string +} + +func newOptions() *options { + return &options{ + antreaNamespace: "kube-system", + } +} + +const ( + testNamespacePrefix = "antrea-test" + clientDeploymentName = "test-client" + echoSameNodeDeploymentName = "echo-same-node" + echoOtherNodeDeploymentName = "echo-other-node" + kindEchoName = "echo" + kindClientName = "client" + agentDaemonSetName = "antrea-agent" + deploymentImage = "registry.k8s.io/e2e-test-images/agnhost:2.29" + 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 + clientPods []corev1.Pod + echoSameNodePod *corev1.Pod + echoOtherNodePod *corev1.Pod + 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 + } + for name, test := range testsRegistry { + testContext.Header("Running test: %s", name) + if err := test.Run(ctx, testContext); err != nil { + testContext.Header("Test %s failed: %s", name, err) + } else { + testContext.Header("Test %s passed", name) + } + } + testContext.Log("Test finished") + testContext.teardown(ctx) + return nil +} + +func agnhostConnectCommand(ip string, port string) []string { + hostPort := net.JoinHostPort(ip, port) + return []string{"/agnhost", "connect", hostPort, "--timeout=5s"} +} + +func newService(name string, selector map[string]string, port int) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + {Name: name, Port: int32(port)}, + }, + Selector: selector, + }, + } +} + +type deploymentParameters struct { + Name string + Role string + Image string + Replicas int + Port int + Command []string + Affinity *corev1.Affinity + Tolerations []corev1.Toleration + Labels map[string]string +} + +func newDeployment(p deploymentParameters) *appsv1.Deployment { + if p.Replicas == 0 { + p.Replicas = 1 + } + replicas32 := int32(p.Replicas) + labels := map[string]string{ + "name": p.Name, + "kind": p.Role, + } + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.Name, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.Name, + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: p.Name, + Env: []corev1.EnvVar{ + {Name: "PORT", Value: fmt.Sprintf("%d", p.Port)}, + }, + Ports: []corev1.ContainerPort{ + {ContainerPort: int32(p.Port)}, + }, + Image: p.Image, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: p.Command, + }, + }, + Affinity: p.Affinity, + Tolerations: p.Tolerations, + }, + }, + Replicas: &replicas32, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": p.Name, + "kind": p.Role, + }, + }, + }, + } +} + +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: generateRandomNamespace(testNamespacePrefix), + } +} + +func generateRandomNamespace(baseName string) string { + const letters = "abcdefghijklmnopqrstuvwxyz0123456789" + bytes := make([]byte, 5) + _, err := rand.Read(bytes) + if err != nil { + panic(err) + } + for i, b := range bytes { + bytes[i] = letters[b%byte(len(letters))] + } + return fmt.Sprintf("%s-%s", baseName, string(bytes)) +} + +func (t *testContext) teardown(ctx context.Context) { + t.Log("Deleting post installation tests setup...") + t.client.CoreV1().Namespaces().Delete(ctx, t.namespace, metav1.DeleteOptions{}) + t.Log("Waiting for Namespace %s to disappear", t.namespace) + err := wait.PollUntilContextTimeout(ctx, 2*time.Second, 1*time.Minute, true, func(ctx context.Context) (bool, error) { + _, err := t.client.CoreV1().Namespaces().Get(ctx, t.namespace, metav1.GetOptions{}) + if err != nil { + return true, nil + } + return false, nil + }) + if err != nil { + t.Log("Setup deletion failed") + } else { + t.Log("Setup deletion successful") + } +} + +func (t *testContext) setup(ctx context.Context) error { + t.Log("Test starting....") + _, err := t.client.AppsV1().DaemonSets(t.antreaNamespace).Get(ctx, agentDaemonSetName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("unable to determine status of Antrea DaemonSet: %w", err) + } + t.Log("Creating Namespace %s for post 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) + } + t.Log("Deploying echo-same-node Service %s...", echoSameNodeDeploymentName) + svc := newService(echoSameNodeDeploymentName, map[string]string{"name": echoSameNodeDeploymentName}, 80) + _, err = t.client.CoreV1().Services(t.namespace).Create(ctx, svc, metav1.CreateOptions{}) + if err != nil { + return err + } + commonToleration := []corev1.Toleration{ + { + Key: "node-role.kubernetes.io/control-plane", + Operator: "Exists", + Effect: "NoSchedule", + }, + } + echoDeployment := newDeployment(deploymentParameters{ + Name: echoSameNodeDeploymentName, + Role: kindEchoName, + Port: 80, + Image: deploymentImage, + Command: []string{"/agnhost", "netexec", "--http-port=80"}, + Affinity: &corev1.Affinity{ + PodAffinity: &corev1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{clientDeploymentName}, + }, + }, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + Tolerations: commonToleration, + Labels: map[string]string{"app": echoSameNodeDeploymentName}, + }) + _, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, echoDeployment, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("unable to create Deployment %s: %s", echoSameNodeDeploymentName, err) + } + t.Log("Deploying client Deployment %s...", clientDeploymentName) + clientDeployment := newDeployment(deploymentParameters{ + Name: clientDeploymentName, + Role: kindClientName, + Image: deploymentImage, + Command: []string{"/agnhost", "pause"}, + Port: 80, + Tolerations: commonToleration, + Labels: map[string]string{"app": clientDeploymentName}, + }) + _, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, clientDeployment, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("unable to create Deployment %s: %s", clientDeploymentName, err) + } + + t.Log("Deploying echo-other-node Service %s...", echoOtherNodeDeploymentName) + svc = newService(echoOtherNodeDeploymentName, map[string]string{"name": echoOtherNodeDeploymentName}, 80) + _, err = t.client.CoreV1().Services(t.namespace).Create(ctx, svc, metav1.CreateOptions{}) + if err != nil { + return err + } + echoOtherNodeDeployment := newDeployment(deploymentParameters{ + Name: echoOtherNodeDeploymentName, + Role: kindEchoName, + Port: 80, + Image: deploymentImage, + Command: []string{"/agnhost", "netexec", "--http-port=80"}, + Affinity: &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "name", Operator: metav1.LabelSelectorOpIn, Values: []string{clientDeploymentName}}, + }, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + Tolerations: commonToleration, + Labels: map[string]string{"app": echoOtherNodeDeploymentName}, + }) + nodes, err := t.client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("unable to list Nodes: %s", err) + } + if len(nodes.Items) >= 2 { + _, err = t.client.AppsV1().Deployments(t.namespace).Create(ctx, echoOtherNodeDeployment, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("unable to create Deployment %s: %s", echoOtherNodeDeploymentName, err) + } + if err := t.waitForDeploymentsReady(ctx, time.Second, podReadyTimeout, clientDeploymentName, echoSameNodeDeploymentName, echoOtherNodeDeploymentName); err != nil { + return err + } + podList, err := t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "name=" + echoOtherNodeDeploymentName}) + if err != nil { + return fmt.Errorf("unable to list Echo Other Node Pod: %s", err) + } + if len(podList.Items) > 0 { + t.echoOtherNodePod = &podList.Items[0] + } + } else { + t.Log("skipping other Node Deployments as multiple Nodes are not available") + if err := t.waitForDeploymentsReady(ctx, time.Second, podReadyTimeout, clientDeploymentName, echoSameNodeDeploymentName); err != nil { + return err + } + } + podList, err := t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "kind=" + kindClientName}) + if err != nil { + return fmt.Errorf("unable to list client Pods: %s", err) + } + t.clientPods = podList.Items + podList, err = t.client.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{LabelSelector: "name=" + echoSameNodeDeploymentName}) + if err != nil { + return fmt.Errorf("unable to list Echo Same Node Pod: %s", err) + } + if len(podList.Items) > 0 { + t.echoSameNodePod = &podList.Items[0] + } + t.Log("Deployment is validated successfully") + return nil +} + +func (t *testContext) waitForDeploymentsReady(ctx context.Context, interval, timeout time.Duration, deployments ...string) error { + for _, deployment := range deployments { + t.Log("Waiting for Deployment %s to become ready...", deployment) + err := wait.PollUntilContextTimeout(ctx, interval, timeout, false, func(ctx context.Context) (bool, error) { + ready, err := check.DeploymentIsReady(ctx, t.client, t.namespace, deployment) + if err != nil { + return false, fmt.Errorf("error checking readiness of Deployment %s: %w", deployment, err) + } + return ready, nil + }) + if err != nil { + return fmt.Errorf("waiting for Deployment %s to become ready has been interrupted: %w", deployment, err) + } + t.Log("Deployment %s is ready.", deployment) + } + return nil +} + +func (t *testContext) Log(format string, a ...interface{}) { + fmt.Fprintf(os.Stdout, fmt.Sprintf("[%s] ", t.clusterName)+format+"\n", a...) +} + +func (t *testContext) Header(format string, a ...interface{}) { + t.Log("-------------------------------------------------------------------------------------------") + t.Log(format, a...) + t.Log("-------------------------------------------------------------------------------------------") +} diff --git a/pkg/antctl/raw/check/installation/test_podtointernet.go b/pkg/antctl/raw/check/installation/test_podtointernet.go new file mode 100644 index 00000000000..50bc7978d54 --- /dev/null +++ b/pkg/antctl/raw/check/installation/test_podtointernet.go @@ -0,0 +1,41 @@ +// 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 installation + +import ( + "context" + "fmt" + + "antrea.io/antrea/pkg/antctl/raw/check" +) + +type PodToInternetConnectivityTest struct{} + +func init() { + RegisterTest("pod-to-internet-connectivity", &PodToInternetConnectivityTest{}) +} + +func (t *PodToInternetConnectivityTest) Run(ctx context.Context, testContext *testContext) error { + for _, clientPod := range testContext.clientPods { + srcPod := testContext.namespace + "/" + clientPod.Name + testContext.Log("Validating connectivity from Pod %s to the world (google.com)...", srcPod) + _, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, clientDeploymentName, agnhostConnectCommand("google.com", "80")) + if err != nil { + return fmt.Errorf("Pod %s was not able to connect to google.com: %w", srcPod, err) + } + testContext.Log("Pod %s was able to connect to google.com", srcPod) + } + return nil +} diff --git a/pkg/antctl/raw/check/installation/test_podtopodinternode.go b/pkg/antctl/raw/check/installation/test_podtopodinternode.go new file mode 100644 index 00000000000..b40f7719a17 --- /dev/null +++ b/pkg/antctl/raw/check/installation/test_podtopodinternode.go @@ -0,0 +1,48 @@ +// 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 installation + +import ( + "context" + "fmt" + + "antrea.io/antrea/pkg/antctl/raw/check" +) + +type PodToPodInterNodeConnectivityTest struct{} + +func init() { + RegisterTest("pod-to-pod-internode-connectivity", &PodToPodInterNodeConnectivityTest{}) +} + +func (t *PodToPodInterNodeConnectivityTest) Run(ctx context.Context, testContext *testContext) error { + if testContext.echoOtherNodePod == nil { + return fmt.Errorf("Skipping Inter-Node test because multiple Nodes are not available") + } + for _, clientPod := range testContext.clientPods { + srcPod := testContext.namespace + "/" + clientPod.Name + dstPod := testContext.namespace + "/" + testContext.echoOtherNodePod.Name + for _, podIP := range testContext.echoOtherNodePod.Status.PodIPs { + echoIP := podIP.IP + testContext.Log("Validating from Pod %s to Pod %s at IP %s...", srcPod, dstPod, echoIP) + _, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, "", agnhostConnectCommand(echoIP, "80")) + if err != nil { + return fmt.Errorf("client Pod %s was not able to communicate with echo Pod %s (%s): %w", clientPod.Name, testContext.echoOtherNodePod.Name, echoIP, err) + } + testContext.Log("client Pod %s was able to communicate with echo Pod %s (%s)", clientPod.Name, testContext.echoOtherNodePod.Name, echoIP) + } + } + return nil +} diff --git a/pkg/antctl/raw/check/installation/test_podtopodintranode.go b/pkg/antctl/raw/check/installation/test_podtopodintranode.go new file mode 100644 index 00000000000..5fae4cbe947 --- /dev/null +++ b/pkg/antctl/raw/check/installation/test_podtopodintranode.go @@ -0,0 +1,45 @@ +// 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 installation + +import ( + "context" + "fmt" + + "antrea.io/antrea/pkg/antctl/raw/check" +) + +type PodToPodIntraNodeConnectivityTest struct{} + +func init() { + RegisterTest("pod-to-pod-intranode-connectivity", &PodToPodIntraNodeConnectivityTest{}) +} + +func (t *PodToPodIntraNodeConnectivityTest) Run(ctx context.Context, testContext *testContext) error { + for _, clientPod := range testContext.clientPods { + srcPod := testContext.namespace + "/" + clientPod.Name + dstPod := testContext.namespace + "/" + testContext.echoSameNodePod.Name + for _, podIP := range testContext.echoSameNodePod.Status.PodIPs { + echoIP := podIP.IP + testContext.Log("Validating from Pod %s to Pod %s at IP %s...", srcPod, dstPod, echoIP) + _, _, err := check.ExecInPod(ctx, testContext.client, testContext.config, testContext.namespace, clientPod.Name, "", agnhostConnectCommand(echoIP, "80")) + if err != nil { + return fmt.Errorf("client Pod %s was not able to communicate with echo Pod %s (%s): %w", clientPod.Name, testContext.echoSameNodePod.Name, echoIP, err) + } + testContext.Log("client Pod %s was able to communicate with echo Pod %s (%s)", clientPod.Name, testContext.echoSameNodePod.Name, echoIP) + } + } + return nil +} diff --git a/pkg/antctl/raw/check/util.go b/pkg/antctl/raw/check/util.go new file mode 100644 index 00000000000..2a6e29936e6 --- /dev/null +++ b/pkg/antctl/raw/check/util.go @@ -0,0 +1,105 @@ +// 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 check + +import ( + "bytes" + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" +) + +func NewClient() (client kubernetes.Interface, config *rest.Config, clusterName string, err error) { + rules := clientcmd.NewDefaultClientConfigLoadingRules() + nonInteractiveClient := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{}) + config, err = nonInteractiveClient.ClientConfig() + if err != nil { + return nil, nil, "", err + } + rawConfig, err := nonInteractiveClient.RawConfig() + if err != nil { + return nil, nil, "", err + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, nil, "", err + } + contextName := rawConfig.CurrentContext + clusterName = "" + if context, ok := rawConfig.Contexts[contextName]; ok { + clusterName = context.Cluster + } + return clientset, config, clusterName, nil +} + +func DeploymentIsReady(ctx context.Context, client kubernetes.Interface, namespace, deploymentName string) (bool, error) { + deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, deploymentName, metav1.GetOptions{}) + if err != nil { + return false, err + } + if deployment.Generation <= deployment.Status.ObservedGeneration { + for _, cond := range deployment.Status.Conditions { + if cond.Type == appsv1.DeploymentProgressing && cond.Reason == "ProgressDeadlineExceeded" { + return false, fmt.Errorf("deployment %q exceeded its progress deadline", deployment.Name) + } + } + if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas { + return false, nil + } + if deployment.Status.Replicas > deployment.Status.UpdatedReplicas { + return false, nil + } + if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas { + return false, nil + } + return true, nil + } + return false, nil +} + +func ExecInPod(ctx context.Context, client kubernetes.Interface, config *rest.Config, namespace, pod, container string, command []string) (string, string, error) { + req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod).Namespace(namespace).SubResource("exec") + req.VersionedParams(&corev1.PodExecOptions{ + Command: command, + Container: container, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return "", "", fmt.Errorf("error while creating executor: %w", err) + } + var stdout, stderr bytes.Buffer + err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + if err != nil { + return "", "", fmt.Errorf("error in stream: %w", err) + } + return stdout.String(), stderr.String(), nil +}