github.com/docker/compose-on-kubernetes@v0.5.0/cmd/api-server/cli/root.go (about) 1 package cli 2 3 import ( 4 "crypto/x509" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "math/rand" 9 "net" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/docker/compose-on-kubernetes/api/compose/v1alpha3" 17 "github.com/docker/compose-on-kubernetes/api/compose/v1beta1" 18 "github.com/docker/compose-on-kubernetes/api/compose/v1beta2" 19 "github.com/docker/compose-on-kubernetes/api/openapi" 20 "github.com/docker/compose-on-kubernetes/internal/apiserver" 21 "github.com/docker/compose-on-kubernetes/internal/internalversion" 22 "github.com/docker/compose-on-kubernetes/internal/keys" 23 "github.com/spf13/cobra" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 utilerrors "k8s.io/apimachinery/pkg/util/errors" 26 genericopenapi "k8s.io/apiserver/pkg/endpoints/openapi" 27 genericapiserver "k8s.io/apiserver/pkg/server" 28 "k8s.io/apiserver/pkg/server/healthz" 29 genericoptions "k8s.io/apiserver/pkg/server/options" 30 certutil "k8s.io/client-go/util/cert" 31 apiregistrationv1beta1types "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" 32 kubeaggreagatorv1beta1 "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1" 33 ) 34 35 const defaultEtcdPathPrefix = "/registry/docker.com/stacks" 36 37 type apiServerOptions struct { 38 RecommendedOptions *genericoptions.RecommendedOptions 39 serviceNamespace string 40 serviceName string 41 caBundleFile string 42 healthzCheckPort int 43 } 44 45 // NewCommandStartComposeServer provides a CLI handler for 'start master' command 46 func NewCommandStartComposeServer(stopCh <-chan struct{}) *cobra.Command { 47 codec := apiserver.Codecs.LegacyCodec(internalversion.StorageSchemeGroupVersion) 48 49 o := &apiServerOptions{ 50 RecommendedOptions: genericoptions.NewRecommendedOptions(defaultEtcdPathPrefix, codec, nil), 51 } 52 53 cmd := &cobra.Command{ 54 Short: "Launch a compose API server", 55 Long: "Launch a compose API server", 56 RunE: func(c *cobra.Command, args []string) error { 57 o.RecommendedOptions.ProcessInfo = genericoptions.NewProcessInfo("compose-on-kubernetes", o.serviceNamespace) 58 errors := []error{} 59 errors = append(errors, o.RecommendedOptions.Validate()...) 60 if err := utilerrors.NewAggregate(errors); err != nil { 61 return err 62 } 63 nextBackoff := time.Second 64 var err error 65 for attempt := 0; attempt < 8; attempt++ { 66 err = runComposeServer(o, stopCh) 67 // If the compose-api starts before the API server is listening then we can get a transient connection refused 68 // while looking up missing authentication information in the cluster (see #120). 69 if err != nil && strings.Contains(err.Error(), "connection refused") { 70 fmt.Fprintf(os.Stderr, "unable to start compose server: %s. Will retry in %s\n", err, nextBackoff) 71 time.Sleep(nextBackoff) 72 nextBackoff = nextBackoff * 2 73 continue 74 } 75 return err 76 } 77 fmt.Fprintf(os.Stderr, "giving up trying to start compose server") 78 return err 79 }, 80 } 81 82 flags := cmd.Flags() 83 o.RecommendedOptions.AddFlags(flags) 84 flags.StringVar(&o.serviceNamespace, "service-namespace", "", "defines the namespace of the service exposing the aggregated API") 85 flags.StringVar(&o.serviceName, "service-name", "", "defines the name of the service exposing the aggregated API") 86 flags.StringVar(&o.caBundleFile, "ca-bundle-file", "", "defines the path to the CA bundle file") 87 flags.IntVar(&o.healthzCheckPort, "healthz-check-port", 8080, "defines the port used by healthz check server (0 to disable it)") 88 return cmd 89 } 90 91 func generateCertificateIfRequired(o *apiServerOptions) error { 92 if o.RecommendedOptions.SecureServing.ServerCert.CertKey.CertFile != "" && o.RecommendedOptions.SecureServing.ServerCert.CertKey.KeyFile != "" { 93 return nil 94 } 95 // generate tls bundle 96 hostName, err := os.Hostname() 97 if err != nil { 98 return err 99 } 100 ca, err := keys.NewSelfSignedCA("compose-api-ca-"+strings.ToLower(hostName), nil) 101 if err != nil { 102 return err 103 } 104 key, err := keys.NewRSASigner() 105 if err != nil { 106 return err 107 } 108 109 cfg := certutil.Config{ 110 CommonName: fmt.Sprintf("%s.%s.svc", o.serviceName, o.serviceNamespace), 111 AltNames: certutil.AltNames{ 112 DNSNames: []string{"localhost", fmt.Sprintf("%s.%s.svc", o.serviceName, o.serviceNamespace)}, 113 IPs: []net.IP{loopbackIP}, 114 }, 115 Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 116 } 117 118 cert, err := ca.NewSignedCert(cfg, key.Public()) 119 if err != nil { 120 return err 121 } 122 123 dir, err := ioutil.TempDir("", "compose-tls-generated") 124 if err != nil { 125 return err 126 } 127 128 keyPath := filepath.Join(dir, "server.key") 129 certPath := filepath.Join(dir, "server.crt") 130 caPath := filepath.Join(dir, "ca.crt") 131 132 o.caBundleFile = caPath 133 o.RecommendedOptions.SecureServing.ServerCert = genericoptions.GeneratableKeyCert{ 134 CertKey: genericoptions.CertKey{ 135 CertFile: certPath, 136 KeyFile: keyPath, 137 }, 138 } 139 if err = ioutil.WriteFile(keyPath, key.PEM(), 0600); err != nil { 140 return err 141 } 142 if err = ioutil.WriteFile(certPath, keys.EncodeCertPEM(cert), 0600); err != nil { 143 return err 144 } 145 if err = ioutil.WriteFile(caPath, keys.EncodeCertPEM(ca.Cert()), 0600); err != nil { 146 return err 147 } 148 149 go func() { 150 // We don't want to actually reach tls timeout 151 // so before it is reached, we exit with non-zero result code in order to let Kubernetes 152 // restart the POD which will re-create a new TLS bundle 153 expiry := ca.Cert().NotAfter 154 if cert.NotAfter.Before(expiry) { 155 expiry = cert.NotAfter 156 } 157 timeout := getTimeout(time.Until(expiry)) 158 time.Sleep(timeout) 159 fmt.Fprint(os.Stderr, "certificate bundle needs regeneration") 160 os.Exit(1) 161 162 }() 163 return nil 164 } 165 166 func getTimeout(tlsExpireTimeout time.Duration) time.Duration { 167 rand.Seed(time.Now().UnixNano()) 168 factor := (rand.Float64() / 2) + 0.49 169 timeoutSeconds := factor * tlsExpireTimeout.Seconds() 170 return time.Duration(float64(time.Second) * timeoutSeconds) 171 } 172 173 var loopbackIP = net.ParseIP("127.0.0.1") 174 175 func registerAggregatedAPIs(aggregatorClient kubeaggreagatorv1beta1.APIServiceInterface, caBundle []byte, serviceNamespace, serviceName string, apiVersions ...string) error { 176 for ix, v := range apiVersions { 177 if err := registerAggregatedAPI(aggregatorClient, caBundle, serviceNamespace, serviceName, v, int32(ix+15)); err != nil { 178 return err 179 } 180 } 181 return nil 182 } 183 184 func registerAggregatedAPI(aggregatorClient kubeaggreagatorv1beta1.APIServiceInterface, 185 caBundle []byte, serviceNamespace, serviceName, apiVersion string, versionPriority int32) error { 186 apiService := &apiregistrationv1beta1types.APIService{ 187 ObjectMeta: metav1.ObjectMeta{ 188 Name: apiVersion + ".compose.docker.com", 189 Labels: map[string]string{ 190 "com.docker.fry": "compose.api", 191 }, 192 }, 193 Spec: apiregistrationv1beta1types.APIServiceSpec{ 194 CABundle: caBundle, 195 Group: v1beta1.GroupName, 196 GroupPriorityMinimum: 1000, 197 VersionPriority: versionPriority, 198 Version: apiVersion, 199 Service: &apiregistrationv1beta1types.ServiceReference{ 200 Namespace: serviceNamespace, 201 Name: serviceName, 202 }, 203 }, 204 } 205 206 existing, err := aggregatorClient.Get(apiVersion+".compose.docker.com", metav1.GetOptions{}) 207 if err == nil { 208 bundle, err := mergeCABundle(existing.Spec.CABundle, caBundle) 209 if err != nil { 210 return err 211 } 212 apiService.ObjectMeta.ResourceVersion = existing.ObjectMeta.ResourceVersion 213 apiService.Spec.CABundle = bundle 214 if _, err := aggregatorClient.Update(apiService); err != nil { 215 return err 216 } 217 } else { 218 if _, err := aggregatorClient.Create(apiService); err != nil { 219 return err 220 } 221 } 222 return nil 223 } 224 225 func runComposeServer(o *apiServerOptions, stopCh <-chan struct{}) error { 226 if err := generateCertificateIfRequired(o); err != nil { 227 return err 228 } 229 caBundle, err := ioutil.ReadFile(o.caBundleFile) 230 if err != nil { 231 return err 232 } 233 serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs) 234 if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil { 235 return err 236 } 237 serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions, genericopenapi.NewDefinitionNamer(apiserver.Scheme)) 238 serverConfig.OpenAPIConfig.Info.Title = "Kube-compose API" 239 serverConfig.OpenAPIConfig.Info.Version = "v1beta2" 240 241 config := &apiserver.Config{ 242 GenericConfig: serverConfig, 243 } 244 245 server, err := config.Complete().New(o.RecommendedOptions.CoreAPI.CoreAPIKubeconfigPath) 246 if err != nil { 247 return err 248 } 249 250 aggregatorClient, err := kubeaggreagatorv1beta1.NewForConfig(serverConfig.ClientConfig) 251 if err != nil { 252 return err 253 } 254 255 if o.healthzCheckPort > 0 { 256 err = server.GenericAPIServer.AddPostStartHook("start-compose-server-healthz-endpoint", func(context genericapiserver.PostStartHookContext) error { 257 m := http.NewServeMux() 258 healthz.InstallHandler(m, server.GenericAPIServer.HealthzChecks()...) 259 srv := &http.Server{ 260 Addr: fmt.Sprintf(":%d", o.healthzCheckPort), 261 Handler: m, 262 } 263 go srv.ListenAndServe() 264 go func() { 265 <-context.StopCh 266 srv.Close() 267 }() 268 return nil 269 }) 270 if err != nil { 271 return err 272 } 273 } 274 err = server.GenericAPIServer.AddPostStartHook("start-compose-server-informers", func(context genericapiserver.PostStartHookContext) error { 275 config.GenericConfig.SharedInformerFactory.Start(context.StopCh) 276 return registerAggregatedAPIs( 277 aggregatorClient.APIServices(), 278 caBundle, 279 o.serviceNamespace, 280 o.serviceName, 281 v1beta1.SchemeGroupVersion.Version, v1beta2.SchemeGroupVersion.Version, v1alpha3.SchemeGroupVersion.Version, 282 ) 283 }) 284 if err != nil { 285 return err 286 } 287 288 return server.GenericAPIServer.PrepareRun().Run(stopCh) 289 } 290 291 func mergeCABundle(existingPEM, newBundlePEM []byte) ([]byte, error) { 292 if len(existingPEM) == 0 { 293 return newBundlePEM, nil 294 } 295 existing, err := certutil.ParseCertsPEM(existingPEM) 296 if err != nil { 297 // existing bundle is unparsable. override it 298 return newBundlePEM, nil 299 } 300 newBundle, err := certutil.ParseCertsPEM(newBundlePEM) 301 if err != nil { 302 return nil, err 303 } 304 if len(newBundle) != 1 { 305 return nil, errors.New("bundle has an unexpected number of certificates in it") 306 } 307 commonName := newBundle[0].Subject.CommonName 308 var result []byte 309 for _, existingCert := range existing { 310 if existingCert.Subject.CommonName != commonName { 311 result = append(result, keys.EncodeCertPEM(existingCert)...) 312 } 313 } 314 result = append(result, newBundlePEM...) 315 return result, nil 316 }