diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index dcdad9b..3a318c9 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -65,37 +65,3 @@ jobs: ${{ env.REPO }}/vngcloud-controller-manager:${{ github.sha }} ${{ env.REPO }}/vngcloud-controller-manager:${{ env.VERSION }} target: vngcloud-controller-manager - - - name: Build and push Docker image - env: - TARGET: vngcloud-cm-webhook - uses: docker/build-push-action@v5 - with: - context: . - push: true - build-args: | - VERSION=${{ env.VERSION }} - platforms: linux/amd64 - outputs: type=registry - tags: | - ghcr.io/vngcloud/vngcloud-cm-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-cm-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-cm-webhook:${{ env.VERSION }} - target: vngcloud-cm-webhook - - - name: Build and push Docker image - env: - TARGET: vngcloud-ic-webhook - uses: docker/build-push-action@v5 - with: - context: . - push: true - build-args: | - VERSION=${{ env.VERSION }} - platforms: linux/amd64 - outputs: type=registry - tags: | - ghcr.io/vngcloud/vngcloud-ic-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-ic-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-ic-webhook:${{ env.VERSION }} - target: vngcloud-ic-webhook diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 713169f..0428e06 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -70,38 +70,6 @@ jobs: ${{ env.REPO }}/vngcloud-controller-manager:${{ github.ref_name }} target: vngcloud-controller-manager - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - build-args: | - VERSION=${{ github.ref_name }} - platforms: linux/amd64 - outputs: type=registry - tags: | - ghcr.io/vngcloud/vngcloud-cm-webhook:${{ github.sha }} - ghcr.io/vngcloud/vngcloud-cm-webhook:${{ github.ref_name }} - ${{ env.REPO }}/vngcloud-cm-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-cm-webhook:${{ github.ref_name }} - target: vngcloud-cm-webhook - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - build-args: | - VERSION=${{ github.ref_name }} - platforms: linux/amd64 - outputs: type=registry - tags: | - ghcr.io/vngcloud/vngcloud-ic-webhook:${{ github.sha }} - ghcr.io/vngcloud/vngcloud-ic-webhook:${{ github.ref_name }} - ${{ env.REPO }}/vngcloud-ic-webhook:${{ github.sha }} - ${{ env.REPO }}/vngcloud-ic-webhook:${{ github.ref_name }} - target: vngcloud-ic-webhook - create_release: runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index dce5bdf..d43ec48 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,6 @@ TestResults.xml /vngcloud-controller-manager /vngcloud-ingress-controller -/vngcloud-cm-webhook -/vngcloud-ic-webhook .go *.crt *.key diff --git a/Dockerfile b/Dockerfile index bf70d5d..69ce569 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,9 +61,7 @@ COPY pkg/ pkg/ RUN make build GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOPROXY=${GOPROXY} VERSION=${VERSION} # COPY vngcloud-controller-manager ./vngcloud-controller-manager # COPY vngcloud-ingress-controller ./vngcloud-ingress-controller -# COPY vngcloud-cm-webhook ./vngcloud-cm-webhook -# COPY vngcloud-ic-webhook ./vngcloud-ic-webhook -# RUN chmod +x ./vngcloud-controller-manager ./vngcloud-ingress-controller ./vngcloud-cm-webhook ./vngcloud-ic-webhook +# RUN chmod +x ./vngcloud-controller-manager ./vngcloud-ingress-controller ################################################################################ @@ -105,39 +103,3 @@ LABEL name="vngcloud-ingress-controller" \ help="none" CMD ["/bin/vngcloud-ingress-controller"] - -## -## vngcloud-cm-webhook -## -FROM --platform=${TARGETPLATFORM} ${DISTROLESS_IMAGE} as vngcloud-cm-webhook - -COPY --from=builder /build/vngcloud-cm-webhook /bin/vngcloud-cm-webhook -COPY --from=certs /etc/ssl/certs /etc/ssl/certs - -LABEL name="vngcloud-cm-webhook" \ - license="Apache Version 2.0" \ - maintainers="annd2@vng.com.vn" \ - description="vngcloud controller manager webhook" \ - distribution-scope="public" \ - summary="vngcloud controller manager webhook" \ - help="none" - -CMD ["/bin/vngcloud-cm-webhook"] - -## -## vngcloud-ic-webhook -## -FROM --platform=${TARGETPLATFORM} ${DISTROLESS_IMAGE} as vngcloud-ic-webhook - -COPY --from=builder /build/vngcloud-ic-webhook /bin/vngcloud-ic-webhook -COPY --from=certs /etc/ssl/certs /etc/ssl/certs - -LABEL name="vngcloud-ic-webhook" \ - license="Apache Version 2.0" \ - maintainers="annd2@vng.com.vn" \ - description="vngcloud ingress controller webhook" \ - distribution-scope="public" \ - summary="vngcloud ingress controller webhook" \ - help="none" - -CMD ["/bin/vngcloud-ic-webhook"] \ No newline at end of file diff --git a/Makefile b/Makefile index 31b8d96..68e1378 100644 --- a/Makefile +++ b/Makefile @@ -51,15 +51,11 @@ GOX_LDFLAGS := $(shell echo "$(LDFLAGS) -extldflags \"-static\"") REGISTRY ?= vcr.vngcloud.vn/81-vks-public IMAGE_OS ?= linux IMAGE_NAMES ?= vngcloud-controller-manager \ - vngcloud-ingress-controller \ - vngcloud-cm-webhook \ - vngcloud-ic-webhook + vngcloud-ingress-controller ARCH ?= amd64 ARCHS ?= amd64 BUILD_CMDS ?= vngcloud-controller-manager \ - vngcloud-ingress-controller \ - vngcloud-cm-webhook \ - vngcloud-ic-webhook + vngcloud-ingress-controller # CTI targets @@ -168,8 +164,6 @@ ifndef HAS_GOX endif CGO_ENABLED=0 gox -parallel=$(GOX_PARALLEL) -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) $(if $(TAGS),-tags '$(TAGS)',) -ldflags '$(GOX_LDFLAGS)' $(GIT_HOST)/$(BASE_DIR)/cmd/vngcloud-controller-manager/ CGO_ENABLED=0 gox -parallel=$(GOX_PARALLEL) -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) $(if $(TAGS),-tags '$(TAGS)',) -ldflags '$(GOX_LDFLAGS)' $(GIT_HOST)/$(BASE_DIR)/cmd/vngcloud-ingress-controller/ - CGO_ENABLED=0 gox -parallel=$(GOX_PARALLEL) -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) $(if $(TAGS),-tags '$(TAGS)',) -ldflags '$(GOX_LDFLAGS)' $(GIT_HOST)/$(BASE_DIR)/cmd/vngcloud-cm-webhook/ - CGO_ENABLED=0 gox -parallel=$(GOX_PARALLEL) -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) $(if $(TAGS),-tags '$(TAGS)',) -ldflags '$(GOX_LDFLAGS)' $(GIT_HOST)/$(BASE_DIR)/cmd/vngcloud-ic-webhook/ .PHONY: dist dist: build-cross diff --git a/cmd/vngcloud-cm-webhook/admission/admission.go b/cmd/vngcloud-cm-webhook/admission/admission.go deleted file mode 100644 index cfd08a9..0000000 --- a/cmd/vngcloud-cm-webhook/admission/admission.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package admission handles kubernetes admissions, -// it takes admission requests and returns admission reviews; -package admission - -import ( - "fmt" - "net/http" - - "github.com/sirupsen/logrus" - admissionv1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" -) - -// Admitter is a container for admission business -type Admitter struct { - Logger *logrus.Entry - Request *admissionv1.AdmissionRequest -} - -// ValidateReview takes an admission request and validates the item within -// it returns an admission review -func (a Admitter) ValidateReview() (*admissionv1.AdmissionReview, error) { - - v := NewValidator(a.Logger) - val, err := v.Validate(a.Request) - if err != nil { - e := fmt.Sprintf("could not validate: %v", err) - return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err - } - - if !val.Valid { - return reviewResponse(a.Request.UID, false, http.StatusForbidden, val.Reason), nil - } - - return reviewResponse(a.Request.UID, true, http.StatusAccepted, "valid"), nil -} - -// reviewResponse TODO: godoc -func reviewResponse(uid types.UID, allowed bool, httpCode int32, - reason string) *admissionv1.AdmissionReview { - return &admissionv1.AdmissionReview{ - TypeMeta: metav1.TypeMeta{ - Kind: "AdmissionReview", - APIVersion: "admission.k8s.io/v1", - }, - Response: &admissionv1.AdmissionResponse{ - UID: uid, - Allowed: allowed, - Result: &metav1.Status{ - Code: httpCode, - Message: reason, - }, - }, - } -} diff --git a/cmd/vngcloud-cm-webhook/admission/validation.go b/cmd/vngcloud-cm-webhook/admission/validation.go deleted file mode 100644 index a2aa86a..0000000 --- a/cmd/vngcloud-cm-webhook/admission/validation.go +++ /dev/null @@ -1,49 +0,0 @@ -package admission - -import ( - "github.com/sirupsen/logrus" - admissionv1 "k8s.io/api/admission/v1" -) - -// Validator is a container for mutation -type Validator struct { - Logger *logrus.Entry -} - -// NewValidator returns an initialised instance of Validator -func NewValidator(logger *logrus.Entry) *Validator { - return &Validator{Logger: logger} -} - -// itemValidators is an interface used to group functions mutating -type itemValidator interface { - Validate(*admissionv1.AdmissionRequest) (validation, error) - Name() string -} - -type validation struct { - Valid bool - Reason string -} - -// Validate returns true if a item is valid -func (v *Validator) Validate(admission *admissionv1.AdmissionRequest) (validation, error) { - // list of all validations to be applied - validations := []itemValidator{ - commonValidator{v.Logger}, - } - - // apply all validations - for _, v := range validations { - var err error - vp, err := v.Validate(admission) - if err != nil { - return validation{Valid: false, Reason: err.Error()}, err - } - if !vp.Valid { - return validation{Valid: false, Reason: vp.Reason}, err - } - } - - return validation{Valid: true, Reason: "valid"}, nil -} diff --git a/cmd/vngcloud-cm-webhook/admission/validator_common.go b/cmd/vngcloud-cm-webhook/admission/validator_common.go deleted file mode 100644 index 72d58a8..0000000 --- a/cmd/vngcloud-cm-webhook/admission/validator_common.go +++ /dev/null @@ -1,80 +0,0 @@ -package admission - -import ( - "encoding/json" - "fmt" - - "github.com/sirupsen/logrus" - admissionv1 "k8s.io/api/admission/v1" - corev1 "k8s.io/api/core/v1" -) - -// commonValidator is a container for validating the name -type commonValidator struct { - Logger logrus.FieldLogger -} - -// commonValidator implements the itemValidator interface -var _ itemValidator = (*commonValidator)(nil) - -// Name returns the name of commonValidator -func (n commonValidator) Name() string { - return "name_validator" -} - -// Validate inspects the name of a given item and returns validation. -// The returned validation is only valid if the item name does not contain some -// bad string. -func (n commonValidator) Validate(a *admissionv1.AdmissionRequest) (validation, error) { - valid := validation{Valid: true, Reason: "valid"} - if a.Kind.Kind != "Service" { - return validation{ - Valid: false, - Reason: fmt.Sprintf("expected kind Service, got %q", a.Kind.Kind), - }, nil - } - service, err := toService(a.Object.Raw) - if err != nil { - return validation{ - Valid: false, - Reason: fmt.Sprintf("could not parse service in admission review request: %v", err), - }, err - } - if service.Spec.Type != corev1.ServiceTypeLoadBalancer { - return valid, nil - } - - // Prevent changing the load balancer name - oldService := service - if a.Operation == admissionv1.Update { - oldService, err = toService(a.OldObject.Raw) - if err != nil { - return validation{ - Valid: false, - Reason: fmt.Sprintf("could not parse old service in admission review request: %v", err), - }, err - } - if oldService.Spec.Type != corev1.ServiceTypeLoadBalancer { - return valid, nil - } - - staticAnnotationKey := "vks.vngcloud.vn/load-balancer-name" - if oldService.Annotations[staticAnnotationKey] != service.Annotations[staticAnnotationKey] && oldService.Annotations[staticAnnotationKey] != "" { - return validation{ - Valid: false, - Reason: fmt.Sprintf("annotation cannot change: %q", staticAnnotationKey), - }, nil - } - } - - return valid, nil -} - -// Service extracts a service from an admission request -func toService(a []byte) (*corev1.Service, error) { - p := corev1.Service{} - if err := json.Unmarshal(a, &p); err != nil { - return nil, err - } - return &p, nil -} diff --git a/cmd/vngcloud-cm-webhook/main.go b/cmd/vngcloud-cm-webhook/main.go deleted file mode 100644 index 4c2f904..0000000 --- a/cmd/vngcloud-cm-webhook/main.go +++ /dev/null @@ -1,354 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/json" - "encoding/pem" - "flag" - "fmt" - "log" - "math/big" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/sirupsen/logrus" - "github.com/vngcloud/cloud-provider-vngcloud/cmd/vngcloud-cm-webhook/admission" - "github.com/vngcloud/cloud-provider-vngcloud/pkg/utils" - admissionv1 "k8s.io/api/admission/v1" - // admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -const ( - TLS_PATH = "/tmp/vngcloud-cm-webhook" - MUTATE_PATH = "/mutate" - VALIDATE_PATH = "/validate" - HEALTH_PATH = "/health" - PORT_HTTP = 8080 -) - -func PointerOf[T any](t T) *T { - return &t -} - -var ( - commonName string - port_https int - - isCreateSecret bool - isCreateWehookConfig bool - namespace string - - debug bool -) - -func init() { - flag.StringVar(&commonName, "common-name", "", "common name for the server certificate") - flag.IntVar(&port_https, "port-https", 8443, "port for https server") - - flag.BoolVar(&isCreateSecret, "create-secret", true, "create secret") - flag.BoolVar(&isCreateWehookConfig, "create-webhook-config", true, "create webhook config") - flag.StringVar(&namespace, "namespace", "default", "namespace as defined by .metadata.namespace") - - flag.BoolVar(&debug, "debug", true, "enable debug") - - flag.Parse() - - // Generate CA certificate and key - caCert, caKey, err := generateCACertificate() - if err != nil { - log.Fatalf("Error generating CA certificate: %s", err) - } - - // Generate server certificate and key - serverCert, serverKey, err := generateServerCertificate(caCert, caKey, commonName+"."+namespace+".svc") - if err != nil { - log.Fatalf("Error generating server certificate: %s", err) - } - - if isCreateSecret { - // Build the Kubernetes client - clientset := buildClientset() - - // Check if exist - curSecret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), commonName, metav1.GetOptions{}) - if err != nil { - log.Fatalf("Secret %s not found in namespace %s", commonName, namespace) - } - - // Update secret data - curSecret.Data = map[string][]byte{ - "tls.crt": pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}), - "tls.key": pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(serverKey)}), - "ca.crt": pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}), - } - if _, err = clientset.CoreV1().Secrets(namespace).Update(context.TODO(), curSecret, metav1.UpdateOptions{}); err != nil { - log.Fatalf("Error updating server TLS secret: %s", err) - } - log.Printf("Secret %s/%s updated successfully\n", namespace, commonName) - } - - // Check if the folder exists - if _, err := os.Stat(TLS_PATH); os.IsNotExist(err) { - err := os.MkdirAll(TLS_PATH, os.ModePerm) - if err != nil { - fmt.Println("Error creating folder:", err) - return - } - fmt.Println("Folder created successfully.") - } else { - fmt.Println("Folder already exists.") - } - writeToFile(TLS_PATH+"/ca.crt", "CERTIFICATE", caCert.Raw) - writeToFile(TLS_PATH+"/ca.key", "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(caKey)) - writeToFile(TLS_PATH+"/tls.crt", "CERTIFICATE", serverCert.Raw) - writeToFile(TLS_PATH+"/tls.key", "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverKey)) - - if isCreateWehookConfig { - clientset := buildClientset() - - // Check if exist - curConfig, err := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), commonName, metav1.GetOptions{}) - if err != nil { - log.Fatalf("ValidatingWebhookConfiguration: %s not found", commonName) - } - - // Update webhook config - curConfig.Webhooks[0].ClientConfig.CABundle = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}) - - if _, err := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), curConfig, metav1.UpdateOptions{}); err != nil { - log.Fatalf("Error updating ValidatingWebhookConfiguration: %s", err) - } - log.Printf("ValidatingWebhookConfiguration %s updated successfully\n", commonName) - } -} - -func generateCACertificate() (*x509.Certificate, *rsa.PrivateKey, error) { - // Generate private key - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // Create CA certificate - caCertTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2024), - Subject: pkix.Name{ - Organization: []string{}, - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - IsCA: true, - } - - caCertBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, &caKey.PublicKey, caKey) - if err != nil { - return nil, nil, err - } - - caCert, err := x509.ParseCertificate(caCertBytes) - if err != nil { - return nil, nil, err - } - - return caCert, caKey, nil -} - -func generateServerCertificate(caCert *x509.Certificate, caKey *rsa.PrivateKey, commonName string) (*x509.Certificate, *rsa.PrivateKey, error) { - // Generate private key - serverKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // Create server certificate - serverCertTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2025), - Subject: pkix.Name{ - Organization: []string{}, - CommonName: commonName, - }, - DNSNames: []string{commonName}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(1 * 365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - - serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverCertTemplate, caCert, &serverKey.PublicKey, caKey) - if err != nil { - return nil, nil, err - } - - serverCert, err := x509.ParseCertificate(serverCertBytes) - if err != nil { - return nil, nil, err - } - - return serverCert, serverKey, nil -} - -func writeToFile(filename, blockType string, data []byte) { - file, err := os.Create(filename) - if err != nil { - log.Fatalf("Error creating file %s: %s", filename, err) - } - defer file.Close() - - err = pem.Encode(file, &pem.Block{Type: blockType, Bytes: data}) - if err != nil { - log.Fatalf("Error writing to file %s: %s", filename, err) - } - log.Printf("Written %s\n", filename) -} - -// Build the Kubernetes client should mount config in /etc/kubernetes/... -func buildClientset() *kubernetes.Clientset { - // initialize k8s client - clientset, err := utils.CreateApiserverClient("", "") - if err != nil { - log.Fatalf("Error building kubernetes clientset: %s", err) - } - return clientset -} - -func main() { - setLogger() - - // handle our core application - http.HandleFunc(VALIDATE_PATH, ServeValidate) - http.HandleFunc(HEALTH_PATH, ServeHealth) - - // Channel to listen for OS signals - signalChan := make(chan os.Signal, 1) - - // Goroutine to handle OS signals - go func() { - // start the server - // listens to clear text http on port ... unless TLS env var is set to "true" - if os.Getenv("TLS") == "true" { - cert := TLS_PATH + "/tls.crt" - key := TLS_PATH + "/tls.key" - logrus.Printf("Listening on port %d ...", port_https) - logrus.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", port_https), cert, key, nil)) - } else { - logrus.Printf("Listening on port %d ...", PORT_HTTP) - logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT_HTTP), nil)) - } - }() - - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) - sig := <-signalChan - fmt.Printf("Received signal shutting down: %s\n", sig) - - _, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - // delete resources here - close(signalChan) - log.Printf("Completed") -} - -// setLogger sets the logger using env vars, it defaults to text logs on -// debug level unless otherwise specified -func setLogger() { - logrus.SetLevel(logrus.DebugLevel) - - lev := os.Getenv("LOG_LEVEL") - if lev != "" { - llev, err := logrus.ParseLevel(lev) - if err != nil { - logrus.Fatalf("cannot set LOG_LEVEL to %q", lev) - } - logrus.SetLevel(llev) - } - - if os.Getenv("LOG_JSON") == "true" { - logrus.SetFormatter(&logrus.JSONFormatter{}) - } -} - -// ServeValidate validates an admission request and then writes an admission -// review to `w` -func ServeValidate(w http.ResponseWriter, r *http.Request) { - logger := logrus.WithField("uri", r.RequestURI) - logger.Debug("------------------------\nreceived validation request") - - in, err := parseRequest(*r) - if err != nil { - logger.Error(err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - adm := admission.Admitter{ - Logger: logger, - Request: in.Request, - } - - out, err := adm.ValidateReview() - if err != nil { - e := fmt.Sprintf("could not generate admission response: %v", err) - logger.Error(e) - http.Error(w, e, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - jout, err := json.Marshal(out) - if err != nil { - e := fmt.Sprintf("could not parse admission response: %v", err) - logger.Error(e) - http.Error(w, e, http.StatusInternalServerError) - return - } - - logger.Debug("sending response") - logger.Debugf("%s", jout) - fmt.Fprintf(w, "%s", jout) -} - -// ServeHealth returns 200 when things are good -func ServeHealth(w http.ResponseWriter, r *http.Request) { - logrus.WithField("uri", r.RequestURI).Debug("healthy") - fmt.Fprint(w, "OK") -} - -// parseRequest extracts an AdmissionReview from an http.Request if possible -func parseRequest(r http.Request) (*admissionv1.AdmissionReview, error) { - if r.Header.Get("Content-Type") != "application/json" { - return nil, fmt.Errorf("Content-Type: %q should be %q", - r.Header.Get("Content-Type"), "application/json") - } - - bodybuf := new(bytes.Buffer) - bodybuf.ReadFrom(r.Body) - body := bodybuf.Bytes() - - if len(body) == 0 { - return nil, fmt.Errorf("admission request body is empty") - } - - var a admissionv1.AdmissionReview - - if err := json.Unmarshal(body, &a); err != nil { - return nil, fmt.Errorf("could not parse admission review request: %v", err) - } - - if a.Request == nil { - return nil, fmt.Errorf("admission review can't be used: Request field is nil") - } - - return &a, nil -} diff --git a/cmd/vngcloud-ic-webhook/admission/admission.go b/cmd/vngcloud-ic-webhook/admission/admission.go deleted file mode 100644 index cfd08a9..0000000 --- a/cmd/vngcloud-ic-webhook/admission/admission.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package admission handles kubernetes admissions, -// it takes admission requests and returns admission reviews; -package admission - -import ( - "fmt" - "net/http" - - "github.com/sirupsen/logrus" - admissionv1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" -) - -// Admitter is a container for admission business -type Admitter struct { - Logger *logrus.Entry - Request *admissionv1.AdmissionRequest -} - -// ValidateReview takes an admission request and validates the item within -// it returns an admission review -func (a Admitter) ValidateReview() (*admissionv1.AdmissionReview, error) { - - v := NewValidator(a.Logger) - val, err := v.Validate(a.Request) - if err != nil { - e := fmt.Sprintf("could not validate: %v", err) - return reviewResponse(a.Request.UID, false, http.StatusBadRequest, e), err - } - - if !val.Valid { - return reviewResponse(a.Request.UID, false, http.StatusForbidden, val.Reason), nil - } - - return reviewResponse(a.Request.UID, true, http.StatusAccepted, "valid"), nil -} - -// reviewResponse TODO: godoc -func reviewResponse(uid types.UID, allowed bool, httpCode int32, - reason string) *admissionv1.AdmissionReview { - return &admissionv1.AdmissionReview{ - TypeMeta: metav1.TypeMeta{ - Kind: "AdmissionReview", - APIVersion: "admission.k8s.io/v1", - }, - Response: &admissionv1.AdmissionResponse{ - UID: uid, - Allowed: allowed, - Result: &metav1.Status{ - Code: httpCode, - Message: reason, - }, - }, - } -} diff --git a/cmd/vngcloud-ic-webhook/admission/validation.go b/cmd/vngcloud-ic-webhook/admission/validation.go deleted file mode 100644 index a2aa86a..0000000 --- a/cmd/vngcloud-ic-webhook/admission/validation.go +++ /dev/null @@ -1,49 +0,0 @@ -package admission - -import ( - "github.com/sirupsen/logrus" - admissionv1 "k8s.io/api/admission/v1" -) - -// Validator is a container for mutation -type Validator struct { - Logger *logrus.Entry -} - -// NewValidator returns an initialised instance of Validator -func NewValidator(logger *logrus.Entry) *Validator { - return &Validator{Logger: logger} -} - -// itemValidators is an interface used to group functions mutating -type itemValidator interface { - Validate(*admissionv1.AdmissionRequest) (validation, error) - Name() string -} - -type validation struct { - Valid bool - Reason string -} - -// Validate returns true if a item is valid -func (v *Validator) Validate(admission *admissionv1.AdmissionRequest) (validation, error) { - // list of all validations to be applied - validations := []itemValidator{ - commonValidator{v.Logger}, - } - - // apply all validations - for _, v := range validations { - var err error - vp, err := v.Validate(admission) - if err != nil { - return validation{Valid: false, Reason: err.Error()}, err - } - if !vp.Valid { - return validation{Valid: false, Reason: vp.Reason}, err - } - } - - return validation{Valid: true, Reason: "valid"}, nil -} diff --git a/cmd/vngcloud-ic-webhook/admission/validator_common.go b/cmd/vngcloud-ic-webhook/admission/validator_common.go deleted file mode 100644 index 34f82d2..0000000 --- a/cmd/vngcloud-ic-webhook/admission/validator_common.go +++ /dev/null @@ -1,81 +0,0 @@ -package admission - -import ( - "encoding/json" - "fmt" - - "github.com/sirupsen/logrus" - "github.com/vngcloud/cloud-provider-vngcloud/pkg/ingress/controller" - admissionv1 "k8s.io/api/admission/v1" - nwv1 "k8s.io/api/networking/v1" -) - -// commonValidator is a container for validating the name -type commonValidator struct { - Logger logrus.FieldLogger -} - -// commonValidator implements the itemValidator interface -var _ itemValidator = (*commonValidator)(nil) - -// Name returns the name of commonValidator -func (n commonValidator) Name() string { - return "name_validator" -} - -// Validate inspects the name of a given item and returns validation. -// The returned validation is only valid if the item name does not contain some -// bad string. -func (n commonValidator) Validate(a *admissionv1.AdmissionRequest) (validation, error) { - valid := validation{Valid: true, Reason: "valid"} - if a.Kind.Kind != "Ingress" { - return validation{ - Valid: false, - Reason: fmt.Sprintf("expected kind Ingress, got %q", a.Kind.Kind), - }, nil - } - ingress, err := toIngress(a.Object.Raw) - if err != nil { - return validation{ - Valid: false, - Reason: fmt.Sprintf("could not parse ingress in admission review request: %v", err), - }, err - } - if !controller.IsValid(ingress) { - return valid, nil - } - - // Prevent changing the load balancer name - oldIngress := ingress - if a.Operation == admissionv1.Update { - oldIngress, err = toIngress(a.OldObject.Raw) - if err != nil { - return validation{ - Valid: false, - Reason: fmt.Sprintf("could not parse old ingress in admission review request: %v", err), - }, err - } - if !controller.IsValid(oldIngress) { - return valid, nil - } - - staticAnnotationKey := "vks.vngcloud.vn/load-balancer-name" - if oldIngress.Annotations[staticAnnotationKey] != ingress.Annotations[staticAnnotationKey] && oldIngress.Annotations[staticAnnotationKey] != "" { - return validation{ - Valid: false, - Reason: fmt.Sprintf("annotation cannot change: %q", staticAnnotationKey), - }, nil - } - } - - return valid, nil -} - -// extracts from an admission request -func toIngress(a []byte) (*nwv1.Ingress, error) { - p := nwv1.Ingress{} - if err := json.Unmarshal(a, &p); err != nil { - return nil, err - } - return &p, nil -} diff --git a/cmd/vngcloud-ic-webhook/main.go b/cmd/vngcloud-ic-webhook/main.go deleted file mode 100644 index 9cce5f4..0000000 --- a/cmd/vngcloud-ic-webhook/main.go +++ /dev/null @@ -1,354 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/json" - "encoding/pem" - "flag" - "fmt" - "log" - "math/big" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/sirupsen/logrus" - "github.com/vngcloud/cloud-provider-vngcloud/cmd/vngcloud-ic-webhook/admission" - "github.com/vngcloud/cloud-provider-vngcloud/pkg/utils" - admissionv1 "k8s.io/api/admission/v1" - // admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -const ( - TLS_PATH = "/tmp/vngcloud-ic-webhook" - MUTATE_PATH = "/mutate" - VALIDATE_PATH = "/validate" - HEALTH_PATH = "/health" - PORT_HTTP = 8080 -) - -func PointerOf[T any](t T) *T { - return &t -} - -var ( - commonName string - port_https int - - isCreateSecret bool - isCreateWehookConfig bool - namespace string - - debug bool -) - -func init() { - flag.StringVar(&commonName, "common-name", "", "common name for the server certificate") - flag.IntVar(&port_https, "port-https", 8443, "port for https server") - - flag.BoolVar(&isCreateSecret, "create-secret", true, "create secret") - flag.BoolVar(&isCreateWehookConfig, "create-webhook-config", true, "create webhook config") - flag.StringVar(&namespace, "namespace", "default", "namespace as defined by .metadata.namespace") - - flag.BoolVar(&debug, "debug", true, "enable debug") - - flag.Parse() - - // Generate CA certificate and key - caCert, caKey, err := generateCACertificate() - if err != nil { - log.Fatalf("Error generating CA certificate: %s", err) - } - - // Generate server certificate and key - serverCert, serverKey, err := generateServerCertificate(caCert, caKey, commonName+"."+namespace+".svc") - if err != nil { - log.Fatalf("Error generating server certificate: %s", err) - } - - if isCreateSecret { - // Build the Kubernetes client - clientset := buildClientset() - - // Check if exist - curSecret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), commonName, metav1.GetOptions{}) - if err != nil { - log.Fatalf("Secret %s not found in namespace %s", commonName, namespace) - } - - // Update secret data - curSecret.Data = map[string][]byte{ - "tls.crt": pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}), - "tls.key": pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(serverKey)}), - "ca.crt": pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}), - } - if _, err = clientset.CoreV1().Secrets(namespace).Update(context.TODO(), curSecret, metav1.UpdateOptions{}); err != nil { - log.Fatalf("Error updating server TLS secret: %s", err) - } - log.Printf("Secret %s/%s updated successfully\n", namespace, commonName) - } - - // Check if the folder exists - if _, err := os.Stat(TLS_PATH); os.IsNotExist(err) { - err := os.MkdirAll(TLS_PATH, os.ModePerm) - if err != nil { - fmt.Println("Error creating folder:", err) - return - } - fmt.Println("Folder created successfully.") - } else { - fmt.Println("Folder already exists.") - } - writeToFile(TLS_PATH+"/ca.crt", "CERTIFICATE", caCert.Raw) - writeToFile(TLS_PATH+"/ca.key", "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(caKey)) - writeToFile(TLS_PATH+"/tls.crt", "CERTIFICATE", serverCert.Raw) - writeToFile(TLS_PATH+"/tls.key", "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverKey)) - - if isCreateWehookConfig { - clientset := buildClientset() - - // Check if exist - curConfig, err := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(), commonName, metav1.GetOptions{}) - if err != nil { - log.Fatalf("ValidatingWebhookConfiguration: %s not found", commonName) - } - - // Update webhook config - curConfig.Webhooks[0].ClientConfig.CABundle = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}) - - if _, err := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.TODO(), curConfig, metav1.UpdateOptions{}); err != nil { - log.Fatalf("Error updating ValidatingWebhookConfiguration: %s", err) - } - log.Printf("ValidatingWebhookConfiguration %s updated successfully\n", commonName) - } -} - -func generateCACertificate() (*x509.Certificate, *rsa.PrivateKey, error) { - // Generate private key - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // Create CA certificate - caCertTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2024), - Subject: pkix.Name{ - Organization: []string{}, - CommonName: commonName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - IsCA: true, - } - - caCertBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, &caKey.PublicKey, caKey) - if err != nil { - return nil, nil, err - } - - caCert, err := x509.ParseCertificate(caCertBytes) - if err != nil { - return nil, nil, err - } - - return caCert, caKey, nil -} - -func generateServerCertificate(caCert *x509.Certificate, caKey *rsa.PrivateKey, commonName string) (*x509.Certificate, *rsa.PrivateKey, error) { - // Generate private key - serverKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // Create server certificate - serverCertTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2025), - Subject: pkix.Name{ - Organization: []string{}, - CommonName: commonName, - }, - DNSNames: []string{commonName}, - NotBefore: time.Now(), - NotAfter: time.Now().Add(1 * 365 * 24 * time.Hour), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - } - - serverCertBytes, err := x509.CreateCertificate(rand.Reader, serverCertTemplate, caCert, &serverKey.PublicKey, caKey) - if err != nil { - return nil, nil, err - } - - serverCert, err := x509.ParseCertificate(serverCertBytes) - if err != nil { - return nil, nil, err - } - - return serverCert, serverKey, nil -} - -func writeToFile(filename, blockType string, data []byte) { - file, err := os.Create(filename) - if err != nil { - log.Fatalf("Error creating file %s: %s", filename, err) - } - defer file.Close() - - err = pem.Encode(file, &pem.Block{Type: blockType, Bytes: data}) - if err != nil { - log.Fatalf("Error writing to file %s: %s", filename, err) - } - log.Printf("Written %s\n", filename) -} - -// Build the Kubernetes client should mount config in /etc/kubernetes/... -func buildClientset() *kubernetes.Clientset { - // initialize k8s client - clientset, err := utils.CreateApiserverClient("", "") - if err != nil { - log.Fatalf("Error building kubernetes clientset: %s", err) - } - return clientset -} - -func main() { - setLogger() - - // handle our core application - http.HandleFunc(VALIDATE_PATH, ServeValidate) - http.HandleFunc(HEALTH_PATH, ServeHealth) - - // Channel to listen for OS signals - signalChan := make(chan os.Signal, 1) - - // Goroutine to handle OS signals - go func() { - // start the server - // listens to clear text http on port ... unless TLS env var is set to "true" - if os.Getenv("TLS") == "true" { - cert := TLS_PATH + "/tls.crt" - key := TLS_PATH + "/tls.key" - logrus.Printf("Listening on port %d ...", port_https) - logrus.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", port_https), cert, key, nil)) - } else { - logrus.Printf("Listening on port %d ...", PORT_HTTP) - logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", PORT_HTTP), nil)) - } - }() - - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) - sig := <-signalChan - fmt.Printf("Received signal shutting down: %s\n", sig) - - _, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - // delete resources here - close(signalChan) - log.Printf("Completed") -} - -// setLogger sets the logger using env vars, it defaults to text logs on -// debug level unless otherwise specified -func setLogger() { - logrus.SetLevel(logrus.DebugLevel) - - lev := os.Getenv("LOG_LEVEL") - if lev != "" { - llev, err := logrus.ParseLevel(lev) - if err != nil { - logrus.Fatalf("cannot set LOG_LEVEL to %q", lev) - } - logrus.SetLevel(llev) - } - - if os.Getenv("LOG_JSON") == "true" { - logrus.SetFormatter(&logrus.JSONFormatter{}) - } -} - -// ServeValidate validates an admission request and then writes an admission -// review to `w` -func ServeValidate(w http.ResponseWriter, r *http.Request) { - logger := logrus.WithField("uri", r.RequestURI) - logger.Debug("------------------------\nreceived validation request") - - in, err := parseRequest(*r) - if err != nil { - logger.Error(err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - adm := admission.Admitter{ - Logger: logger, - Request: in.Request, - } - - out, err := adm.ValidateReview() - if err != nil { - e := fmt.Sprintf("could not generate admission response: %v", err) - logger.Error(e) - http.Error(w, e, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - jout, err := json.Marshal(out) - if err != nil { - e := fmt.Sprintf("could not parse admission response: %v", err) - logger.Error(e) - http.Error(w, e, http.StatusInternalServerError) - return - } - - logger.Debug("sending response") - logger.Debugf("%s", jout) - fmt.Fprintf(w, "%s", jout) -} - -// ServeHealth returns 200 when things are good -func ServeHealth(w http.ResponseWriter, r *http.Request) { - logrus.WithField("uri", r.RequestURI).Debug("healthy") - fmt.Fprint(w, "OK") -} - -// parseRequest extracts an AdmissionReview from an http.Request if possible -func parseRequest(r http.Request) (*admissionv1.AdmissionReview, error) { - if r.Header.Get("Content-Type") != "application/json" { - return nil, fmt.Errorf("Content-Type: %q should be %q", - r.Header.Get("Content-Type"), "application/json") - } - - bodybuf := new(bytes.Buffer) - bodybuf.ReadFrom(r.Body) - body := bodybuf.Bytes() - - if len(body) == 0 { - return nil, fmt.Errorf("admission request body is empty") - } - - var a admissionv1.AdmissionReview - - if err := json.Unmarshal(body, &a); err != nil { - return nil, fmt.Errorf("could not parse admission review request: %v", err) - } - - if a.Request == nil { - return nil, fmt.Errorf("admission review can't be used: Request field is nil") - } - - return &a, nil -}