From d47a5ba2365e383abeeb034f69b460f45639e18c Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Tue, 13 Feb 2024 14:52:20 +0100 Subject: [PATCH] feat: init project --- .changelog/.gitkeep | 0 .github/workflows/new-release.yaml | 234 +++++++++++++++++++++++++ .github/workflows/pr-close.yaml | 34 ++++ .goreleaser.yaml | 85 +++++++++ CHANGELOG.md | 5 + Dockerfile | 9 + README.md | 14 +- cmd/cert.go | 90 ++++++++++ cmd/main.go | 99 +++++++++++ cmd/webhook.go | 261 ++++++++++++++++++++++++++++ cmd/webhookconfig.go | 108 ++++++++++++ go.mod | 48 +++++ go.sum | 156 +++++++++++++++++ manifests/cluster-role-binding.yaml | 14 ++ manifests/cluster-role.yaml | 10 ++ manifests/configmap.yaml | 11 ++ manifests/deployment.yaml | 37 ++++ manifests/env-config.yaml | 7 + manifests/manifests.yaml | 95 ++++++++++ manifests/service-account.yaml | 6 + manifests/service.yaml | 12 ++ prestop.sh | 21 +++ 22 files changed, 1355 insertions(+), 1 deletion(-) create mode 100644 .changelog/.gitkeep create mode 100644 .github/workflows/new-release.yaml create mode 100644 .github/workflows/pr-close.yaml create mode 100644 .goreleaser.yaml create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 cmd/cert.go create mode 100644 cmd/main.go create mode 100644 cmd/webhook.go create mode 100644 cmd/webhookconfig.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 manifests/cluster-role-binding.yaml create mode 100644 manifests/cluster-role.yaml create mode 100644 manifests/configmap.yaml create mode 100644 manifests/deployment.yaml create mode 100644 manifests/env-config.yaml create mode 100644 manifests/manifests.yaml create mode 100644 manifests/service-account.yaml create mode 100644 manifests/service.yaml create mode 100644 prestop.sh diff --git a/.changelog/.gitkeep b/.changelog/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/new-release.yaml b/.github/workflows/new-release.yaml new file mode 100644 index 0000000..11142e0 --- /dev/null +++ b/.github/workflows/new-release.yaml @@ -0,0 +1,234 @@ +name: TagRelease + +on: + workflow_dispatch: + inputs: + tagType: + description: 'TagType' + required: true + default: 'pre-tag' + type: choice + options: + - normal + - pre-tag + tag: + description: 'Tag version number (Eg: v0.1.0)' + required: true + type: string + message: + description: 'Tag message' + required: true + +permissions: + contents: write + packages: write + +jobs: + # * Step 0: Pre-Check + pre-check: + runs-on: ubuntu-latest + outputs: + TAG_NAME: ${{ steps.set-tag.outputs.TAG_NAME }} + PRERELEASE: ${{ steps.set-tag.outputs.PRERELEASE }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.CHANGELOG_PAT }} + # ! TODO add check if tag format is valid (semver) + - name: Check if tag is valid + run : | + # Check if the tag start with 'v', if not, add it + if [[ ! ${{ github.event.inputs.tag }} =~ ^v.* ]]; then + echo "Error tag format is invalid. The format is vx.x.x" >> "$GITHUB_OUTPUT" + exit 1 + fi + - name: Construct Tag for Pre-Release + id: set-tag + run: | + # Construct the tag name + if [ "${{ github.event.inputs.tagType }}" == "pre-tag" ]; then + echo "TAG_NAME=$(echo ${{ github.event.inputs.tag }}-alpha$(date +%Y%m%d%H%M))" >> "$GITHUB_OUTPUT" + echo "PRERELEASE=true" >> "$GITHUB_OUTPUT" + else + echo "TAG_NAME=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" + fi + # * Step 1: Check if everything is ok + tag-already-exist: + needs: [pre-check] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.CHANGELOG_PAT }} + - name: Check if tag not already exists + run: | + if git rev-parse ${{ needs.pre-check.outputs.TAG_NAME }} >/dev/null 2>&1; then + echo "Tag ${{ needs.pre-check.outputs.TAG_NAME }} already exists" >> "$GITHUB_OUTPUT" + exit 1 + fi + golangci-lint: + needs: [pre-check] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.CHANGELOG_PAT }} + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - run: go mod download + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + # TODO add tests + + # * Step 2: Create a new tag + tag: + needs: [golangci-lint, pre-check, tag-already-exist] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.CHANGELOG_PAT }} + - uses: rickstaa/action-create-tag@v1 + id: "tag_create" + with: + tag: ${{ needs.pre-check.outputs.TAG_NAME }} + tag_exists_error: true + message: ${{ github.event.inputs.message }} + + release-notes: + runs-on: ubuntu-latest + needs: [tag, pre-check] + steps: + - uses: actions/checkout@v4 # v3.5.3 + with: + fetch-depth: 0 + ref: ${{ needs.pre-check.outputs.TAG_NAME }} + - name: Generate Release Notes + run: | + echo "" > release-notes.txt + + if [ ${{ needs.pre-check.outputs.PRERELEASE }} == "true" ]; then + echo "## :construction: Prerelease" >> release-notes.txt + export PREV_TAG=$(git tag --list 'v*' --sort=-version:refname | grep -E "v[0-9]+\.[0-9]+\.[0-9]+$" | head -n 1) + export PREV_VERSION=${PREV_TAG//v} + else + export PREV_TAG=$(git tag --list 'v*' --sort=-version:refname | grep -E "v[0-9]+\.[0-9]+\.[0-9]+$" | head -n 2 | tail -n 1) + export PREV_VERSION=${PREV_TAG//v} + fi + + sed -n -e "1{/# /d;}" -e "2{/^$/d;}" -e "/# $PREV_VERSION/q;p" CHANGELOG.md >> release-notes.txt + - uses: actions/upload-artifact@v4 + with: + name: release-notes + path: release-notes.txt + retention-days: 1 + release-app: + runs-on: ubuntu-latest + needs: [release-notes, golangci-lint, pre-check] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.pre-check.outputs.TAG_NAME }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: release-notes-download + name: Release Notes Download + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 + with: + name: release-notes + path: /tmp + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + if: success() + with: + distribution: goreleaser + version: latest + args: release --clean -f .goreleaser.yaml --release-notes=${{ steps.release-notes-download.outputs.download-path }}/release-notes.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + trigger-doc-update: + needs: [release-app, pre-check] + runs-on: ubuntu-latest + if: needs.pre-check.outputs.PRERELEASE == 'false' + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3 + with: + event-type: update-doc + highest-version-tag: + needs: [pre-check,release-app] + runs-on: ubuntu-latest + if: needs.pre-check.outputs.PRERELEASE == 'false' + outputs: + tag: ${{ steps.highest-version-tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 # v3.5.3 + with: + ref: ${{ needs.pre-check.outputs.TAG_NAME }} + fetch-depth: 0 + - name: Output highest version tag + id: highest-version-tag + run: | + HIGHEST=$(git tag | sort -V | tail -1) + echo "tag=$HIGHEST" >> "$GITHUB_OUTPUT" + changelog-newversion: + needs: [release-app, highest-version-tag, pre-check] + # write new changelog header only if release tag is the $HIGHEST i.e. exists on main + # and not a backport release branch (e.g. release/3.x). This results in + # manually updating the CHANGELOG header if releasing from the non-default branch. + # TODO: find a more deterministic way to determine release branch from tag commit + if: github.ref_name == needs.highest-version-tag.outputs.tag && needs.pre-check.outputs.PRERELEASE == 'false' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 # v3.5.3 + with: + token: ${{ secrets.CHANGELOG_PAT }} + fetch-depth: 0 + ref: main + - name: Update Changelog Header + run: | + CHANGELOG_FILE_NAME="CHANGELOG.md" + PREVIOUS_RELEASE_TAG=${{ github.ref_name }} + + # Add Release Date + RELEASE_DATE=`date +%B' '%e', '%Y` + sed -i -e "1 s/.*Unreleased.*/## ${PREVIOUS_RELEASE_TAG#v} ($RELEASE_DATE)/" $CHANGELOG_FILE_NAME + + # Prepend next release line + echo Previous release is: $PREVIOUS_RELEASE_TAG + + NEW_RELEASE_LINE=$(echo $PREVIOUS_RELEASE_TAG | awk -F. '{ + $1 = substr($1,2) + $2 += 1 + printf("%s.%01d.0\n\n", $1, $2); + }') + + echo New minor version is: v$NEW_RELEASE_LINE + + echo -e "## $NEW_RELEASE_LINE (Unreleased)\n$(cat $CHANGELOG_FILE_NAME)" > $CHANGELOG_FILE_NAME + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: Update CHANGELOG.md after ${{ github.ref_name }}" + commit_options: '--no-verify --signoff' + file_pattern: CHANGELOG.md + commit_user_name: Changelog[bot] + commit_user_email: changelog-bot@orange.com + commit_author: Changelog Bot + diff --git a/.github/workflows/pr-close.yaml b/.github/workflows/pr-close.yaml new file mode 100644 index 0000000..f0ed0c4 --- /dev/null +++ b/.github/workflows/pr-close.yaml @@ -0,0 +1,34 @@ +name: PR Close +on: + pull_request: + types: [closed] + workflow_dispatch: + +concurrency: + group: ${{ github.ref }}-${{ github.head_ref }}-changelog + cancel-in-progress: true + +jobs: + GenerateChangelog: + if: github.event.pull_request.merged || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.CHANGELOG_PAT }} + fetch-depth: 0 + submodules: true + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + # * CHANGELOG + - run: go install github.com/hashicorp/go-changelog/cmd/changelog-build@latest + - run: bash .github/changelog/generate-changelog.sh + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: Update CHANGELOG.md" + commit_options: '--no-verify --signoff' + file_pattern: CHANGELOG.md + commit_user_name: Changelog[bot] + commit_user_email: changelog-bot@orange.com + commit_author: Changelog Bot diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..0ead62b --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,85 @@ +project_name: k8-env + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +release: + prerelease: auto + + +snapshot: + name_template: "{{ .Tag }}" + +checksum: + name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' + +builds: + - id: "k8-env" + binary: k8-env + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}' + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - arm + - arm64 + env: + - CGO_ENABLED=0 + +dockers: + - goarch: amd64 + image_templates: + - "ghcr.io/orange-cloudavenue/{{.ProjectName}}:v{{ .Version }}-amd64" + extra_files: + - ui/ + dockerfile: Dockerfile + use: buildx + ids: + - k8-env + build_flag_templates: + - --platform=linux/amd64 + - --pull + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }} + - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{.ProjectName}} + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + + - goarch: arm64 + image_templates: + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}-arm64v8" + extra_files: + - ui/ + dockerfile: Dockerfile + use: buildx + ids: + - golink + - glctl + build_flag_templates: + - --platform=linux/arm64/v8 + - --pull + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .ProjectName }} + - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }} + - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{ .ProjectName }} + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + +docker_manifests: +- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}" + image_templates: + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}-arm64v8" +- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:latest" + image_templates: + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}-amd64" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}:v{{ .Version }}-arm64v8" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b5c66a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.1.0 (Unreleased) + +## 0.0.1 (February 12, 2024) + +* Init project diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b18ac8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine + +ENV GID 1000 +ENV UID 1000 + +COPY --chown=${UID}:${GID} prestop.sh /usr/bin +COPY k8-env /usr/bin + +ENTRYPOINT ["/usr/bin/golink"] \ No newline at end of file diff --git a/README.md b/README.md index 61ce626..20fd685 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ # k8-env -A Kubernetes mutating webhook server that implements environnement variable injection + +A Kubernetes mutating webhook server that implements environnement variable injection. + +## How to use + +### Prerequisites + +- A Kubernetes cluster with the `admissionregistration.k8s.io/v1beta1` API enabled. +Verify that by the following command : + +```bash +kubectl api-versions | grep admissionregistration.k8s.io +``` diff --git a/cmd/cert.go b/cmd/cert.go new file mode 100644 index 0000000..5749129 --- /dev/null +++ b/cmd/cert.go @@ -0,0 +1,90 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" +) + +// generateCert generate a self-signed CA for given organization +// and sign certificate with the CA for given common name and dns names +// it resurns the CA, certificate and private key in PEM format +func generateCert(orgs, dnsNames []string, commonName string) (*bytes.Buffer, *bytes.Buffer, *bytes.Buffer, error) { + // init CA config + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2022), + Subject: pkix.Name{Organization: orgs}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // expired in 1 year + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // generate private key for CA + caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, err + } + + // create the CA certificate + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivateKey.PublicKey, caPrivateKey) + if err != nil { + return nil, nil, nil, err + } + + // CA certificate with PEM encoded + caPEM := new(bytes.Buffer) + _ = pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + // new certificate config + newCert := &x509.Certificate{ + DNSNames: dnsNames, + SerialNumber: big.NewInt(1024), + Subject: pkix.Name{ + CommonName: commonName, + Organization: orgs, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // expired in 1 year + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + // generate new private key + newPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, nil, err + } + + // sign the new certificate + newCertBytes, err := x509.CreateCertificate(rand.Reader, newCert, ca, &newPrivateKey.PublicKey, caPrivateKey) + if err != nil { + return nil, nil, nil, err + } + + // new certificate with PEM encoded + newCertPEM := new(bytes.Buffer) + _ = pem.Encode(newCertPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: newCertBytes, + }) + + // new private key with PEM encoded + newPrivateKeyPEM := new(bytes.Buffer) + _ = pem.Encode(newPrivateKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(newPrivateKey), + }) + + return caPEM, newCertPEM, newPrivateKeyPEM, nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..c878257 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" +) + +var ( + infoLogger *log.Logger + warningLogger *log.Logger + errorLogger *log.Logger +) + +var ( + port int + envConfigFile string + webhookNamespace, webhookServiceName string +) + +func init() { + // init loggers + infoLogger = log.New(os.Stderr, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) + warningLogger = log.New(os.Stderr, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile) + errorLogger = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) + + // webhook server running namespace + webhookNamespace = os.Getenv("POD_NAMESPACE") +} + +func main() { + // init command flags + flag.IntVar(&port, "port", 8443, "Webhook server port.") + flag.StringVar(&webhookServiceName, "service-name", "k8-env", "Webhook service name.") + flag.StringVar(&envConfigFile, "env-config-file", "/etc/webhook/config/envconfig.yaml", "Environnement variable injector configuration file.") + flag.Parse() + + dnsNames := []string{ + webhookServiceName, + webhookServiceName + "." + webhookNamespace, + webhookServiceName + "." + webhookNamespace + ".svc", + } + commonName := webhookServiceName + "." + webhookNamespace + ".svc" + + caPEM, certPEM, certKeyPEM, err := generateCert([]string{admissionWebhookAnnotationBase}, dnsNames, commonName) + if err != nil { + errorLogger.Fatalf("Failed to generate ca and certificate key pair: %v", err) + } + + pair, err := tls.X509KeyPair(certPEM.Bytes(), certKeyPEM.Bytes()) + if err != nil { + errorLogger.Fatalf("Failed to load certificate key pair: %v", err) + } + + envConfig, err := loadConfig(envConfigFile) + if err != nil { + errorLogger.Fatalf("Failed to load configuration: %v", err) + } + + // create or update the mutatingwebhookconfiguration + err = createOrUpdateMutatingWebhookConfiguration(caPEM, webhookServiceName, webhookNamespace) + if err != nil { + errorLogger.Fatalf("Failed to create or update the mutating webhook configuration: %v", err) + } + + whsvr := &WebhookServer{ + envConfig: envConfig, + server: &http.Server{ + Addr: fmt.Sprintf(":%v", port), + TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}}, + }, + } + + // define http server and server handler + mux := http.NewServeMux() + mux.HandleFunc(webhookInjectPath, whsvr.serve) + whsvr.server.Handler = mux + + // start webhook server in new rountine + go func() { + if err := whsvr.server.ListenAndServeTLS("", ""); err != nil { + errorLogger.Fatalf("Failed to listen and serve webhook server: %v", err) + } + }() + + // listening OS shutdown singal + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + <-signalChan + + infoLogger.Printf("Got OS shutdown signal, shutting down webhook server gracefully...") + whsvr.server.Shutdown(context.Background()) //nolint:errcheck +} diff --git a/cmd/webhook.go b/cmd/webhook.go new file mode 100644 index 0000000..b5d9540 --- /dev/null +++ b/cmd/webhook.go @@ -0,0 +1,261 @@ +package main + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "gopkg.in/yaml.v2" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +var ( + runtimeScheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(runtimeScheme) + deserializer = codecs.UniversalDeserializer() +) + +var ignoredNamespaces = []string{ + metav1.NamespaceSystem, + metav1.NamespacePublic, +} + +const ( + admissionWebhookAnnotationBase = "env-vars.k8-env.io" + admissionWebhookAnnotationInjectKey = admissionWebhookAnnotationBase + "/inject" + admissionWebhookAnnotationStatusKey = admissionWebhookAnnotationBase + "/status" +) + +type ( + WebhookServer struct { + envConfig *Config + server *http.Server + } + + Config struct { + Env []corev1.EnvVar `yaml:"env"` + } + patchOperation struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` + } +) + +func loadConfig(configFile string) (*Config, error) { + data, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + infoLogger.Printf("New configuration: sha256sum %x", sha256.Sum256(data)) + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +// Check whether the target resoured need to be mutated +func mutationRequired(ignoredList []string, metadata *metav1.ObjectMeta) bool { + // skip special kubernete system namespaces + for _, namespace := range ignoredList { + if metadata.Namespace == namespace { + infoLogger.Printf("Skip mutation for %v for it's in special namespace:%v", metadata.Name, metadata.Namespace) + return false + } + } + + annotations := metadata.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + status := annotations[admissionWebhookAnnotationStatusKey] + + // determine whether to perform mutation based on annotation for the target resource + var required bool + if strings.ToLower(status) == "injected" { + required = false + } else { + switch strings.ToLower(annotations[admissionWebhookAnnotationInjectKey]) { + default: + required = true + case "n", "not", "false", "off": + required = false + } + } + + infoLogger.Printf("Mutation policy for %v/%v: status: %q required:%v", metadata.Namespace, metadata.Name, status, required) + return required +} + +// addEnvironnement add environnement to the target container +func addEnvironnement(containers []corev1.Container, added []corev1.EnvVar, basePath string) (patch []patchOperation) { + // for each containers in the pod spec, append the env vars + for i := range containers { + // if the container has no env vars, initialize the env vars + if containers[i].Env == nil { + containers[i].Env = make([]corev1.EnvVar, 0) + } + + // for each env var in the added env vars, append the env var to the container + for _, add := range added { + // Compute the path for the env var + path := basePath + "/" + fmt.Sprintf("%d/env", i) + patch = append(patch, patchOperation{ + Op: "add", + Path: path, + Value: add, + }) + } + } + + return patch +} + +func updateAnnotation(target map[string]string, added map[string]string) (patch []patchOperation) { + for key, value := range added { + if target == nil || target[key] == "" { + target = map[string]string{} + patch = append(patch, patchOperation{ + Op: "add", + Path: "/metadata/annotations", + Value: map[string]string{ + key: value, + }, + }) + } else { + patch = append(patch, patchOperation{ + Op: "replace", + Path: "/metadata/annotations/" + key, + Value: value, + }) + } + } + return patch +} + +// create mutation patch for resoures +func createPatch(pod *corev1.Pod, config *Config, annotations map[string]string) ([]byte, error) { + var patch []patchOperation + + patch = append(patch, addEnvironnement(pod.Spec.Containers, config.Env, "/spec/container")...) + patch = append(patch, updateAnnotation(pod.Annotations, annotations)...) + + return json.Marshal(patch) +} + +// main mutation process +func (whsvr *WebhookServer) mutate(ar *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse { + req := ar.Request + var pod corev1.Pod + if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { + warningLogger.Printf("Could not unmarshal raw object: %v", err) + return &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } + + infoLogger.Printf("AdmissionReview for Kind=%v, Namespace=%v Name=%v (%v) UID=%v patchOperation=%v UserInfo=%v", + req.Kind, req.Namespace, req.Name, pod.Name, req.UID, req.Operation, req.UserInfo) + + // determine whether to perform mutation + if !mutationRequired(ignoredNamespaces, &pod.ObjectMeta) { + infoLogger.Printf("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name) + return &admissionv1.AdmissionResponse{ + Allowed: true, + } + } + + annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"} + patchBytes, err := createPatch(&pod, whsvr.envConfig, annotations) + if err != nil { + return &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } + + infoLogger.Printf("AdmissionResponse: patch=%v\n", string(patchBytes)) + return &admissionv1.AdmissionResponse{ + Allowed: true, + Patch: patchBytes, + PatchType: func() *admissionv1.PatchType { + pt := admissionv1.PatchTypeJSONPatch + return &pt + }(), + } +} + +// Serve method for webhook server +func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { + var body []byte + if r.Body != nil { + if data, err := io.ReadAll(r.Body); err == nil { + body = data + } + } + if len(body) == 0 { + warningLogger.Println("empty body") + http.Error(w, "empty body", http.StatusBadRequest) + return + } + + // verify the content type is accurate + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + warningLogger.Printf("Content-Type=%s, expect application/json", contentType) + http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) + return + } + + var admissionResponse *admissionv1.AdmissionResponse + ar := admissionv1.AdmissionReview{} + if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { + warningLogger.Printf("Can't decode body: %v", err) + admissionResponse = &admissionv1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } + } else { + admissionResponse = whsvr.mutate(&ar) + } + + admissionReview := admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admission.k8s.io/v1", + Kind: "AdmissionReview", + }, + } + if admissionResponse != nil { + admissionReview.Response = admissionResponse + if ar.Request != nil { + admissionReview.Response.UID = ar.Request.UID + } + } + + resp, err := json.Marshal(admissionReview) + if err != nil { + warningLogger.Printf("Can't encode response: %v", err) + http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) + } + infoLogger.Printf("Ready to write reponse ...") + if _, err := w.Write(resp); err != nil { + warningLogger.Printf("Can't write response: %v", err) + http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) + } +} diff --git a/cmd/webhookconfig.go b/cmd/webhookconfig.go new file mode 100644 index 0000000..c057253 --- /dev/null +++ b/cmd/webhookconfig.go @@ -0,0 +1,108 @@ +package main + +import ( + "bytes" + "context" + "os" + "reflect" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +var ( + webhookConfigName = "sidecar-injector-webhook" + webhookInjectPath = "/inject" +) + +func createOrUpdateMutatingWebhookConfiguration(caPEM *bytes.Buffer, webhookService, webhookNamespace string) error { + infoLogger.Println("Initializing the kube client...") + + kubeconfig := os.Getenv("KUBECONFIG") + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return err + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + mutatingWebhookConfigV1Client := clientset.AdmissionregistrationV1() + + infoLogger.Printf("Creating or updating the mutatingwebhookconfiguration: %s", webhookConfigName) + fail := admissionregistrationv1.Fail + sideEffect := admissionregistrationv1.SideEffectClassNone + mutatingWebhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: webhookConfigName, + }, + Webhooks: []admissionregistrationv1.MutatingWebhook{{ + Name: "sidecar-injector.morven.me", + AdmissionReviewVersions: []string{"v1", "v1beta1"}, + SideEffects: &sideEffect, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + CABundle: caPEM.Bytes(), // self-generated CA for the webhook + Service: &admissionregistrationv1.ServiceReference{ + Name: webhookService, + Namespace: webhookNamespace, + Path: &webhookInjectPath, + }, + }, + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"pods"}, + }, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "sidecar-injection": "enabled", + }, + }, + FailurePolicy: &fail, + }}, + } + + foundWebhookConfig, err := mutatingWebhookConfigV1Client.MutatingWebhookConfigurations().Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) + if err != nil && apierrors.IsNotFound(err) { + if _, err := mutatingWebhookConfigV1Client.MutatingWebhookConfigurations().Create(context.TODO(), mutatingWebhookConfig, metav1.CreateOptions{}); err != nil { + warningLogger.Printf("Failed to create the mutatingwebhookconfiguration: %s", webhookConfigName) + return err + } + infoLogger.Printf("Created mutatingwebhookconfiguration: %s", webhookConfigName) + } else if err != nil { + warningLogger.Printf("Failed to check the mutatingwebhookconfiguration: %s", webhookConfigName) + return err + } else { + // there is an existing mutatingWebhookConfiguration + if len(foundWebhookConfig.Webhooks) != len(mutatingWebhookConfig.Webhooks) || + !(foundWebhookConfig.Webhooks[0].Name == mutatingWebhookConfig.Webhooks[0].Name && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].AdmissionReviewVersions, mutatingWebhookConfig.Webhooks[0].AdmissionReviewVersions) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].SideEffects, mutatingWebhookConfig.Webhooks[0].SideEffects) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].FailurePolicy, mutatingWebhookConfig.Webhooks[0].FailurePolicy) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].Rules, mutatingWebhookConfig.Webhooks[0].Rules) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].NamespaceSelector, mutatingWebhookConfig.Webhooks[0].NamespaceSelector) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].ClientConfig.CABundle, mutatingWebhookConfig.Webhooks[0].ClientConfig.CABundle) && + reflect.DeepEqual(foundWebhookConfig.Webhooks[0].ClientConfig.Service, mutatingWebhookConfig.Webhooks[0].ClientConfig.Service)) { + mutatingWebhookConfig.ObjectMeta.ResourceVersion = foundWebhookConfig.ObjectMeta.ResourceVersion + if _, err := mutatingWebhookConfigV1Client.MutatingWebhookConfigurations().Update(context.TODO(), mutatingWebhookConfig, metav1.UpdateOptions{}); err != nil { + warningLogger.Printf("Failed to update the mutatingwebhookconfiguration: %s", webhookConfigName) + return err + } + infoLogger.Printf("Updated the mutatingwebhookconfiguration: %s", webhookConfigName) + } + infoLogger.Printf("The mutatingwebhookconfiguration: %s already exists and has no change", webhookConfigName) + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ae71a01 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module github.com/orange-cloudavenue/k8-env + +go 1.21.6 + +require ( + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.29.1 + k8s.io/apimachinery v0.29.1 + k8s.io/client-go v0.29.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c086b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,156 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= +k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= +k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= +k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= +k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/manifests/cluster-role-binding.yaml b/manifests/cluster-role-binding.yaml new file mode 100644 index 0000000..97f7eeb --- /dev/null +++ b/manifests/cluster-role-binding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: k8-env + labels: + app: k8-env +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: k8-env +subjects: +- kind: ServiceAccount + name: k8-env + namespace: k8-env \ No newline at end of file diff --git a/manifests/cluster-role.yaml b/manifests/cluster-role.yaml new file mode 100644 index 0000000..e39fee2 --- /dev/null +++ b/manifests/cluster-role.yaml @@ -0,0 +1,10 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: k8-env + labels: + app: k8-env +rules: +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations"] + verbs: ["create", "get", "delete", "list", "patch", "update", "watch"] \ No newline at end of file diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml new file mode 100644 index 0000000..aa96d27 --- /dev/null +++ b/manifests/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8-env + labels: + app: k8-env +data: + sidecarconfig.yaml: | + env: + - name: demo + Value: demo diff --git a/manifests/deployment.yaml b/manifests/deployment.yaml new file mode 100644 index 0000000..ae7b17f --- /dev/null +++ b/manifests/deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8-env + labels: + app: k8-env +spec: + replicas: 1 + selector: + matchLabels: + app: k8-env + template: + metadata: + labels: + app: k8-env + spec: + serviceAccountName: k8-env + containers: + - name: k8-env + image: k8-env + imagePullPolicy: Always + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/usr/bin/prestop.sh"] + volumeMounts: + - name: webhook-config + mountPath: /etc/webhook/config + volumes: + - name: webhook-config + configMap: + name: k8-env diff --git a/manifests/env-config.yaml b/manifests/env-config.yaml new file mode 100644 index 0000000..29f11c0 --- /dev/null +++ b/manifests/env-config.yaml @@ -0,0 +1,7 @@ +env: + - name: HTTP_PROXY + Value: http://10.1.2.3:3128 + - name: HTTPS_PROXY + Value: http://10.1.2.3:3128 + - name: NO_PROXY + Value: cluster.local,localhost,127.0.0.1 \ No newline at end of file diff --git a/manifests/manifests.yaml b/manifests/manifests.yaml new file mode 100644 index 0000000..f7c2e6c --- /dev/null +++ b/manifests/manifests.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8-env + labels: + app: k8-env +spec: + replicas: 1 + selector: + matchLabels: + app: k8-env + template: + metadata: + labels: + app: k8-env + spec: + serviceAccountName: k8-env + containers: + - name: k8-env + image: k8-env + imagePullPolicy: Always + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/usr/bin/prestop.sh"] + volumeMounts: + - name: webhook-config + mountPath: /etc/webhook/config + volumes: + - name: webhook-config + configMap: + name: k8-env +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: k8-env + labels: + app: k8-env +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: k8-env +subjects: +- kind: ServiceAccount + name: k8-env + namespace: k8-env +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: k8-env + labels: + app: k8-env +rules: +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations"] + verbs: ["create", "get", "delete", "list", "patch", "update", "watch"] +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8-env + labels: + app: k8-env +data: + env-config.yaml: | + env: + - name: demo + Value: demo +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8-env + labels: + app: k8-env +--- +apiVersion: v1 +kind: Service +metadata: + name: k8-env + labels: + app: k8-env +spec: + ports: + - port: 443 + targetPort: 8443 + selector: + app: k8-env diff --git a/manifests/service-account.yaml b/manifests/service-account.yaml new file mode 100644 index 0000000..9d0dcb6 --- /dev/null +++ b/manifests/service-account.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8-env + labels: + app: k8-env \ No newline at end of file diff --git a/manifests/service.yaml b/manifests/service.yaml new file mode 100644 index 0000000..9cd2bcd --- /dev/null +++ b/manifests/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: k8-env + labels: + app: k8-env +spec: + ports: + - port: 443 + targetPort: 8443 + selector: + app: k8-env \ No newline at end of file diff --git a/prestop.sh b/prestop.sh new file mode 100644 index 0000000..966400d --- /dev/null +++ b/prestop.sh @@ -0,0 +1,21 @@ +set -exo pipefail + +# Point to the internal API server hostname +APISERVER=https://kubernetes.default.svc + +# Path to ServiceAccount token +SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount + +# Read this Pod's namespace +NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace) + +# Read the ServiceAccount bearer token +TOKEN=$(cat ${SERVICEACCOUNT}/token) + +# Reference the internal certificate authority (CA) +CACERT=${SERVICEACCOUNT}/ca.crt + +MutatingWebhookConfigurationName=sidecar-injector-webhook + +# Delete the validatingwebhookconfiguration with TOKEN +curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X DELETE ${APISERVER}/apis/admissionregistration.k8s.io/v1/mutatingwebhookconfigurations/${MutatingWebhookConfigurationName} \ No newline at end of file