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  }