2020-07-08 17:06:44 +00:00
/ *
Copyright 2020 VMware , Inc .
SPDX - License - Identifier : Apache - 2.0
* /
// Package app is the command line entry point for placeholder-name.
package app
import (
2020-07-13 19:30:16 +00:00
"context"
"crypto/tls"
2020-07-23 15:05:21 +00:00
"crypto/x509"
2020-07-13 19:30:16 +00:00
"crypto/x509/pkix"
2020-07-23 15:05:21 +00:00
"encoding/pem"
2020-07-13 19:30:16 +00:00
"fmt"
2020-07-08 17:06:44 +00:00
"io"
2020-07-13 19:30:16 +00:00
"time"
2020-07-08 17:06:44 +00:00
2020-07-27 12:55:33 +00:00
"github.com/spf13/cobra"
2020-07-16 19:24:30 +00:00
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-07-23 15:05:21 +00:00
"k8s.io/apimachinery/pkg/runtime"
2020-07-27 12:55:33 +00:00
"k8s.io/apimachinery/pkg/util/intstr"
2020-07-23 15:05:21 +00:00
genericapiserver "k8s.io/apiserver/pkg/server"
2020-07-27 12:55:33 +00:00
"k8s.io/apiserver/pkg/server/dynamiccertificates"
2020-07-23 15:05:21 +00:00
genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
2020-07-16 19:24:30 +00:00
"k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
2020-07-27 12:55:33 +00:00
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
2020-07-16 19:24:30 +00:00
aggregationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
2020-07-19 03:57:00 +00:00
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
2020-07-27 12:55:33 +00:00
"github.com/suzerain-io/placeholder-name/internal/autoregistration"
"github.com/suzerain-io/placeholder-name/internal/certauthority"
"github.com/suzerain-io/placeholder-name/internal/downward"
2020-07-23 15:05:21 +00:00
"github.com/suzerain-io/placeholder-name/pkg/apiserver"
2020-07-14 15:50:14 +00:00
"github.com/suzerain-io/placeholder-name/pkg/config"
2020-07-08 17:06:44 +00:00
)
// App is an object that represents the placeholder-name application.
type App struct {
cmd * cobra . Command
2020-07-16 19:24:30 +00:00
// CLI flags
2020-07-24 20:41:51 +00:00
configPath string
downwardAPIPath string
clusterSigningCertFilePath string
clusterSigningKeyFilePath string
2020-07-16 19:24:30 +00:00
2020-07-23 15:05:21 +00:00
recommendedOptions * genericoptions . RecommendedOptions
2020-07-08 17:06:44 +00:00
}
2020-07-23 15:05:21 +00:00
// This is ignored for now because we turn off etcd storage below, but this is
// the right prefix in case we turn it back on.
const defaultEtcdPathPrefix = "/registry/" + placeholderv1alpha1 . GroupName
2020-07-08 17:06:44 +00:00
// New constructs a new App with command line args, stdout and stderr.
2020-07-23 15:05:21 +00:00
func New ( ctx context . Context , args [ ] string , stdout , stderr io . Writer ) * App {
2020-07-08 17:06:44 +00:00
a := & App {
2020-07-23 15:05:21 +00:00
recommendedOptions : genericoptions . NewRecommendedOptions (
defaultEtcdPathPrefix ,
apiserver . Codecs . LegacyCodec ( placeholderv1alpha1 . SchemeGroupVersion ) ,
// TODO we should check to see if all the other default settings are acceptable for us
) ,
2020-07-08 17:06:44 +00:00
}
2020-07-23 15:05:21 +00:00
a . recommendedOptions . Etcd = nil // turn off etcd storage because we don't need it yet
2020-07-08 17:06:44 +00:00
cmd := & cobra . Command {
Use : ` placeholder-name ` ,
Long : ` placeholder - name provides a generic API for mapping an external
credential from somewhere to an internal credential to be used for
authenticating to the Kubernetes API . ` ,
2020-07-13 19:30:16 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-07-16 19:24:30 +00:00
// Load the Kubernetes client configuration (kubeconfig),
2020-07-19 03:52:18 +00:00
kubeConfig , err := restclient . InClusterConfig ( )
2020-07-16 19:24:30 +00:00
if err != nil {
return fmt . Errorf ( "could not load in-cluster configuration: %w" , err )
}
2020-07-19 03:52:18 +00:00
// explicitly use protobuf when talking to built-in kube APIs
protoKubeConfig := createProtoKubeConfig ( kubeConfig )
2020-07-16 19:24:30 +00:00
// Connect to the core Kubernetes API.
2020-07-19 03:52:18 +00:00
k8s , err := kubernetes . NewForConfig ( protoKubeConfig )
2020-07-16 19:24:30 +00:00
if err != nil {
return fmt . Errorf ( "could not initialize Kubernetes client: %w" , err )
}
// Connect to the Kubernetes aggregation API.
2020-07-19 03:52:18 +00:00
aggregation , err := aggregationv1client . NewForConfig ( protoKubeConfig )
2020-07-16 19:24:30 +00:00
if err != nil {
return fmt . Errorf ( "could not initialize Kubernetes client: %w" , err )
}
2020-07-23 15:05:21 +00:00
return a . run ( ctx , k8s . CoreV1 ( ) , aggregation )
2020-07-08 17:06:44 +00:00
} ,
Args : cobra . NoArgs ,
}
cmd . SetArgs ( args )
cmd . SetOut ( stdout )
cmd . SetErr ( stderr )
cmd . Flags ( ) . StringVarP (
2020-07-16 19:24:30 +00:00
& a . configPath ,
2020-07-08 17:06:44 +00:00
"config" ,
"c" ,
"placeholder-name.yaml" ,
"path to configuration file" ,
)
2020-07-16 19:24:30 +00:00
cmd . Flags ( ) . StringVar (
& a . downwardAPIPath ,
"downward-api-path" ,
"/etc/podinfo" ,
"path to Downward API volume mount" ,
)
2020-07-24 20:41:51 +00:00
cmd . Flags ( ) . StringVar (
& a . clusterSigningCertFilePath ,
"cluster-signing-cert-file" ,
"" ,
"path to cluster signing certificate" ,
)
cmd . Flags ( ) . StringVar (
& a . clusterSigningKeyFilePath ,
"cluster-signing-key-file" ,
"" ,
"path to cluster signing private key" ,
)
2020-07-08 17:06:44 +00:00
a . cmd = cmd
return a
}
func ( a * App ) Run ( ) error {
return a . cmd . Execute ( )
}
2020-07-13 19:30:16 +00:00
2020-07-23 15:05:21 +00:00
func ( a * App ) run (
ctx context . Context ,
k8s corev1client . CoreV1Interface ,
aggregation aggregationv1client . Interface ,
) error {
2020-07-16 19:24:30 +00:00
cfg , err := config . FromPath ( a . configPath )
2020-07-14 15:50:14 +00:00
if err != nil {
return fmt . Errorf ( "could not load config: %w" , err )
}
2020-07-27 12:55:33 +00:00
// Load the Kubernetes cluster signing CA.
clientCA , err := certauthority . Load ( a . clusterSigningCertFilePath , a . clusterSigningKeyFilePath )
if err != nil {
return fmt . Errorf ( "could not load cluster signing CA: %w" , err )
}
2020-07-23 15:05:21 +00:00
webhookTokenAuthenticator , err := config . NewWebhook ( cfg . WebhookConfig )
2020-07-14 15:50:14 +00:00
if err != nil {
2020-07-27 12:55:33 +00:00
return fmt . Errorf ( "could not create webhook client: %w" , err )
2020-07-14 15:50:14 +00:00
}
2020-07-16 19:24:30 +00:00
podinfo , err := downward . Load ( a . downwardAPIPath )
if err != nil {
return fmt . Errorf ( "could not read pod metadata: %w" , err )
}
2020-07-23 15:05:21 +00:00
// TODO use the postStart hook to generate certs?
2020-07-27 12:55:33 +00:00
apiCA , err := certauthority . New ( pkix . Name { CommonName : "Placeholder CA" } )
2020-07-13 19:30:16 +00:00
if err != nil {
return fmt . Errorf ( "could not initialize CA: %w" , err )
}
2020-07-23 15:05:21 +00:00
const serviceName = "placeholder-name-api"
2020-07-27 12:55:33 +00:00
cert , err := apiCA . Issue (
2020-07-23 15:05:21 +00:00
pkix . Name { CommonName : serviceName + "." + podinfo . Namespace + ".svc" } ,
[ ] string { } ,
2020-07-13 19:30:16 +00:00
24 * 365 * time . Hour ,
)
if err != nil {
return fmt . Errorf ( "could not issue serving certificate: %w" , err )
}
2020-07-16 19:24:30 +00:00
// Dynamically register our v1alpha1 API service.
service := corev1 . Service {
2020-07-23 15:05:21 +00:00
ObjectMeta : metav1 . ObjectMeta { Name : serviceName } ,
2020-07-16 19:24:30 +00:00
Spec : corev1 . ServiceSpec {
Ports : [ ] corev1 . ServicePort {
{
Protocol : corev1 . ProtocolTCP ,
Port : 443 ,
2020-07-23 15:05:21 +00:00
TargetPort : intstr . IntOrString { IntVal : 443 } ,
2020-07-16 19:24:30 +00:00
} ,
} ,
Selector : podinfo . Labels ,
Type : corev1 . ServiceTypeClusterIP ,
} ,
}
apiService := apiregistrationv1 . APIService {
ObjectMeta : metav1 . ObjectMeta {
2020-07-19 03:57:00 +00:00
Name : placeholderv1alpha1 . SchemeGroupVersion . Version + "." + placeholderv1alpha1 . GroupName ,
2020-07-16 19:24:30 +00:00
} ,
Spec : apiregistrationv1 . APIServiceSpec {
2020-07-19 03:57:00 +00:00
Group : placeholderv1alpha1 . GroupName ,
Version : placeholderv1alpha1 . SchemeGroupVersion . Version ,
2020-07-27 12:55:33 +00:00
CABundle : apiCA . Bundle ( ) ,
2020-07-23 15:05:21 +00:00
GroupPriorityMinimum : 2500 , // TODO what is the right value? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#apiservicespec-v1beta1-apiregistration-k8s-io
VersionPriority : 10 , // TODO what is the right value? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#apiservicespec-v1beta1-apiregistration-k8s-io
2020-07-16 19:24:30 +00:00
} ,
}
if err := autoregistration . Setup ( ctx , autoregistration . SetupOptions {
CoreV1 : k8s ,
AggregationV1 : aggregation ,
Namespace : podinfo . Namespace ,
ServiceTemplate : service ,
APIServiceTemplate : apiService ,
} ) ; err != nil {
return fmt . Errorf ( "could not register API service: %w" , err )
}
2020-07-27 12:55:33 +00:00
apiServerConfig , err := a . ConfigServer ( cert , webhookTokenAuthenticator , clientCA )
2020-07-23 15:05:21 +00:00
if err != nil {
2020-07-13 19:30:16 +00:00
return err
}
2020-07-23 15:05:21 +00:00
server , err := apiServerConfig . Complete ( ) . New ( )
if err != nil {
return fmt . Errorf ( "could not issue serving certificate: %w" , err )
}
2020-07-14 15:50:14 +00:00
2020-07-23 15:05:21 +00:00
return server . GenericAPIServer . PrepareRun ( ) . Run ( ctx . Done ( ) )
2020-07-13 19:30:16 +00:00
}
2020-07-27 12:55:33 +00:00
func ( a * App ) ConfigServer ( cert * tls . Certificate , webhookTokenAuthenticator * webhook . WebhookTokenAuthenticator , ca * certauthority . CA ) ( * apiserver . Config , error ) {
2020-07-23 15:05:21 +00:00
provider , err := createStaticCertKeyProvider ( cert )
if err != nil {
return nil , fmt . Errorf ( "could not create static cert key provider: %w" , err )
}
a . recommendedOptions . SecureServing . ServerCert . GeneratedCert = provider
2020-07-13 19:30:16 +00:00
2020-07-23 15:05:21 +00:00
serverConfig := genericapiserver . NewRecommendedConfig ( apiserver . Codecs )
if err := a . recommendedOptions . ApplyTo ( serverConfig ) ; err != nil {
return nil , err
}
2020-07-13 19:30:16 +00:00
2020-07-23 15:05:21 +00:00
apiServerConfig := & apiserver . Config {
GenericConfig : serverConfig ,
ExtraConfig : apiserver . ExtraConfig {
Webhook : webhookTokenAuthenticator ,
2020-07-27 12:55:33 +00:00
Issuer : ca ,
2020-07-23 15:05:21 +00:00
} ,
}
return apiServerConfig , nil
2020-07-13 19:30:16 +00:00
}
2020-07-19 03:52:18 +00:00
// createProtoKubeConfig returns a copy of the input config with the ContentConfig set to use protobuf.
// do not use this config to communicate with any CRD based APIs.
func createProtoKubeConfig ( kubeConfig * restclient . Config ) * restclient . Config {
protoKubeConfig := restclient . CopyConfig ( kubeConfig )
const protoThenJSON = runtime . ContentTypeProtobuf + "," + runtime . ContentTypeJSON
protoKubeConfig . AcceptContentTypes = protoThenJSON
protoKubeConfig . ContentType = runtime . ContentTypeProtobuf
return protoKubeConfig
}
2020-07-23 15:05:21 +00:00
func createStaticCertKeyProvider ( cert * tls . Certificate ) ( dynamiccertificates . CertKeyContentProvider , error ) {
privateKeyDER , err := x509 . MarshalPKCS8PrivateKey ( cert . PrivateKey )
if err != nil {
return nil , fmt . Errorf ( "error marshalling private key: %w" , err )
}
privateKeyPEM := pem . EncodeToMemory ( & pem . Block {
Type : "PRIVATE KEY" ,
Headers : nil ,
Bytes : privateKeyDER ,
} )
certChainPEM := make ( [ ] byte , 0 )
for _ , certFromChain := range cert . Certificate {
certPEMBytes := pem . EncodeToMemory ( & pem . Block {
Type : "CERTIFICATE" ,
Headers : nil ,
Bytes : certFromChain ,
} )
certChainPEM = append ( certChainPEM , certPEMBytes ... )
}
return dynamiccertificates . NewStaticCertKeyContent ( "some-name???" , certChainPEM , privateKeyPEM )
}