sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/webhook-server/main.go (about)

     1  /*
     2  Copyright 2022 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 main
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"encoding/json"
    23  	"flag"
    24  	"fmt"
    25  	"net/http"
    26  	"os"
    27  	"path/filepath"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/sirupsen/logrus"
    32  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/prow/cmd/webhook-server/secretmanager"
    34  	"sigs.k8s.io/prow/pkg/config"
    35  	"sigs.k8s.io/prow/pkg/flagutil"
    36  	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
    37  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    38  	"sigs.k8s.io/prow/pkg/interrupts"
    39  	"sigs.k8s.io/prow/pkg/logrusutil"
    40  	"sigs.k8s.io/prow/pkg/pjutil"
    41  	"sigs.k8s.io/prow/pkg/plank"
    42  )
    43  
    44  const (
    45  	configAlreadyExistsError = "already exists"
    46  	certFile                 = "certFile.pem"
    47  	privKeyFile              = "privKeyFile.pem"
    48  	caBundleFile             = "caBundle.pem"
    49  )
    50  
    51  type ClientInterface interface {
    52  	CreateSecret(ctx context.Context, secretID string) error
    53  	AddSecretVersion(ctx context.Context, secretName string, payload []byte) error
    54  	GetSecretValue(ctx context.Context, secretName string, versionName string) ([]byte, bool, error)
    55  }
    56  
    57  type options struct {
    58  	kubernetes     prowflagutil.KubernetesOptions
    59  	secretID       string
    60  	projectId      string
    61  	expiryInYears  int
    62  	dnsNames       prowflagutil.Strings
    63  	fileSystemPath string
    64  	config         configflagutil.ConfigOptions
    65  	storage        prowflagutil.StorageClientOptions
    66  	time           int
    67  	dryRun         bool
    68  }
    69  
    70  type clientOptions struct {
    71  	secretID      string
    72  	expiryInYears int
    73  	dnsNames      prowflagutil.Strings
    74  }
    75  
    76  type webhookAgent struct {
    77  	storage  prowflagutil.StorageClientOptions
    78  	statuses map[string]plank.ClusterStatus
    79  	mu       sync.Mutex
    80  	plank    config.Plank
    81  }
    82  
    83  func (o *options) DefaultAndValidate() error {
    84  	optionGroup := []flagutil.OptionGroup{&o.kubernetes, &o.config, &o.storage}
    85  	if err := optionGroup[0].Validate(o.dryRun); err != nil {
    86  		return err
    87  	}
    88  	if o.expiryInYears < 0 {
    89  		return fmt.Errorf("invalid expiry years")
    90  	}
    91  	if o.projectId == "" && o.fileSystemPath == "" {
    92  		return fmt.Errorf("both projectid and filesystem path cannot be specified")
    93  	}
    94  	if o.projectId != "" && o.fileSystemPath != "" {
    95  		return fmt.Errorf("either projectid or filesystem path must be specified")
    96  	}
    97  	if o.projectId != "" && o.secretID == "" {
    98  		return fmt.Errorf("secretID must be specified if choosing to use a GCP project")
    99  	}
   100  	if o.dnsNames.StringSet().Len() == 0 {
   101  		o.dnsNames.Add(prowjobAdmissionServiceName + ".default.svc")
   102  	}
   103  	return nil
   104  }
   105  
   106  func gatherOptions(fs *flag.FlagSet, args ...string) options {
   107  	var o options
   108  	fs.StringVar(&o.projectId, "project-id", "", "Project ID for storing GCP Secrets")
   109  	fs.StringVar(&o.fileSystemPath, "filesys-path", "./prowjob-webhook-ca-cert", "File system path for storing ca-cert secrets")
   110  	fs.StringVar(&o.secretID, "secret-id", "", "GCP Project secret name")
   111  	fs.IntVar(&o.expiryInYears, "expiry-years", 30, "CA certificate expiry in years")
   112  	fs.BoolVar(&o.dryRun, "dry-run", true, "Whether to mutate any real-world state")
   113  	fs.IntVar(&o.time, "time", 1, "duration in minutes to fetch build clusters")
   114  	fs.Var(&o.dnsNames, "dns", "DNS Names CA-Cert config")
   115  	optionGroups := []flagutil.OptionGroup{&o.kubernetes, &o.config}
   116  	for _, optionGroup := range optionGroups {
   117  		optionGroup.AddFlags(fs)
   118  	}
   119  	fs.Parse(args)
   120  	return o
   121  }
   122  
   123  func main() {
   124  	logrusutil.ComponentInit()
   125  	logrus.SetLevel(logrus.DebugLevel)
   126  	o := gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...)
   127  	if err := o.DefaultAndValidate(); err != nil {
   128  		logrus.WithError(err).Fatal("Invalid options")
   129  	}
   130  	defer interrupts.WaitForGracefulShutdown()
   131  	health := pjutil.NewHealth()
   132  	kubeCfg, err := o.kubernetes.InfrastructureClusterConfig(o.dryRun)
   133  	if err != nil {
   134  		logrus.WithError(err).Fatal("Error getting kubeconfig")
   135  	}
   136  	var certFile string
   137  	var privKeyFile string
   138  	ctx := context.Background()
   139  	cl, err := ctrlruntimeclient.New(kubeCfg, ctrlruntimeclient.Options{})
   140  	if err != nil {
   141  		logrus.WithError(err).Fatal("Could not create writer client")
   142  	}
   143  	var client ClientInterface
   144  	statuses := make(map[string]plank.ClusterStatus)
   145  	clientoptions := &clientOptions{
   146  		secretID:      o.secretID,
   147  		dnsNames:      o.dnsNames,
   148  		expiryInYears: o.expiryInYears,
   149  	}
   150  	if o.projectId != "" {
   151  		secretManagerClient, err := secretmanager.NewClient(o.projectId, false)
   152  		if err != nil {
   153  			logrus.WithError(err).Fatal("Unable to create secretmanager client", err)
   154  		}
   155  		client = newGCPClient(secretManagerClient, o.secretID)
   156  		if err != nil {
   157  			logrus.WithError(err).Fatal("Unable to create secret manager client")
   158  		}
   159  	}
   160  	if o.fileSystemPath != "" {
   161  		absPath, err := filepath.Abs(o.fileSystemPath)
   162  		if err != nil {
   163  			logrus.WithError(err).Fatal("Unable to generate absolute file path")
   164  		}
   165  		client = NewLocalFSClient(absPath, o.expiryInYears, o.dnsNames.Strings())
   166  	}
   167  	certFile, privKeyFile, err = handleSecrets(client, ctx, *clientoptions, cl)
   168  	if err != nil {
   169  		logrus.WithError(err).Fatal("could not get necessary ca secret files", err)
   170  	}
   171  	configAgent, err := o.config.ConfigAgent()
   172  	if err != nil {
   173  		logrus.WithError(err).Fatal("could not create config agent")
   174  	}
   175  	cfg := configAgent.Config()
   176  	wa := &webhookAgent{
   177  		storage:  o.storage,
   178  		statuses: statuses,
   179  		plank:    cfg.Plank,
   180  	}
   181  	interrupts.Run(func(ctx context.Context) {
   182  		wa.fetchClusters(time.Duration(o.time*int(time.Minute)), ctx, &wa.statuses, configAgent)
   183  	})
   184  
   185  	mux := http.NewServeMux()
   186  	mux.HandleFunc(validatePath, wa.serveValidate)
   187  	mux.HandleFunc(mutatePath, wa.serveMutate)
   188  	s := http.Server{
   189  		Addr: ":8008",
   190  		TLSConfig: &tls.Config{
   191  			ClientAuth: tls.NoClientCert,
   192  		},
   193  		Handler: mux,
   194  	}
   195  	logrus.Info("Listening on port 8008...")
   196  	interrupts.ListenAndServeTLS(&s, certFile, privKeyFile, 5*time.Second)
   197  	health.ServeReady(func() bool {
   198  		return true
   199  	})
   200  }
   201  
   202  // get or creates the necessary ca secret files and returns the ca-cert file name, priv-key file name and tempDir name
   203  // for use by the http listenAndServe
   204  func handleSecrets(client ClientInterface, ctx context.Context, clientoptions clientOptions, cl ctrlruntimeclient.Client) (string, string, error) {
   205  	var cert string
   206  	var privKey string
   207  	var caPem string
   208  	secretsMap := make(map[string]string)
   209  	data, exist, err := client.GetSecretValue(ctx, clientoptions.secretID, "latest")
   210  	if err != nil {
   211  		return "", "", err
   212  	}
   213  	if !exist {
   214  		logrus.WithError(err).Info("Secret does not exist, now creating")
   215  		cert, privKey, caPem, err = createSecret(client, ctx, clientoptions)
   216  		if err != nil {
   217  			return "", "", fmt.Errorf("unable to create ca certificate %v", err)
   218  		}
   219  	} else {
   220  		err = json.Unmarshal(data, &secretsMap)
   221  		if err != nil {
   222  			return "", "", fmt.Errorf("error marshalling CA cert secret data: %v", err)
   223  		}
   224  		cert = secretsMap[certFile]
   225  		privKey = secretsMap[privKeyFile]
   226  		if err := isCertValid(cert); err != nil {
   227  			logrus.WithError(err).Info("Certificate is not valid, will replace.")
   228  			cert, privKey, caPem, err = updateSecret(client, ctx, clientoptions)
   229  			if err != nil {
   230  				return "", "", fmt.Errorf("unable to update secret %v", err)
   231  			}
   232  		}
   233  	}
   234  	if err = reconcileWebhooks(ctx, caPem, cl); err != nil {
   235  		return "", "", err
   236  	}
   237  	tempDir, err := os.MkdirTemp("", "cert")
   238  	if err != nil {
   239  		return "", "", fmt.Errorf("unable to create temp directory %v", err)
   240  	}
   241  	certFile := filepath.Join(tempDir, certFile)
   242  	if err := os.WriteFile(certFile, []byte(cert), 0666); err != nil {
   243  		return "", "", fmt.Errorf("could not write contents of cert file %v", err)
   244  	}
   245  	privKeyFile := filepath.Join(tempDir, privKeyFile)
   246  	if err := os.WriteFile(privKeyFile, []byte(privKey), 0666); err != nil {
   247  		return "", "", fmt.Errorf("could not write contents of privKey file %v", err)
   248  	}
   249  	return certFile, privKeyFile, nil
   250  }