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 }