mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Merge pull request #59220 from neolit123/test-token
Automatic merge from submit-queue (batch tested with PRs 59220, 62927, 63084, 63090, 62284). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. kubeadm: add better test coverage to token.go **What this PR does / why we need it**: a PR for adding some more tests in `kubeadm/cmd` for `token.go`. some areas of the `token.go` like listing, creating and deleting tokens can present challenges. coverage was increased to around 87%. **Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: please, link issue # if you know of such. **Special notes for your reviewer**: none **Release note**: ```release-note NONE ```
This commit is contained in:
commit
47ece3a2ca
@ -98,6 +98,7 @@ go_test(
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//vendor/k8s.io/client-go/testing:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/bootstrap/token/api:go_default_library",
|
||||
"//vendor/k8s.io/utils/exec:go_default_library",
|
||||
"//vendor/k8s.io/utils/exec/testing:go_default_library",
|
||||
],
|
||||
|
@ -18,8 +18,17 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@ -27,11 +36,47 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
bootstrapapi "k8s.io/client-go/tools/bootstrap/token/api"
|
||||
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenExpectedRegex = "^\\S{6}\\.\\S{16}\n$"
|
||||
TestConfig = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data:
|
||||
server: localhost:8000
|
||||
name: prod
|
||||
contexts:
|
||||
- context:
|
||||
cluster: prod
|
||||
namespace: default
|
||||
user: default-service-account
|
||||
name: default
|
||||
current-context: default
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kubernetes-admin
|
||||
user:
|
||||
client-certificate-data:
|
||||
client-key-data:
|
||||
`
|
||||
TestConfigCertAuthorityData = "certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFM01USXhOREUxTlRFek1Gb1hEVEkzTVRJeE1qRTFOVEV6TUZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTlZrCnNkT0NjRDBIOG9ycXZ5djBEZ09jZEpjRGc4aTJPNGt3QVpPOWZUanJGRHJqbDZlVXRtdlMyZ1lZd0c4TGhPV2gKb0lkZ3AvbVkrbVlDakliUUJtTmE2Ums1V2JremhJRzM1c1lseE9NVUJJR0xXMzN0RTh4SlR1RVd3V0NmZnpLcQpyaU1UT1A3REF3MUxuM2xUNlpJNGRNM09NOE1IUk9Wd3lRMDVpbWo5eUx5R1lYdTlvSncwdTVXWVpFYmpUL3VpCjJBZ2QwVDMrZGFFb044aVBJOTlVQkQxMzRkc2VGSEJEY3hHcmsvVGlQdHBpSC9IOGoxRWZaYzRzTGlONzJmL2YKYUpacTROSHFiT2F5UkpITCtJejFNTW1DRkN3cjdHOHVENWVvWWp2dEdZN2xLc1pBTlUwK3VlUnJsTitxTzhQWQpxaTZNMDFBcmV1UzFVVHFuTkM4Q0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFNbXo4Nm9LMmFLa0owMnlLSC9ZcTlzaDZZcDEKYmhLS25mMFJCaTA1clRacWdhTi9oTnROdmQxSzJxZGRLNzhIT2pVdkpNRGp3NERieXF0Wll2V01XVFRCQnQrSgpPMGNyWkg5NXlqUW42YzRlcU1FTjFhOUFKNXRlclNnTDVhREJsK0FMTWxaNVpxTzBUOUJDdTJtNXV3dGNWaFZuCnh6cGpTT3V5WVdOQ3A5bW9mV2VPUTljNXhEcElWeUlMUkFvNmZ5Z2c3N25TSDN4ckVmd0VKUHFMd1RPYVk1bTcKeEZWWWJoR3dxUGU5V0I5aTR5cnNrZUFBWlpUSzdVbklKMXFkRmlHQk9aZlRtaDhYQ3BOTHZZcFBLQW9hWWlsRwpjOW1acVhpWVlESTV6R1IxMElpc2FWNXJUY2hDenNQVWRhQzRVbnpTZG01cTdKYTAyb0poQlU1TE1FMD0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
|
||||
TestConfigNoCluster = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server:
|
||||
name: prod
|
||||
contexts:
|
||||
- context:
|
||||
namespace: default
|
||||
user: default-service-account
|
||||
name: default
|
||||
kind: Config
|
||||
preferences: {}
|
||||
`
|
||||
)
|
||||
|
||||
func TestRunGenerateToken(t *testing.T) {
|
||||
@ -46,7 +91,7 @@ func TestRunGenerateToken(t *testing.T) {
|
||||
|
||||
matched, err := regexp.MatchString(TokenExpectedRegex, output)
|
||||
if err != nil {
|
||||
t.Fatalf("encountered an error while trying to match RunGenerateToken's output: %v", err)
|
||||
t.Fatalf("Encountered an error while trying to match RunGenerateToken's output: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("RunGenerateToken's output did not match expected regex; wanted: [%s], got: [%s]", TokenExpectedRegex, output)
|
||||
@ -65,6 +110,7 @@ func TestRunCreateToken(t *testing.T) {
|
||||
token string
|
||||
usages []string
|
||||
extraGroups []string
|
||||
printJoin bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
@ -123,10 +169,18 @@ func TestRunCreateToken(t *testing.T) {
|
||||
extraGroups: []string{"system:bootstrappers:foo"},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: print join command",
|
||||
token: "",
|
||||
usages: []string{"signing", "authentication"},
|
||||
extraGroups: []string{"system:bootstrappers:foo"},
|
||||
printJoin: true,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
|
||||
cfg := &kubeadmapiext.MasterConfiguration{
|
||||
|
||||
// KubernetesVersion is not used by bootstrap-token, but we set this explicitly to avoid
|
||||
// the lookup of the version from the internet when executing ConfigFileAndDefaultsToInternalConfig
|
||||
KubernetesVersion: "v1.9.0",
|
||||
@ -136,9 +190,345 @@ func TestRunCreateToken(t *testing.T) {
|
||||
TokenGroups: tc.extraGroups,
|
||||
}
|
||||
|
||||
err := RunCreateToken(&buf, fakeClient, "", cfg, "", false, "")
|
||||
err := RunCreateToken(&buf, fakeClient, "", cfg, "", tc.printJoin, "")
|
||||
if (err != nil) != tc.expectedError {
|
||||
t.Errorf("Test case %s: RunCreateToken expected error: %v, saw: %v", tc.name, tc.expectedError, (err != nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCmdTokenGenerate(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
args := []string{}
|
||||
|
||||
cmd := NewCmdTokenGenerate(&buf)
|
||||
cmd.SetArgs(args)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Errorf("Cannot execute token command: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCmdToken(t *testing.T) {
|
||||
var buf, bufErr bytes.Buffer
|
||||
testConfigFile := "test-config-file"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "kubeadm-token-test")
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
fullPath := filepath.Join(tmpDir, testConfigFile)
|
||||
|
||||
f, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create test file %q: %v", fullPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configToWrite string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "valid: generate",
|
||||
args: []string{"generate"},
|
||||
configToWrite: "",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "valid: delete",
|
||||
args: []string{"delete", "abcdef.1234567890123456", "--dry-run", "--kubeconfig=" + fullPath},
|
||||
configToWrite: TestConfig,
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdToken(&buf, &bufErr)
|
||||
for _, tc := range testCases {
|
||||
if _, err = f.WriteString(tc.configToWrite); err != nil {
|
||||
t.Errorf("Unable to write test file %q: %v", fullPath, err)
|
||||
}
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
if (err != nil) != tc.expectedError {
|
||||
t.Errorf("Test case %q: NewCmdToken expected error: %v, saw: %v", tc.name, tc.expectedError, (err != nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecretString(t *testing.T) {
|
||||
secret := v1.Secret{}
|
||||
key := "test-key"
|
||||
if str := getSecretString(&secret, key); str != "" {
|
||||
t.Errorf("getSecretString() did not return empty string for a nil v1.Secret.Data")
|
||||
}
|
||||
secret.Data = make(map[string][]byte)
|
||||
if str := getSecretString(&secret, key); str != "" {
|
||||
t.Errorf("getSecretString() did not return empty string for missing v1.Secret.Data key")
|
||||
}
|
||||
secret.Data[key] = []byte("test-value")
|
||||
if str := getSecretString(&secret, key); str == "" {
|
||||
t.Errorf("getSecretString() failed for a valid v1.Secret.Data key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientset(t *testing.T) {
|
||||
testConfigFile := "test-config-file"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "kubeadm-token-test")
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
fullPath := filepath.Join(tmpDir, testConfigFile)
|
||||
|
||||
// test dryRun = false on a non-exisiting file
|
||||
if _, err = getClientset(fullPath, false); err == nil {
|
||||
t.Errorf("getClientset(); dry-run: false; did no fail for test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
// test dryRun = true on a non-exisiting file
|
||||
if _, err = getClientset(fullPath, true); err == nil {
|
||||
t.Errorf("getClientset(); dry-run: true; did no fail for test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
f, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create test file %q: %v", fullPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.WriteString(TestConfig); err != nil {
|
||||
t.Errorf("Unable to write test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
// test dryRun = true on an exisiting file
|
||||
if _, err = getClientset(fullPath, true); err != nil {
|
||||
t.Errorf("getClientset(); dry-run: true; failed for test file %q: %v", fullPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDeleteToken(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "kubeadm-token-test")
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
fullPath := filepath.Join(tmpDir, "test-config-file")
|
||||
|
||||
f, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create test file %q: %v", fullPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.WriteString(TestConfig); err != nil {
|
||||
t.Errorf("Unable to write test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
client, err := getClientset(fullPath, true)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to run getClientset() for test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
// test valid; should not fail
|
||||
// for some reason Secrets().Delete() does not fail even for this dummy config
|
||||
if err = RunDeleteToken(&buf, client, "abcdef.1234567890123456"); err != nil {
|
||||
t.Errorf("RunDeleteToken() failed for a valid token: %v", err)
|
||||
}
|
||||
|
||||
// test invalid token; should fail
|
||||
if err = RunDeleteToken(&buf, client, "invalid-token"); err == nil {
|
||||
t.Errorf("RunDeleteToken() succeeded for an invalid token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var httpTestItr uint32
|
||||
var httpSentResponse uint32 = 1
|
||||
|
||||
func TestRunListTokens(t *testing.T) {
|
||||
var err error
|
||||
var bufOut, bufErr bytes.Buffer
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "kubeadm-token-test")
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
fullPath := filepath.Join(tmpDir, "test-config-file")
|
||||
|
||||
f, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to create test file %q: %v", fullPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// test config without secrets; should fail
|
||||
if _, err = f.WriteString(TestConfig); err != nil {
|
||||
t.Errorf("Unable to write test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
client, err := getClientset(fullPath, true)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to run getClientset() for test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
if err = RunListTokens(&bufOut, &bufErr, client); err == nil {
|
||||
t.Errorf("RunListTokens() did not fail for a config without secrets: %v", err)
|
||||
}
|
||||
|
||||
// test config without secrets but use a dummy API server that returns secrets
|
||||
portString := "9008"
|
||||
http.HandleFunc("/", httpHandler)
|
||||
httpServer := &http.Server{Addr: "localhost:" + portString}
|
||||
go func() {
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to start dummy API server: localhost:%s", portString)
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("dummy API server listening on localhost:%s\n", portString)
|
||||
testConfigOpenPort := strings.Replace(TestConfig, "server: localhost:8000", "server: localhost:"+portString, -1)
|
||||
|
||||
if _, err = f.WriteString(testConfigOpenPort); err != nil {
|
||||
t.Errorf("Unable to write test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
client, err = getClientset(fullPath, true)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to run getClientset() for test file %q: %v", fullPath, err)
|
||||
}
|
||||
|
||||
// the order of these tests should match the case check
|
||||
// for httpTestItr in httpHandler
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "token-id not defined",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "secret name not formatted correctly",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "token-secret not defined",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "token expiration not formatted correctly",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "token expiration formatted correctly",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "token usage constant not true",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "token usage constant set to true",
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
bufErr.Reset()
|
||||
atomic.StoreUint32(&httpSentResponse, 0)
|
||||
fmt.Printf("Running HTTP test case (%d) %q\n", atomic.LoadUint32(&httpTestItr), tc.name)
|
||||
// should always return nil here if a valid list of secrets if fetched
|
||||
err := RunListTokens(&bufOut, &bufErr, client)
|
||||
if err != nil {
|
||||
t.Errorf("HTTP test case %d: Was unable to fetch a list of secrets", atomic.LoadUint32(&httpTestItr))
|
||||
}
|
||||
// wait for a response from the dummy HTTP server
|
||||
timeSpent := 0 * time.Millisecond
|
||||
timeToSleep := 50 * time.Millisecond
|
||||
timeMax := 2000 * time.Millisecond
|
||||
for {
|
||||
if atomic.LoadUint32(&httpSentResponse) == 1 {
|
||||
break
|
||||
}
|
||||
if timeSpent >= timeMax {
|
||||
t.Errorf("HTTP test case %d: The server did not respond within %d ms", atomic.LoadUint32(&httpTestItr), timeMax)
|
||||
}
|
||||
timeSpent += timeToSleep
|
||||
time.Sleep(timeToSleep)
|
||||
}
|
||||
// check if an error is written in the error buffer
|
||||
hasError := bufErr.Len() != 0
|
||||
if hasError != tc.expectedError {
|
||||
t.Errorf("HTTP test case %d: RunListTokens expected error: %v, saw: %v; %v", atomic.LoadUint32(&httpTestItr), tc.expectedError, hasError, bufErr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only one of these should run at a time in a goroutine
|
||||
func httpHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tokenID := []byte("07401b")
|
||||
tokenSecret := []byte("f395accd246ae52d")
|
||||
tokenExpire := []byte("2012-11-01T22:08:41+00:00")
|
||||
badValue := "bad-value"
|
||||
name := bootstrapapi.BootstrapTokenSecretPrefix + string(tokenID)
|
||||
tokenUsageKey := bootstrapapi.BootstrapTokenUsagePrefix + "test"
|
||||
|
||||
secret := v1.Secret{}
|
||||
secret.Type = bootstrapapi.SecretTypeBootstrapToken
|
||||
secret.TypeMeta = metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}
|
||||
secret.Data = map[string][]byte{}
|
||||
|
||||
switch atomic.LoadUint32(&httpTestItr) {
|
||||
case 0:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = []byte("")
|
||||
case 1:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: badValue}
|
||||
case 2:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.Data[bootstrapapi.BootstrapTokenSecretKey] = []byte("")
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: name}
|
||||
case 3:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.Data[bootstrapapi.BootstrapTokenSecretKey] = tokenSecret
|
||||
secret.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(badValue)
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: name}
|
||||
case 4:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.Data[bootstrapapi.BootstrapTokenSecretKey] = tokenSecret
|
||||
secret.Data[bootstrapapi.BootstrapTokenExpirationKey] = tokenExpire
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: name}
|
||||
case 5:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.Data[bootstrapapi.BootstrapTokenSecretKey] = tokenSecret
|
||||
secret.Data[bootstrapapi.BootstrapTokenExpirationKey] = tokenExpire
|
||||
secret.Data[tokenUsageKey] = []byte("false")
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: name}
|
||||
case 6:
|
||||
secret.Data[bootstrapapi.BootstrapTokenIDKey] = tokenID
|
||||
secret.Data[bootstrapapi.BootstrapTokenSecretKey] = tokenSecret
|
||||
secret.Data[bootstrapapi.BootstrapTokenExpirationKey] = tokenExpire
|
||||
secret.Data[tokenUsageKey] = []byte("true")
|
||||
secret.ObjectMeta = metav1.ObjectMeta{Name: name}
|
||||
}
|
||||
|
||||
secretList := v1.SecretList{}
|
||||
secretList.Items = []v1.Secret{secret}
|
||||
secretList.TypeMeta = metav1.TypeMeta{APIVersion: "v1", Kind: "SecretList"}
|
||||
|
||||
output, err := json.Marshal(secretList)
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(output))
|
||||
}
|
||||
atomic.AddUint32(&httpTestItr, 1)
|
||||
atomic.StoreUint32(&httpSentResponse, 1)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user