k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kube-controller-manager/app/testing/testserver.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testing 18 19 import ( 20 "context" 21 "fmt" 22 "net" 23 "os" 24 "time" 25 26 "github.com/spf13/pflag" 27 28 "k8s.io/apimachinery/pkg/util/wait" 29 "k8s.io/client-go/kubernetes" 30 restclient "k8s.io/client-go/rest" 31 logsapi "k8s.io/component-base/logs/api/v1" 32 "k8s.io/klog/v2" 33 "k8s.io/kubernetes/cmd/kube-controller-manager/app" 34 kubecontrollerconfig "k8s.io/kubernetes/cmd/kube-controller-manager/app/config" 35 "k8s.io/kubernetes/cmd/kube-controller-manager/app/options" 36 ) 37 38 func init() { 39 // If instantiated more than once or together with other servers, the 40 // servers would try to modify the global logging state. This must get 41 // ignored during testing. 42 logsapi.ReapplyHandling = logsapi.ReapplyHandlingIgnoreUnchanged 43 } 44 45 // TearDownFunc is to be called to tear down a test server. 46 type TearDownFunc func() 47 48 // TestServer return values supplied by kube-test-ApiServer 49 type TestServer struct { 50 LoopbackClientConfig *restclient.Config // Rest client config using the magic token 51 Options *options.KubeControllerManagerOptions 52 Config *kubecontrollerconfig.Config 53 TearDownFn TearDownFunc // TearDown function 54 TmpDir string // Temp Dir used, by the apiserver 55 } 56 57 // StartTestServer starts a kube-controller-manager. A rest client config and a tear-down func, 58 // and location of the tmpdir are returned. 59 // 60 // Note: we return a tear-down func instead of a stop channel because the later will leak temporary 61 // files that because Golang testing's call to os.Exit will not give a stop channel go routine 62 // enough time to remove temporary files. 63 func StartTestServer(ctx context.Context, customFlags []string) (result TestServer, err error) { 64 logger := klog.FromContext(ctx) 65 ctx, cancel := context.WithCancel(ctx) 66 var errCh chan error 67 tearDown := func() { 68 cancel() 69 70 // If the kube-controller-manager was started, let's wait for 71 // it to shutdown cleanly. 72 if errCh != nil { 73 err, ok := <-errCh 74 if ok && err != nil { 75 logger.Error(err, "Failed to shutdown test server cleanly") 76 } 77 } 78 if len(result.TmpDir) != 0 { 79 os.RemoveAll(result.TmpDir) 80 } 81 } 82 defer func() { 83 if result.TearDownFn == nil { 84 tearDown() 85 } 86 }() 87 88 result.TmpDir, err = os.MkdirTemp("", "kube-controller-manager") 89 if err != nil { 90 return result, fmt.Errorf("failed to create temp dir: %v", err) 91 } 92 93 fs := pflag.NewFlagSet("test", pflag.PanicOnError) 94 95 s, err := options.NewKubeControllerManagerOptions() 96 if err != nil { 97 return TestServer{}, err 98 } 99 all, disabled, aliases := app.KnownControllers(), app.ControllersDisabledByDefault(), app.ControllerAliases() 100 namedFlagSets := s.Flags(all, disabled, aliases) 101 for _, f := range namedFlagSets.FlagSets { 102 fs.AddFlagSet(f) 103 } 104 fs.Parse(customFlags) 105 106 if s.SecureServing.BindPort != 0 { 107 s.SecureServing.Listener, s.SecureServing.BindPort, err = createListenerOnFreePort() 108 if err != nil { 109 return result, fmt.Errorf("failed to create listener: %v", err) 110 } 111 s.SecureServing.ServerCert.CertDirectory = result.TmpDir 112 113 logger.Info("kube-controller-manager will listen securely", "port", s.SecureServing.BindPort) 114 } 115 116 config, err := s.Config(all, disabled, aliases) 117 if err != nil { 118 return result, fmt.Errorf("failed to create config from options: %v", err) 119 } 120 121 errCh = make(chan error) 122 go func(ctx context.Context) { 123 defer close(errCh) 124 125 if err := app.Run(ctx, config.Complete()); err != nil { 126 errCh <- err 127 } 128 }(ctx) 129 130 logger.Info("Waiting for /healthz to be ok...") 131 client, err := kubernetes.NewForConfig(config.LoopbackClientConfig) 132 if err != nil { 133 return result, fmt.Errorf("failed to create a client: %v", err) 134 } 135 err = wait.PollWithContext(ctx, 100*time.Millisecond, 30*time.Second, func(ctx context.Context) (bool, error) { 136 select { 137 case <-ctx.Done(): 138 return false, ctx.Err() 139 case err := <-errCh: 140 return false, err 141 default: 142 } 143 144 result := client.CoreV1().RESTClient().Get().AbsPath("/healthz").Do(context.TODO()) 145 status := 0 146 result.StatusCode(&status) 147 if status == 200 { 148 return true, nil 149 } 150 return false, nil 151 }) 152 if err != nil { 153 return result, fmt.Errorf("failed to wait for /healthz to return ok: %v", err) 154 } 155 156 // from here the caller must call tearDown 157 result.LoopbackClientConfig = config.LoopbackClientConfig 158 result.Options = s 159 result.Config = config 160 result.TearDownFn = tearDown 161 162 return result, nil 163 } 164 165 // StartTestServerOrDie calls StartTestServer t.Fatal if it does not succeed. 166 func StartTestServerOrDie(ctx context.Context, flags []string) *TestServer { 167 result, err := StartTestServer(ctx, flags) 168 if err == nil { 169 return &result 170 } 171 172 panic(fmt.Errorf("failed to launch server: %v", err)) 173 } 174 175 func createListenerOnFreePort() (net.Listener, int, error) { 176 ln, err := net.Listen("tcp", ":0") 177 if err != nil { 178 return nil, 0, err 179 } 180 181 // get port 182 tcpAddr, ok := ln.Addr().(*net.TCPAddr) 183 if !ok { 184 ln.Close() 185 return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String()) 186 } 187 188 return ln, tcpAddr.Port, nil 189 }