forked from karmada-io/karmada
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In this commit, we unit test describe command on karmadactl by creating a fake rest client builder and make sure the described k8s object returned successfully when the command submitted. Signed-off-by: Mohamed Awnallah <[email protected]>
- Loading branch information
1 parent
ff92a84
commit 0d28b73
Showing
2 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,299 @@ | ||
/* | ||
Copyright 2024 The Karmada 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 describe | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
appsv1 "k8s.io/api/apps/v1" | ||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/meta" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
jsonk8serializer "k8s.io/apimachinery/pkg/runtime/serializer/json" | ||
"k8s.io/cli-runtime/pkg/genericiooptions" | ||
"k8s.io/cli-runtime/pkg/resource" | ||
fakerest "k8s.io/client-go/rest/fake" | ||
kubectldescribe "k8s.io/kubectl/pkg/cmd/describe" | ||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" | ||
describecmd "k8s.io/kubectl/pkg/describe" | ||
|
||
"github.com/karmada-io/karmada/pkg/karmadactl/options" | ||
) | ||
|
||
var deployment = &appsv1.Deployment{ | ||
TypeMeta: metav1.TypeMeta{ | ||
Kind: "Deployment", | ||
APIVersion: "apps/v1", | ||
}, | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "test-deployment", | ||
Namespace: "default", | ||
}, | ||
Spec: appsv1.DeploymentSpec{ | ||
Selector: &metav1.LabelSelector{ | ||
MatchLabels: map[string]string{"app": "nginx"}, | ||
}, | ||
Template: corev1.PodTemplateSpec{ | ||
ObjectMeta: metav1.ObjectMeta{}, | ||
Spec: corev1.PodSpec{ | ||
Containers: []corev1.Container{ | ||
{ | ||
Image: "nginx", | ||
ImagePullPolicy: "Always", | ||
Name: "nginx", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// var deploymentResponse = map[string]interface{}{ | ||
// "apiVersion": "apps/v1", | ||
// "kind": "Deployment", | ||
// "metadata": map[string]interface{}{ | ||
// "annotations": map[string]interface{}{ | ||
// "propagationpolicy.karmada.io/name": "nginx-propagation", | ||
// "propagationpolicy.karmada.io/namespace": "default", | ||
// }, | ||
// "labels": map[string]interface{}{ | ||
// "app": "nginx", | ||
// "propagationpolicy.karmada.io/permanent-id": "a06b8325-778f-4724-996b-9994a21394c5", | ||
// }, | ||
// "name": "nginx", | ||
// "namespace": "default", | ||
// }, | ||
// "spec": map[string]interface{}{ | ||
// "selector": map[string]interface{}{ | ||
// "matchLabels": map[string]string{ | ||
// "app": "nginx", | ||
// }, | ||
// }, | ||
// "template": map[string]interface{}{ | ||
// "metadata": map[string]interface{}{ | ||
// "creationTimestamp": nil, | ||
// "labels": map[string]string{ | ||
// "app": "nginx", | ||
// }, | ||
// }, | ||
// "spec": map[string]interface{}{ | ||
// "containers": []map[string]interface{}{ | ||
// { | ||
// "image": "nginx", | ||
// "imagePullPolicy": "Always", | ||
// "name": "nginx", | ||
// "resources": map[string]interface{}{}, | ||
// "terminationMessagePath": "/dev/termination-log", | ||
// "terminationMessagePolicy": "File", | ||
// }, | ||
// }, | ||
// "dnsPolicy": "ClusterFirst", | ||
// "restartPolicy": "Always", | ||
// "schedulerName": "default-scheduler", | ||
// "securityContext": map[string]interface{}{}, | ||
// "terminationGracePeriodSeconds": 30, | ||
// }, | ||
// }, | ||
// }, | ||
// "status": map[string]interface{}{ | ||
// "availableReplicas": 2, | ||
// "observedGeneration": 2, | ||
// "readyReplicas": 2, | ||
// "replicas": 2, | ||
// "updatedReplicas": 2, | ||
// }, | ||
// } | ||
|
||
type ResourceDescribe struct{} | ||
|
||
func (rd ResourceDescribe) Describe(string, string, describecmd.DescriberSettings) (output string, err error) { | ||
// Serialize the deployment response to JSON. | ||
bodyBytes, err := json.Marshal(deployment) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to serialize deployment response: %v", err) | ||
} | ||
|
||
// Return the serialized deployment as a string (pretty-printed JSON). | ||
return string(bodyBytes), nil | ||
} | ||
|
||
func TestValidate(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
describeOpts *CommandDescribeOptions | ||
wantErr bool | ||
errMsg string | ||
}{ | ||
{ | ||
name: "Validate_WithMemberOperationScopeAndWithoutCluster_MemberClusterOptMustBeSpecified", | ||
describeOpts: &CommandDescribeOptions{OperationScope: options.Members}, | ||
wantErr: true, | ||
errMsg: "must specify a member cluster", | ||
}, | ||
{ | ||
name: "Validate__WithMemberOperationScopeAndWithCluster_Validated", | ||
describeOpts: &CommandDescribeOptions{ | ||
OperationScope: options.Members, | ||
Cluster: "test-cluster", | ||
}, | ||
wantErr: false, | ||
}, | ||
} | ||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
err := test.describeOpts.Validate() | ||
if err == nil && test.wantErr { | ||
t.Fatal("expected an error, but got none") | ||
} | ||
if err != nil && !test.wantErr { | ||
t.Errorf("unexpected error, got: %v", err) | ||
} | ||
if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { | ||
t.Errorf("expected error message %s to be in %s", test.errMsg, err.Error()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestRunDescribe(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
describeOpts *CommandDescribeOptions | ||
testFactory *cmdtesting.TestFactory | ||
output *bytes.Buffer | ||
prep func(*CommandDescribeOptions, *cmdtesting.TestFactory, *bytes.Buffer) error | ||
cleanup func(*cmdtesting.TestFactory) error | ||
verify func(*bytes.Buffer) error | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Run_DescribeDeploymentInfo_DeploymentInfoDescribed", | ||
describeOpts: &CommandDescribeOptions{ | ||
KubectlDescribeOptions: &kubectldescribe.DescribeOptions{ | ||
FilenameOptions: &resource.FilenameOptions{}, | ||
DescriberSettings: &describecmd.DescriberSettings{}, | ||
BuilderArgs: []string{"deployment.apps"}, | ||
CmdParent: "test-deployment", | ||
Describer: func(*meta.RESTMapping) (describecmd.ResourceDescriber, error) { | ||
return ResourceDescribe{}, nil | ||
}, | ||
IOStreams: genericiooptions.IOStreams{}, | ||
}, | ||
}, | ||
testFactory: cmdtesting.NewTestFactory(), | ||
output: &bytes.Buffer{}, | ||
prep: func(describeOpts *CommandDescribeOptions, testFactory *cmdtesting.TestFactory, output *bytes.Buffer) error { | ||
client, err := generateFakeRestClient() | ||
if err != nil { | ||
return fmt.Errorf("failed to generate the fake rest client, got: %v", err) | ||
} | ||
testFactory.Client = client | ||
describeOpts.KubectlDescribeOptions.IOStreams.Out = output | ||
describeOpts.KubectlDescribeOptions.NewBuilder = func() *resource.Builder { | ||
return testFactory.NewBuilder() | ||
} | ||
return nil | ||
}, | ||
verify: func(output *bytes.Buffer) error { | ||
var deploymentUnmarshalled appsv1.Deployment | ||
err := json.Unmarshal(output.Bytes(), &deploymentUnmarshalled) | ||
if err != nil { | ||
return fmt.Errorf("error unmarshalling JSON: %v", err) | ||
} | ||
if deploymentUnmarshalled.Name != deployment.Name { | ||
return fmt.Errorf("expected deployment name to be %s, but got %s", deployment.Name, deploymentUnmarshalled.Name) | ||
} | ||
if deploymentUnmarshalled.Namespace != deployment.Namespace { | ||
return fmt.Errorf("expected deployment namespace to be %s, but got %s", deployment.Namespace, deploymentUnmarshalled.Namespace) | ||
} | ||
return nil | ||
}, | ||
cleanup: func(testFactory *cmdtesting.TestFactory) error { | ||
testFactory.Cleanup() | ||
return nil | ||
}, | ||
wantErr: false, | ||
}, | ||
} | ||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
if err := test.prep(test.describeOpts, test.testFactory, test.output); err != nil { | ||
t.Fatalf("failed to prep test environment, got error: %v", err) | ||
} | ||
defer func() { | ||
if err := test.cleanup(test.testFactory); err != nil { | ||
t.Errorf("failed to cleanup test environment, got error: %v", err) | ||
} | ||
}() | ||
err := test.describeOpts.Run() | ||
if err == nil && test.wantErr { | ||
t.Fatal("expected an error, but got none") | ||
} | ||
if err != nil && !test.wantErr { | ||
t.Errorf("unexpected error, got: %v", err) | ||
} | ||
if err := test.verify(test.output); err != nil { | ||
t.Errorf("failed to verify describing deployment, got error: %v", err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
// generateFakeRestClient creates and returns a fake REST client for testing purposes. | ||
func generateFakeRestClient() (cmdtesting.RESTClient, error) { | ||
// Define the runtime scheme. | ||
scheme := runtime.NewScheme() | ||
if err := appsv1.AddToScheme(scheme); err != nil { | ||
return nil, fmt.Errorf("failed to add scheme, got error: %v", err) | ||
} | ||
|
||
// Set up a JSON serializer. | ||
options := jsonk8serializer.SerializerOptions{ | ||
Yaml: false, | ||
Pretty: false, | ||
Strict: true, | ||
} | ||
// Serialize the deployment response to JSON. | ||
bodyBytes, err := json.Marshal(deployment) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to marshal deployment, got error: %v", err) | ||
} | ||
|
||
client := &fakerest.RESTClient{ | ||
NegotiatedSerializer: runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{ | ||
MediaType: "application/json", | ||
MediaTypeType: "application", | ||
MediaTypeSubType: "json", | ||
Serializer: jsonk8serializer.NewSerializerWithOptions(jsonk8serializer.DefaultMetaFactory, scheme, scheme, options), | ||
}), | ||
Client: fakerest.CreateHTTPClient(func(*http.Request) (*http.Response, error) { | ||
return &http.Response{ | ||
StatusCode: http.StatusOK, | ||
Body: io.NopCloser(bytes.NewReader(bodyBytes)), | ||
Header: http.Header{"Content-Type": []string{"application/json"}}, | ||
}, nil | ||
}), | ||
} | ||
return client, nil | ||
} |