├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── gendoc.go ├── load.go ├── root.go ├── version.go └── watch.go ├── docs ├── git2kube.md ├── git2kube_completion.md ├── git2kube_completion_bash.md ├── git2kube_completion_fish.md ├── git2kube_completion_powershell.md ├── git2kube_completion_zsh.md ├── git2kube_gendoc.md ├── git2kube_load.md ├── git2kube_load_configmap.md ├── git2kube_load_folder.md ├── git2kube_load_secret.md ├── git2kube_version.md ├── git2kube_watch.md ├── git2kube_watch_configmap.md ├── git2kube_watch_folder.md └── git2kube_watch_secret.md ├── example ├── README.md ├── cronjob.yaml ├── sidecar.yaml └── watcher.yaml ├── go.mod ├── go.sum ├── main.go └── pkg ├── cmd ├── common.go └── common_test.go ├── fetch ├── fetch.go ├── fetch_test.go └── testdata │ └── dummy.key └── upload ├── testdata ├── test.json └── test.yaml ├── upload.go └── upload_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | Dockerfile 4 | LICENSE 5 | README.md 6 | docker-compose.yml 7 | target/ 8 | logs/ 9 | vendor/ 10 | .project 11 | .classpath 12 | *.prefs 13 | *.iml 14 | .idea 15 | .travis.yml 16 | git2kube 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | reviewers: 8 | - "wandera/delta" 9 | groups: 10 | gomod: 11 | patterns: 12 | - "*" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | reviewers: 18 | - "wandera/delta" 19 | - package-ecosystem: "docker" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | reviewers: 24 | - "wandera/delta" 25 | ignore: 26 | - dependency-name: "*" 27 | update-types: ["version-update:semver-major"] 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | jobs: 5 | golangci: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout code 9 | uses: actions/checkout@v4 10 | - name: Install Go 11 | uses: actions/setup-go@v5 12 | with: 13 | cache: false 14 | go-version-file: go.mod 15 | - name: Lint 16 | uses: golangci/golangci-lint-action@v8 17 | with: 18 | version: v2.1 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | BINARY_NAME: ${{ github.event.repository.name }} 11 | IMAGE_NAME: ${{ github.repository }} 12 | TAG: ${{ github.ref_name }} 13 | 14 | jobs: 15 | release-binary: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | goos: [linux, windows, darwin] 20 | goarch: [386, arm64, amd64] 21 | exclude: 22 | - goarch: "386" 23 | goos: darwin 24 | steps: 25 | - name: Checkout the latest code 26 | uses: actions/checkout@v4 27 | - name: Install Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version-file: go.mod 31 | - name: Build ${{ matrix.goos }}/${{ matrix.goarch }} 32 | env: 33 | GOOS: ${{ matrix.goos }} 34 | GOARCH: ${{ matrix.goarch }} 35 | shell: bash 36 | run: | 37 | if [ "$GOOS" = "windows" ]; then 38 | GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o dist/${{ env.BINARY_NAME }}.exe -ldflags '-w -s -X 'github.com/wandera/${{ env.BINARY_NAME }}/cmd.Version=${{ env.TAG }} 39 | else 40 | CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o dist/${{ env.BINARY_NAME }} -ldflags '-w -s -X 'github.com/wandera/${{ env.BINARY_NAME }}/cmd.Version=${{ env.TAG }} 41 | fi 42 | tar -czvf ${{ env.BINARY_NAME }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz -C dist/ . 43 | - name: Release 44 | uses: softprops/action-gh-release@v2 45 | with: 46 | files: ${{ env.BINARY_NAME }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz 47 | 48 | release-docker-image: 49 | runs-on: ubuntu-latest 50 | permissions: 51 | contents: read 52 | packages: write 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Log in to the Container registry 59 | uses: docker/login-action@v3 60 | with: 61 | registry: ${{ env.REGISTRY }} 62 | username: ${{ github.actor }} 63 | password: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: Extract metadata (tags, labels) for Docker 66 | id: meta 67 | uses: docker/metadata-action@v5 68 | with: 69 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 70 | tags: | 71 | type=semver,pattern={{raw}} 72 | type=semver,pattern=v{{major}}.{{minor}} 73 | type=semver,pattern=v{{major}} 74 | type=sha 75 | 76 | - name: Set up Docker Buildx 77 | uses: docker/setup-buildx-action@v3 78 | 79 | - name: Docker build & push 80 | uses: docker/build-push-action@v6 81 | with: 82 | push: true 83 | context: . 84 | platforms: linux/amd64,linux/arm64 85 | build-args: | 86 | VERSION=${{ github.ref_name }} 87 | tags: ${{ steps.meta.outputs.tags }} 88 | labels: ${{ steps.meta.outputs.labels }} 89 | 90 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | issue_comment: 9 | types: 10 | - created 11 | 12 | jobs: 13 | unit: 14 | if: (github.event.issue.pull_request != '' && contains(github.event.comment.body, '/test')) || github.event_name == 'pull_request' || github.event_name == 'push' 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | - name: Install Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: go.mod 26 | - name: Test 27 | run: go test -v ./... 28 | build: 29 | if: (github.event.issue.pull_request != '' && contains(github.event.comment.body, '/test')) || github.event_name == 'pull_request' || github.event_name == 'push' 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | - name: Docker build 37 | uses: docker/build-push-action@v6 38 | with: 39 | context: . 40 | build-args: | 41 | VERSION=${{ github.ref_name }} 42 | vulncheck: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout code 46 | uses: actions/checkout@v4 47 | - name: Install Go 48 | uses: actions/setup-go@v5 49 | id: go-version 50 | with: 51 | cache: false 52 | go-version-file: go.mod 53 | - name: Scan for Vulnerabilities in Code 54 | uses: wandera/govulncheck-action@v1.0.0 55 | with: 56 | go-version: ${{ steps.go-version.outputs.go-version }} 57 | package: ./... 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Package Files # 2 | *.jar 3 | *.war 4 | *.ear 5 | 6 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 7 | hs_err_pid* 8 | .DS_Store 9 | .classpath 10 | .project 11 | .settings 12 | .idea 13 | *.iml 14 | /target 15 | /vendor 16 | git2kube 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 5m 4 | linters: 5 | default: none 6 | enable: 7 | - godot 8 | - govet 9 | - gosec 10 | - ineffassign 11 | - staticcheck 12 | - unparam 13 | - unused 14 | - whitespace 15 | settings: 16 | gosec: 17 | config: 18 | global: 19 | audit: true 20 | exclusions: 21 | rules: 22 | # Exclude some linters from running on tests files. 23 | - path: _test\.go 24 | linters: 25 | - ineffassign 26 | - gosec 27 | formatters: 28 | enable: 29 | - gci 30 | - gofumpt 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder image 2 | FROM golang:1.24-alpine3.21 AS builder 3 | 4 | WORKDIR /build 5 | 6 | ARG VERSION 7 | 8 | COPY . . 9 | RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build GOMODCACHE=/go/pkg/mod GOCACHE=/root/.cache/go-build go build -v -ldflags '-w -s -X 'github.com/wandera/git2kube/cmd.Version=${VERSION} 10 | 11 | # Runtime image 12 | FROM alpine:3.21.0 13 | RUN apk --no-cache add ca-certificates 14 | 15 | RUN apk --no-cache --virtual .openssh add openssh \ 16 | && mkdir -p /etc/ssh \ 17 | && ssh-keyscan -t rsa github.com > /etc/ssh/ssh_known_hosts \ 18 | && apk del .openssh 19 | 20 | COPY --from=builder /build/git2kube /app/git2kube 21 | WORKDIR /app 22 | 23 | ENTRYPOINT ["./git2kube"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wandera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git2kube - From Git to Kubernetes 2 | 3 | [![Test](https://github.com/wandera/git2kube/actions/workflows/test.yml/badge.svg)](https://github.com/wandera/git2kube/actions/workflows/test.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/wandera/git2kube)](https://goreportcard.com/report/github.com/wandera/git2kube) 5 | [![GitHub release](https://img.shields.io/github/release/wandera/git2kube.svg)](https://github.com/wandera/git2kube/releases/latest) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/wandera/scccmd/blob/master/LICENSE) 7 | 8 | Tool for syncing git with Kubernetes. 9 | 10 | ### Features 11 | * Synchronisation of Git repository with Kubernetes ConfigMap/Secret 12 | * One shot or periodic 13 | * Configurable healthcheck 14 | * Configurable labels and annotations 15 | * Configurable include/exclude rules for filtering files that should be synchronised 16 | * Ability to synchronise git into target folder using symlinks (suitable for sidecar deployments) 17 | * SSH key and Basic auth 18 | 19 | ### Quickstart 20 | Check out [example](example) folder that should get you started. 21 | 22 | ### Docker repository 23 | The tool is released as docker image as well, check the [repository](https://github.com/wandera/git2kube/pkgs/container/git2kube). 24 | 25 | ### Documentation 26 | * [docs](docs/git2kube.md) - Generated documentation for the tool 27 | * [example](example) - Kubernetes deployment examples 28 | 29 | ### How to develop 30 | * Tests are started by `go test -v ./...` 31 | * Or if you dont want to setup your local go env just use the provided Dockerfile 32 | -------------------------------------------------------------------------------- /cmd/gendoc.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/cobra/doc" 6 | ) 7 | 8 | var docDestination string 9 | 10 | var genDocCmd = &cobra.Command{ 11 | Use: "gendoc", 12 | Short: "Generates documentation for this tool in Markdown format", 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | return executeGenDoc() 15 | }, 16 | } 17 | 18 | func executeGenDoc() error { 19 | err := doc.GenMarkdownTree(rootCmd, docDestination) 20 | return err 21 | } 22 | 23 | func init() { 24 | genDocCmd.Flags().StringVarP(&docDestination, "destination", "d", "", "destination for documentation") 25 | genDocCmd.MarkFlagRequired("destination") // #nosec G104 26 | } 27 | -------------------------------------------------------------------------------- /cmd/load.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/wandera/git2kube/pkg/cmd" 8 | "github.com/wandera/git2kube/pkg/fetch" 9 | "github.com/wandera/git2kube/pkg/upload" 10 | ) 11 | 12 | var lp = struct { 13 | kubeconfig bool 14 | git string 15 | branch string 16 | folder string 17 | target string 18 | namespace string 19 | mergetype string 20 | includes []string 21 | excludes []string 22 | sshkey string 23 | labels []string 24 | annotations []string 25 | }{} 26 | 27 | var loadCmd = &cobra.Command{ 28 | Use: "load", 29 | Short: "Loads files from git repository into target", 30 | DisableFlagParsing: true, 31 | PersistentPreRunE: cmd.ExpandArgs, 32 | } 33 | 34 | var loadConfigmapCmd = &cobra.Command{ 35 | Use: "configmap", 36 | Short: "Loads files from git repository into ConfigMap", 37 | DisableFlagParsing: true, 38 | RunE: func(c *cobra.Command, args []string) error { 39 | return executeLoad(upload.ConfigMap) 40 | }, 41 | } 42 | 43 | var loadSecretCmd = &cobra.Command{ 44 | Use: "secret", 45 | Short: "Loads files from git repository into Secret", 46 | DisableFlagParsing: true, 47 | RunE: func(c *cobra.Command, args []string) error { 48 | return executeLoad(upload.Secret) 49 | }, 50 | } 51 | 52 | var loadFolderCmd = &cobra.Command{ 53 | Use: "folder", 54 | Short: "Loads files from git repository into Folder", 55 | DisableFlagParsing: true, 56 | RunE: func(c *cobra.Command, args []string) error { 57 | return executeLoad(upload.Folder) 58 | }, 59 | } 60 | 61 | func executeLoad(lt upload.LoadType) error { 62 | // #nosec G301 63 | if err := os.MkdirAll(lp.folder, os.ModePerm); err != nil { 64 | return err 65 | } 66 | 67 | auth, err := fetch.NewAuth(lp.git, lp.sshkey) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | fetcher := fetch.NewFetcher(lp.git, lp.folder, lp.branch, auth) 73 | 74 | c, err := fetcher.Fetch() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | iter, err := c.Files() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | uploader, err := upload.NewUploader(lt, upload.UploaderOptions{ 85 | Source: lp.folder, 86 | Kubeconfig: lp.kubeconfig, 87 | Target: lp.target, 88 | Namespace: lp.namespace, 89 | MergeType: upload.MergeType(lp.mergetype), 90 | Includes: lp.includes, 91 | Excludes: lp.excludes, 92 | Annotations: lp.annotations, 93 | Labels: lp.labels, 94 | }) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = uploader.Upload(c.ID().String(), iter) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | return err 105 | } 106 | 107 | func init() { 108 | loadCmd.PersistentFlags().StringVarP(&lp.git, "git", "g", "", "git repository address, either http(s) or ssh protocol has to be specified") 109 | loadCmd.PersistentFlags().StringVarP(&lp.branch, "branch", "b", "master", "branch name to pull") 110 | loadCmd.PersistentFlags().StringVarP(&lp.folder, "cache-folder", "c", "/tmp/git2kube/data/", "destination on filesystem where cache of repository will be stored") 111 | loadCmd.PersistentFlags().StringSliceVar(&lp.includes, "include", []string{".*"}, "regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder") 112 | loadCmd.PersistentFlags().StringSliceVar(&lp.excludes, "exclude", []string{"^\\..*"}, "regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder") 113 | loadCmd.PersistentFlags().StringVarP(&lp.sshkey, "ssh-key", "p", "", "path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git)") 114 | 115 | loadCmd.MarkPersistentFlagRequired("git") // #nosec G104 116 | loadCmd.MarkPersistentFlagFilename("cache-folder") // #nosec G104 117 | 118 | loadConfigmapCmd.Flags().BoolVarP(&lp.kubeconfig, "kubeconfig", "k", false, "true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false)") 119 | loadConfigmapCmd.Flags().StringVarP(&lp.namespace, "namespace", "n", "default", "target namespace for the resulting ConfigMap") 120 | loadConfigmapCmd.Flags().StringVarP(&lp.target, "configmap", "m", "", "name for the resulting ConfigMap") 121 | loadConfigmapCmd.Flags().StringSliceVar(&lp.labels, "label", []string{}, "label to add to K8s ConfigMap (format NAME=VALUE)") 122 | loadConfigmapCmd.Flags().StringSliceVar(&lp.annotations, "annotation", []string{}, "annotation to add to K8s ConfigMap (format NAME=VALUE)") 123 | loadConfigmapCmd.Flags().StringVarP(&lp.mergetype, "merge-type", "", "delete", "how to merge ConfigMap data whether to also delete missing values or just upsert new (options: delete|upsert)") 124 | loadConfigmapCmd.MarkFlagFilename("kubeconfig") // #nosec G104 125 | loadConfigmapCmd.MarkFlagRequired("configmap") // #nosec G104 126 | 127 | loadSecretCmd.Flags().BoolVarP(&lp.kubeconfig, "kubeconfig", "k", false, "true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false)") 128 | loadSecretCmd.Flags().StringVarP(&lp.namespace, "namespace", "n", "default", "target namespace for the resulting ConfigMap") 129 | loadSecretCmd.Flags().StringVarP(&lp.target, "secret", "s", "", "name for the resulting Secret") 130 | loadSecretCmd.Flags().StringSliceVar(&lp.labels, "label", []string{}, "label to add to K8s Secret (format NAME=VALUE)") 131 | loadSecretCmd.Flags().StringSliceVar(&lp.annotations, "annotation", []string{}, "annotation to add to K8s Secret (format NAME=VALUE)") 132 | loadSecretCmd.Flags().StringVarP(&lp.mergetype, "merge-type", "", "delete", "how to merge Secret data whether to also delete missing values or just upsert new (options: delete|upsert)") 133 | loadSecretCmd.MarkFlagFilename("kubeconfig") // #nosec G104 134 | loadSecretCmd.MarkFlagRequired("secret") // #nosec G104 135 | 136 | loadFolderCmd.Flags().StringVarP(&lp.target, "target-folder", "t", "", "path to target folder") 137 | loadFolderCmd.MarkFlagRequired("target-folder") // #nosec G104 138 | loadFolderCmd.MarkFlagFilename("target-folder") // #nosec G104 139 | 140 | loadCmd.AddCommand(loadConfigmapCmd) 141 | loadCmd.AddCommand(loadSecretCmd) 142 | loadCmd.AddCommand(loadFolderCmd) 143 | } 144 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var loglevel string 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "git2kube", 14 | DisableAutoGenTag: true, 15 | Short: "Git to ConfigMap conversion tool", 16 | Long: `Commandline tool for loading files from git repository into K8s ConfigMap`, 17 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 18 | lvl, err := log.ParseLevel(loglevel) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | log.SetLevel(lvl) 24 | return nil 25 | }, 26 | } 27 | 28 | func init() { 29 | rootCmd.PersistentFlags().StringVarP(&loglevel, "log-level", "l", "info", fmt.Sprintf("command log level (options: %s)", log.AllLevels)) 30 | 31 | rootCmd.AddCommand(loadCmd) 32 | rootCmd.AddCommand(watchCmd) 33 | rootCmd.AddCommand(genDocCmd) 34 | rootCmd.AddCommand(versionCmd) 35 | } 36 | 37 | // Execute run root command (main entrypoint). 38 | func Execute() error { 39 | return rootCmd.Execute() 40 | } 41 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // Version variable is set at build time. 10 | var Version = "unknown" 11 | 12 | var versionCmd = &cobra.Command{ 13 | Use: "version", 14 | Short: "Print the version information", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println(Version) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /cmd/watch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "path" 7 | "syscall" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | "github.com/wandera/git2kube/pkg/cmd" 13 | "github.com/wandera/git2kube/pkg/fetch" 14 | "github.com/wandera/git2kube/pkg/upload" 15 | ) 16 | 17 | type healthCheckStatus string 18 | 19 | const ( 20 | ok healthCheckStatus = "OK" 21 | nok healthCheckStatus = "NOK" 22 | ) 23 | 24 | var wp = struct { 25 | kubeconfig bool 26 | git string 27 | branch string 28 | folder string 29 | target string 30 | namespace string 31 | mergetype string 32 | interval int 33 | includes []string 34 | excludes []string 35 | sshkey string 36 | labels []string 37 | annotations []string 38 | healthCheckFile string 39 | }{} 40 | 41 | var watchCmd = &cobra.Command{ 42 | Use: "watch", 43 | Short: "Runs watcher that periodically check the provided repository", 44 | DisableFlagParsing: true, 45 | PersistentPreRunE: cmd.ExpandArgs, 46 | } 47 | 48 | var watchConfigmapCmd = &cobra.Command{ 49 | Use: "configmap", 50 | Short: "Runs watcher that periodically check the provided repository and updates K8s ConfigMap accordingly", 51 | DisableFlagParsing: true, 52 | RunE: func(c *cobra.Command, args []string) error { 53 | return executeWatch(upload.ConfigMap) 54 | }, 55 | } 56 | 57 | var watchSecretCmd = &cobra.Command{ 58 | Use: "secret", 59 | Short: "Runs watcher that periodically check the provided repository and updates K8s Secret accordingly", 60 | DisableFlagParsing: true, 61 | RunE: func(c *cobra.Command, args []string) error { 62 | return executeWatch(upload.Secret) 63 | }, 64 | } 65 | 66 | var watchFolderCmd = &cobra.Command{ 67 | Use: "folder", 68 | Short: "Runs watcher that periodically check the provided repository and updates target folder accordingly", 69 | DisableFlagParsing: true, 70 | RunE: func(c *cobra.Command, args []string) error { 71 | return executeWatch(upload.Folder) 72 | }, 73 | } 74 | 75 | func executeWatch(lt upload.LoadType) error { 76 | // #nosec G301 77 | if err := os.MkdirAll(wp.folder, os.ModePerm); err != nil { 78 | return err 79 | } 80 | 81 | auth, err := fetch.NewAuth(wp.git, wp.sshkey) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | fetcher := fetch.NewFetcher(wp.git, wp.folder, wp.branch, auth) 87 | 88 | uploader, err := upload.NewUploader(lt, upload.UploaderOptions{ 89 | Source: wp.folder, 90 | Kubeconfig: wp.kubeconfig, 91 | Target: wp.target, 92 | Namespace: wp.namespace, 93 | MergeType: upload.MergeType(wp.mergetype), 94 | Includes: wp.includes, 95 | Excludes: wp.excludes, 96 | Annotations: wp.annotations, 97 | Labels: wp.labels, 98 | }) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = refresh(fetcher, uploader) 104 | if err != nil { 105 | log.Errorf("Initial sync failed: %v", err) 106 | return err 107 | } 108 | log.Info("Initial sync succeeded") 109 | 110 | ticker := time.NewTicker(time.Duration(wp.interval) * time.Second) 111 | stop := make(chan struct{}) 112 | 113 | go func() { 114 | for { 115 | select { 116 | case <-ticker.C: 117 | err := refresh(fetcher, uploader) 118 | if err != nil { 119 | log.Warnf("Sync failed: %v", err) 120 | } 121 | case <-stop: 122 | ticker.Stop() 123 | return 124 | } 125 | } 126 | }() 127 | 128 | signalChan := make(chan os.Signal, 1) 129 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 130 | 131 | log.Info("Started watcher") 132 | <-signalChan 133 | log.Info("Shutdown signal received, exiting...") 134 | close(stop) 135 | 136 | return nil 137 | } 138 | 139 | func refresh(fetcher fetch.Fetcher, uploader upload.Uploader) error { 140 | c, err := fetcher.Fetch() 141 | if err != nil { 142 | writeHealthCheck(nok) 143 | return err 144 | } 145 | 146 | iter, err := c.Files() 147 | if err != nil { 148 | writeHealthCheck(nok) 149 | return err 150 | } 151 | 152 | err = uploader.Upload(c.ID().String(), iter) 153 | if err != nil { 154 | writeHealthCheck(nok) 155 | return err 156 | } 157 | 158 | writeHealthCheck(ok) 159 | return nil 160 | } 161 | 162 | func writeHealthCheck(status healthCheckStatus) { 163 | if wp.healthCheckFile != "" { 164 | go func() { 165 | dir := path.Dir(wp.healthCheckFile) 166 | err := os.MkdirAll(dir, os.ModePerm) // #nosec G301 167 | if err != nil { 168 | log.Errorf("Unable to create healthcheck folder") 169 | } 170 | 171 | err = os.WriteFile(wp.healthCheckFile, []byte(status), 0o600) 172 | if err != nil { 173 | log.Errorf("Unable to write healthcheck file") 174 | } 175 | }() 176 | } 177 | } 178 | 179 | func init() { 180 | watchCmd.PersistentFlags().StringVar(&wp.healthCheckFile, "healthcheck-file", "", "path to file where each refresh writes if it was successful or not, useful for K8s liveness/readiness probe") 181 | watchCmd.PersistentFlags().IntVarP(&wp.interval, "interval", "i", 10, "interval in seconds in which to try refreshing ConfigMap from git") 182 | watchCmd.PersistentFlags().StringVarP(&wp.git, "git", "g", "", "git repository address, either http(s) or ssh protocol has to be specified") 183 | watchCmd.PersistentFlags().StringVarP(&wp.branch, "branch", "b", "master", "branch name to pull") 184 | watchCmd.PersistentFlags().StringVarP(&wp.folder, "cache-folder", "c", "/tmp/git2kube/data/", "destination on filesystem where cache of repository will be stored") 185 | watchCmd.PersistentFlags().StringSliceVar(&wp.includes, "include", []string{".*"}, "regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder") 186 | watchCmd.PersistentFlags().StringSliceVar(&wp.excludes, "exclude", []string{"^\\..*"}, "regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder") 187 | watchCmd.PersistentFlags().StringVarP(&wp.sshkey, "ssh-key", "p", "", "path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git)") 188 | watchCmd.MarkPersistentFlagRequired("git") // #nosec G104 189 | watchCmd.MarkPersistentFlagFilename("cache-folder") // #nosec G104 190 | watchCmd.MarkPersistentFlagFilename("healthcheck-file") // #nosec G104 191 | 192 | watchConfigmapCmd.Flags().BoolVarP(&wp.kubeconfig, "kubeconfig", "k", false, "true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false)") 193 | watchConfigmapCmd.Flags().StringVarP(&wp.namespace, "namespace", "n", "default", "target namespace for the resulting ConfigMap") 194 | watchConfigmapCmd.Flags().StringVarP(&wp.target, "configmap", "m", "", "name for the resulting ConfigMap") 195 | watchConfigmapCmd.Flags().StringSliceVar(&wp.labels, "label", []string{}, "label to add to K8s ConfigMap (format NAME=VALUE)") 196 | watchConfigmapCmd.Flags().StringSliceVar(&wp.annotations, "annotation", []string{}, "annotation to add to K8s ConfigMap (format NAME=VALUE)") 197 | watchConfigmapCmd.Flags().StringVarP(&wp.mergetype, "merge-type", "", "delete", "how to merge ConfigMap data whether to also delete missing values or just upsert new (options: delete|upsert)") 198 | watchConfigmapCmd.MarkFlagFilename("kubeconfig") // #nosec G104 199 | watchConfigmapCmd.MarkFlagRequired("configmap") // #nosec G104 200 | 201 | watchSecretCmd.Flags().BoolVarP(&wp.kubeconfig, "kubeconfig", "k", false, "true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false)") 202 | watchSecretCmd.Flags().StringVarP(&wp.namespace, "namespace", "n", "default", "target namespace for the resulting ConfigMap") 203 | watchSecretCmd.Flags().StringVarP(&wp.target, "secret", "s", "", "name for the resulting Secret") 204 | watchSecretCmd.Flags().StringSliceVar(&wp.labels, "label", []string{}, "label to add to K8s Secret (format NAME=VALUE)") 205 | watchSecretCmd.Flags().StringSliceVar(&wp.annotations, "annotation", []string{}, "annotation to add to K8s Secret (format NAME=VALUE)") 206 | watchSecretCmd.Flags().StringVarP(&wp.mergetype, "merge-type", "", "delete", "how to merge Secret data whether to also delete missing values or just upsert new (options: delete|upsert)") 207 | watchSecretCmd.MarkFlagFilename("kubeconfig") // #nosec G104 208 | watchSecretCmd.MarkFlagRequired("secret") // #nosec G104 209 | 210 | watchFolderCmd.Flags().StringVarP(&wp.target, "target-folder", "t", "", "path to target folder") 211 | watchFolderCmd.MarkFlagRequired("target-folder") // #nosec G104 212 | watchFolderCmd.MarkFlagFilename("target-folder") // #nosec G104 213 | 214 | watchCmd.AddCommand(watchConfigmapCmd) 215 | watchCmd.AddCommand(watchSecretCmd) 216 | watchCmd.AddCommand(watchFolderCmd) 217 | } 218 | -------------------------------------------------------------------------------- /docs/git2kube.md: -------------------------------------------------------------------------------- 1 | ## git2kube 2 | 3 | Git to ConfigMap conversion tool 4 | 5 | ### Synopsis 6 | 7 | Commandline tool for loading files from git repository into K8s ConfigMap 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for git2kube 13 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [git2kube completion](git2kube_completion.md) - Generate the autocompletion script for the specified shell 19 | * [git2kube gendoc](git2kube_gendoc.md) - Generates documentation for this tool in Markdown format 20 | * [git2kube load](git2kube_load.md) - Loads files from git repository into target 21 | * [git2kube version](git2kube_version.md) - Print the version information 22 | * [git2kube watch](git2kube_watch.md) - Runs watcher that periodically check the provided repository 23 | 24 | -------------------------------------------------------------------------------- /docs/git2kube_completion.md: -------------------------------------------------------------------------------- 1 | ## git2kube completion 2 | 3 | Generate the autocompletion script for the specified shell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for git2kube for the specified shell. 8 | See each sub-command's help for details on how to use the generated script. 9 | 10 | 11 | ### Options 12 | 13 | ``` 14 | -h, --help help for completion 15 | ``` 16 | 17 | ### Options inherited from parent commands 18 | 19 | ``` 20 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [git2kube](git2kube.md) - Git to ConfigMap conversion tool 26 | * [git2kube completion bash](git2kube_completion_bash.md) - Generate the autocompletion script for bash 27 | * [git2kube completion fish](git2kube_completion_fish.md) - Generate the autocompletion script for fish 28 | * [git2kube completion powershell](git2kube_completion_powershell.md) - Generate the autocompletion script for powershell 29 | * [git2kube completion zsh](git2kube_completion_zsh.md) - Generate the autocompletion script for zsh 30 | 31 | -------------------------------------------------------------------------------- /docs/git2kube_completion_bash.md: -------------------------------------------------------------------------------- 1 | ## git2kube completion bash 2 | 3 | Generate the autocompletion script for bash 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the bash shell. 8 | 9 | This script depends on the 'bash-completion' package. 10 | If it is not installed already, you can install it via your OS's package manager. 11 | 12 | To load completions in your current shell session: 13 | 14 | source <(git2kube completion bash) 15 | 16 | To load completions for every new session, execute once: 17 | 18 | #### Linux: 19 | 20 | git2kube completion bash > /etc/bash_completion.d/git2kube 21 | 22 | #### macOS: 23 | 24 | git2kube completion bash > $(brew --prefix)/etc/bash_completion.d/git2kube 25 | 26 | You will need to start a new shell for this setup to take effect. 27 | 28 | 29 | ``` 30 | git2kube completion bash 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | -h, --help help for bash 37 | --no-descriptions disable completion descriptions 38 | ``` 39 | 40 | ### Options inherited from parent commands 41 | 42 | ``` 43 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 44 | ``` 45 | 46 | ### SEE ALSO 47 | 48 | * [git2kube completion](git2kube_completion.md) - Generate the autocompletion script for the specified shell 49 | 50 | -------------------------------------------------------------------------------- /docs/git2kube_completion_fish.md: -------------------------------------------------------------------------------- 1 | ## git2kube completion fish 2 | 3 | Generate the autocompletion script for fish 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the fish shell. 8 | 9 | To load completions in your current shell session: 10 | 11 | git2kube completion fish | source 12 | 13 | To load completions for every new session, execute once: 14 | 15 | git2kube completion fish > ~/.config/fish/completions/git2kube.fish 16 | 17 | You will need to start a new shell for this setup to take effect. 18 | 19 | 20 | ``` 21 | git2kube completion fish [flags] 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | -h, --help help for fish 28 | --no-descriptions disable completion descriptions 29 | ``` 30 | 31 | ### Options inherited from parent commands 32 | 33 | ``` 34 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 35 | ``` 36 | 37 | ### SEE ALSO 38 | 39 | * [git2kube completion](git2kube_completion.md) - Generate the autocompletion script for the specified shell 40 | 41 | -------------------------------------------------------------------------------- /docs/git2kube_completion_powershell.md: -------------------------------------------------------------------------------- 1 | ## git2kube completion powershell 2 | 3 | Generate the autocompletion script for powershell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for powershell. 8 | 9 | To load completions in your current shell session: 10 | 11 | git2kube completion powershell | Out-String | Invoke-Expression 12 | 13 | To load completions for every new session, add the output of the above command 14 | to your powershell profile. 15 | 16 | 17 | ``` 18 | git2kube completion powershell [flags] 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | -h, --help help for powershell 25 | --no-descriptions disable completion descriptions 26 | ``` 27 | 28 | ### Options inherited from parent commands 29 | 30 | ``` 31 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 32 | ``` 33 | 34 | ### SEE ALSO 35 | 36 | * [git2kube completion](git2kube_completion.md) - Generate the autocompletion script for the specified shell 37 | 38 | -------------------------------------------------------------------------------- /docs/git2kube_completion_zsh.md: -------------------------------------------------------------------------------- 1 | ## git2kube completion zsh 2 | 3 | Generate the autocompletion script for zsh 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the zsh shell. 8 | 9 | If shell completion is not already enabled in your environment you will need 10 | to enable it. You can execute the following once: 11 | 12 | echo "autoload -U compinit; compinit" >> ~/.zshrc 13 | 14 | To load completions in your current shell session: 15 | 16 | source <(git2kube completion zsh) 17 | 18 | To load completions for every new session, execute once: 19 | 20 | #### Linux: 21 | 22 | git2kube completion zsh > "${fpath[1]}/_git2kube" 23 | 24 | #### macOS: 25 | 26 | git2kube completion zsh > $(brew --prefix)/share/zsh/site-functions/_git2kube 27 | 28 | You will need to start a new shell for this setup to take effect. 29 | 30 | 31 | ``` 32 | git2kube completion zsh [flags] 33 | ``` 34 | 35 | ### Options 36 | 37 | ``` 38 | -h, --help help for zsh 39 | --no-descriptions disable completion descriptions 40 | ``` 41 | 42 | ### Options inherited from parent commands 43 | 44 | ``` 45 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 46 | ``` 47 | 48 | ### SEE ALSO 49 | 50 | * [git2kube completion](git2kube_completion.md) - Generate the autocompletion script for the specified shell 51 | 52 | -------------------------------------------------------------------------------- /docs/git2kube_gendoc.md: -------------------------------------------------------------------------------- 1 | ## git2kube gendoc 2 | 3 | Generates documentation for this tool in Markdown format 4 | 5 | ``` 6 | git2kube gendoc [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -d, --destination string destination for documentation 13 | -h, --help help for gendoc 14 | ``` 15 | 16 | ### Options inherited from parent commands 17 | 18 | ``` 19 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 20 | ``` 21 | 22 | ### SEE ALSO 23 | 24 | * [git2kube](git2kube.md) - Git to ConfigMap conversion tool 25 | 26 | -------------------------------------------------------------------------------- /docs/git2kube_load.md: -------------------------------------------------------------------------------- 1 | ## git2kube load 2 | 3 | Loads files from git repository into target 4 | 5 | ### Options 6 | 7 | ``` 8 | -b, --branch string branch name to pull (default "master") 9 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 10 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 11 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 12 | -h, --help help for load 13 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 14 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 15 | ``` 16 | 17 | ### Options inherited from parent commands 18 | 19 | ``` 20 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [git2kube](git2kube.md) - Git to ConfigMap conversion tool 26 | * [git2kube load configmap](git2kube_load_configmap.md) - Loads files from git repository into ConfigMap 27 | * [git2kube load folder](git2kube_load_folder.md) - Loads files from git repository into Folder 28 | * [git2kube load secret](git2kube_load_secret.md) - Loads files from git repository into Secret 29 | 30 | -------------------------------------------------------------------------------- /docs/git2kube_load_configmap.md: -------------------------------------------------------------------------------- 1 | ## git2kube load configmap 2 | 3 | Loads files from git repository into ConfigMap 4 | 5 | ``` 6 | git2kube load configmap [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --annotation strings annotation to add to K8s ConfigMap (format NAME=VALUE) 13 | -m, --configmap string name for the resulting ConfigMap 14 | -h, --help help for configmap 15 | -k, --kubeconfig true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false) 16 | --label strings label to add to K8s ConfigMap (format NAME=VALUE) 17 | --merge-type string how to merge ConfigMap data whether to also delete missing values or just upsert new (options: delete|upsert) (default "delete") 18 | -n, --namespace string target namespace for the resulting ConfigMap (default "default") 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | -b, --branch string branch name to pull (default "master") 25 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 26 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 27 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 28 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 29 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 30 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 31 | ``` 32 | 33 | ### SEE ALSO 34 | 35 | * [git2kube load](git2kube_load.md) - Loads files from git repository into target 36 | 37 | -------------------------------------------------------------------------------- /docs/git2kube_load_folder.md: -------------------------------------------------------------------------------- 1 | ## git2kube load folder 2 | 3 | Loads files from git repository into Folder 4 | 5 | ``` 6 | git2kube load folder [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for folder 13 | -t, --target-folder string path to target folder 14 | ``` 15 | 16 | ### Options inherited from parent commands 17 | 18 | ``` 19 | -b, --branch string branch name to pull (default "master") 20 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 21 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 22 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 23 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 24 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 25 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [git2kube load](git2kube_load.md) - Loads files from git repository into target 31 | 32 | -------------------------------------------------------------------------------- /docs/git2kube_load_secret.md: -------------------------------------------------------------------------------- 1 | ## git2kube load secret 2 | 3 | Loads files from git repository into Secret 4 | 5 | ``` 6 | git2kube load secret [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --annotation strings annotation to add to K8s Secret (format NAME=VALUE) 13 | -h, --help help for secret 14 | -k, --kubeconfig true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false) 15 | --label strings label to add to K8s Secret (format NAME=VALUE) 16 | --merge-type string how to merge Secret data whether to also delete missing values or just upsert new (options: delete|upsert) (default "delete") 17 | -n, --namespace string target namespace for the resulting ConfigMap (default "default") 18 | -s, --secret string name for the resulting Secret 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | -b, --branch string branch name to pull (default "master") 25 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 26 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 27 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 28 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 29 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 30 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 31 | ``` 32 | 33 | ### SEE ALSO 34 | 35 | * [git2kube load](git2kube_load.md) - Loads files from git repository into target 36 | 37 | -------------------------------------------------------------------------------- /docs/git2kube_version.md: -------------------------------------------------------------------------------- 1 | ## git2kube version 2 | 3 | Print the version information 4 | 5 | ``` 6 | git2kube version [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for version 13 | ``` 14 | 15 | ### Options inherited from parent commands 16 | 17 | ``` 18 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 19 | ``` 20 | 21 | ### SEE ALSO 22 | 23 | * [git2kube](git2kube.md) - Git to ConfigMap conversion tool 24 | 25 | -------------------------------------------------------------------------------- /docs/git2kube_watch.md: -------------------------------------------------------------------------------- 1 | ## git2kube watch 2 | 3 | Runs watcher that periodically check the provided repository 4 | 5 | ### Options 6 | 7 | ``` 8 | -b, --branch string branch name to pull (default "master") 9 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 10 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 11 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 12 | --healthcheck-file string path to file where each refresh writes if it was successful or not, useful for K8s liveness/readiness probe 13 | -h, --help help for watch 14 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 15 | -i, --interval int interval in seconds in which to try refreshing ConfigMap from git (default 10) 16 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [git2kube](git2kube.md) - Git to ConfigMap conversion tool 28 | * [git2kube watch configmap](git2kube_watch_configmap.md) - Runs watcher that periodically check the provided repository and updates K8s ConfigMap accordingly 29 | * [git2kube watch folder](git2kube_watch_folder.md) - Runs watcher that periodically check the provided repository and updates target folder accordingly 30 | * [git2kube watch secret](git2kube_watch_secret.md) - Runs watcher that periodically check the provided repository and updates K8s Secret accordingly 31 | 32 | -------------------------------------------------------------------------------- /docs/git2kube_watch_configmap.md: -------------------------------------------------------------------------------- 1 | ## git2kube watch configmap 2 | 3 | Runs watcher that periodically check the provided repository and updates K8s ConfigMap accordingly 4 | 5 | ``` 6 | git2kube watch configmap [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --annotation strings annotation to add to K8s ConfigMap (format NAME=VALUE) 13 | -m, --configmap string name for the resulting ConfigMap 14 | -h, --help help for configmap 15 | -k, --kubeconfig true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false) 16 | --label strings label to add to K8s ConfigMap (format NAME=VALUE) 17 | --merge-type string how to merge ConfigMap data whether to also delete missing values or just upsert new (options: delete|upsert) (default "delete") 18 | -n, --namespace string target namespace for the resulting ConfigMap (default "default") 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | -b, --branch string branch name to pull (default "master") 25 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 26 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 27 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 28 | --healthcheck-file string path to file where each refresh writes if it was successful or not, useful for K8s liveness/readiness probe 29 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 30 | -i, --interval int interval in seconds in which to try refreshing ConfigMap from git (default 10) 31 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 32 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 33 | ``` 34 | 35 | ### SEE ALSO 36 | 37 | * [git2kube watch](git2kube_watch.md) - Runs watcher that periodically check the provided repository 38 | 39 | -------------------------------------------------------------------------------- /docs/git2kube_watch_folder.md: -------------------------------------------------------------------------------- 1 | ## git2kube watch folder 2 | 3 | Runs watcher that periodically check the provided repository and updates target folder accordingly 4 | 5 | ``` 6 | git2kube watch folder [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for folder 13 | -t, --target-folder string path to target folder 14 | ``` 15 | 16 | ### Options inherited from parent commands 17 | 18 | ``` 19 | -b, --branch string branch name to pull (default "master") 20 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 21 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 22 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 23 | --healthcheck-file string path to file where each refresh writes if it was successful or not, useful for K8s liveness/readiness probe 24 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 25 | -i, --interval int interval in seconds in which to try refreshing ConfigMap from git (default 10) 26 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 27 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 28 | ``` 29 | 30 | ### SEE ALSO 31 | 32 | * [git2kube watch](git2kube_watch.md) - Runs watcher that periodically check the provided repository 33 | 34 | -------------------------------------------------------------------------------- /docs/git2kube_watch_secret.md: -------------------------------------------------------------------------------- 1 | ## git2kube watch secret 2 | 3 | Runs watcher that periodically check the provided repository and updates K8s Secret accordingly 4 | 5 | ``` 6 | git2kube watch secret [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --annotation strings annotation to add to K8s Secret (format NAME=VALUE) 13 | -h, --help help for secret 14 | -k, --kubeconfig true if locally stored ~/.kube/config should be used, InCluster config will be used if false (options: true|false) (default: false) 15 | --label strings label to add to K8s Secret (format NAME=VALUE) 16 | --merge-type string how to merge Secret data whether to also delete missing values or just upsert new (options: delete|upsert) (default "delete") 17 | -n, --namespace string target namespace for the resulting ConfigMap (default "default") 18 | -s, --secret string name for the resulting Secret 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | -b, --branch string branch name to pull (default "master") 25 | -c, --cache-folder string destination on filesystem where cache of repository will be stored (default "/tmp/git2kube/data/") 26 | --exclude strings regex that if is a match excludes the file from the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [^\..*]) 27 | -g, --git string git repository address, either http(s) or ssh protocol has to be specified 28 | --healthcheck-file string path to file where each refresh writes if it was successful or not, useful for K8s liveness/readiness probe 29 | --include strings regex that if is a match includes the file in the upload, example: '*.yaml' or 'folder/*' if you want to match a folder (default [.*]) 30 | -i, --interval int interval in seconds in which to try refreshing ConfigMap from git (default 10) 31 | -l, --log-level string command log level (options: [panic fatal error warning info debug trace]) (default "info") 32 | -p, --ssh-key string path to the SSH private key (git repository address should be 'git@
', example: git@github.com:wandera/git2kube.git) 33 | ``` 34 | 35 | ### SEE ALSO 36 | 37 | * [git2kube watch](git2kube_watch.md) - Runs watcher that periodically check the provided repository 38 | 39 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## CronJob 2 | * [cronjob.yaml](cronjob.yaml) 3 | * Deploy git2kube as a Kubernetes CronJob 4 | * Synchronise with Kubernetes ConfigMap or Secret 5 | * Suitable for longer refresh intervals 6 | * Might be harder to monitor 7 | * Updates might have higher latency due to scheduling 8 | * Low resource requirements 9 | 10 | ## Watcher 11 | * [watcher.yaml](watcher.yaml) 12 | * Deploy git2kube as a Kubernetes Deployment 13 | * Synchronise with Kubernetes ConfigMap or Secret 14 | * Suitable for short refresh intervals 15 | * Easier to monitor 16 | * Low latency updates 17 | * Low resource requirements 18 | 19 | ## Sidecar 20 | * [sidecar.yaml](sidecar.yaml) 21 | * Deploy git2kube as part of different application Pod 22 | * Synchronise with application by using shared volume 23 | * Suitable for short refresh intervals 24 | * Easier to monitor 25 | * Low latency updates 26 | * Bigger resource requirements -------------------------------------------------------------------------------- /example/cronjob.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: git2kube-watcher 6 | namespace: config 7 | labels: 8 | app: git2kube-watcher 9 | --- 10 | apiVersion: rbac.authorization.k8s.io/v1 11 | kind: ClusterRole 12 | metadata: 13 | name: git2kube-watcher 14 | labels: 15 | app: git2kube-watcher 16 | rules: 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - namespaces 21 | - configmaps 22 | verbs: 23 | - get 24 | - list 25 | - watch 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - configmaps 30 | verbs: 31 | - create 32 | - update 33 | - patch 34 | --- 35 | kind: ClusterRoleBinding 36 | apiVersion: rbac.authorization.k8s.io/v1 37 | metadata: 38 | name: git2kube-watcher 39 | labels: 40 | app: git2kube-watcher 41 | roleRef: 42 | apiGroup: rbac.authorization.k8s.io 43 | kind: ClusterRole 44 | name: git2kube-watcher 45 | subjects: 46 | - kind: ServiceAccount 47 | name: git2kube-watcher 48 | namespace: config 49 | --- 50 | apiVersion: batch/v1beta1 51 | kind: CronJob 52 | metadata: 53 | name: git2kube 54 | namespace: config 55 | labels: 56 | app: git2kube 57 | spec: 58 | schedule: "*/5 * * * *" 59 | jobTemplate: 60 | metadata: 61 | labels: 62 | app: git2kube 63 | spec: 64 | template: 65 | metadata: 66 | labels: 67 | app: git2kube 68 | spec: 69 | serviceAccountName: git2kube-watcher 70 | containers: 71 | - name: git2kube-prometheus-rules 72 | image: ghcr.io/wandera/git2kube 73 | args: 74 | - 'load' 75 | - 'configmap' 76 | - '--configmap=alert-rules' 77 | - '--namespace=monitoring' 78 | - '--branch=master' 79 | - '--git=https://github.com/prometheus/mysqld_exporter.git' 80 | - '--include=.*\.rules' 81 | - '--label=prometheus=k8s' 82 | - '--label=role=alert-rules' 83 | resources: 84 | requests: 85 | cpu: 100m 86 | memory: 32Mi 87 | limits: 88 | cpu: 200m 89 | memory: 64Mi 90 | -------------------------------------------------------------------------------- /example/sidecar.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: git2kube-watcher 6 | namespace: config 7 | labels: 8 | app: git2kube-watcher 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: git2kube-watcher 14 | template: 15 | metadata: 16 | labels: 17 | app: git2kube-watcher 18 | spec: 19 | containers: 20 | - name: busybox 21 | image: busybox 22 | command: 23 | - watch 24 | - cat 25 | - "/rules/example.rules" 26 | volumeMounts: 27 | - mountPath: /rules 28 | name: rules 29 | readOnly: true 30 | - name: git2kube-prometheus-rules 31 | image: ghcr.io/wandera/git2kube 32 | args: 33 | - 'watch' 34 | - 'folder' 35 | - '--healthcheck-file=/tmp/health' 36 | - '--branch=master' 37 | - '--git=https://github.com/prometheus/mysqld_exporter.git' 38 | - '--include=.*\.rules' 39 | - '--interval=30' 40 | - '--target-folder=/rules' 41 | livenessProbe: 42 | exec: 43 | command: 44 | - /bin/sh 45 | - -c 46 | - "grep -Fxq OK /tmp/health" 47 | initialDelaySeconds: 10 48 | periodSeconds: 35 49 | failureThreshold: 3 50 | readinessProbe: 51 | exec: 52 | command: 53 | - /bin/sh 54 | - -c 55 | - "grep -Fxq OK /tmp/health" 56 | initialDelaySeconds: 10 57 | periodSeconds: 10 58 | resources: 59 | requests: 60 | cpu: 100m 61 | memory: 32Mi 62 | limits: 63 | cpu: 200m 64 | memory: 64Mi 65 | volumeMounts: 66 | - mountPath: /rules 67 | name: rules 68 | volumes: 69 | - name: rules 70 | emptyDir: {} 71 | -------------------------------------------------------------------------------- /example/watcher.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: git2kube-watcher 6 | namespace: config 7 | labels: 8 | app: git2kube-watcher 9 | --- 10 | apiVersion: rbac.authorization.k8s.io/v1 11 | kind: ClusterRole 12 | metadata: 13 | name: git2kube-watcher 14 | labels: 15 | app: git2kube-watcher 16 | rules: 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - namespaces 21 | - configmaps 22 | verbs: 23 | - get 24 | - list 25 | - watch 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - configmaps 30 | verbs: 31 | - create 32 | - update 33 | - patch 34 | --- 35 | kind: ClusterRoleBinding 36 | apiVersion: rbac.authorization.k8s.io/v1 37 | metadata: 38 | name: git2kube-watcher 39 | labels: 40 | app: git2kube-watcher 41 | roleRef: 42 | apiGroup: rbac.authorization.k8s.io 43 | kind: ClusterRole 44 | name: git2kube-watcher 45 | subjects: 46 | - kind: ServiceAccount 47 | name: git2kube-watcher 48 | namespace: config 49 | --- 50 | apiVersion: apps/v1 51 | kind: Deployment 52 | metadata: 53 | name: git2kube-watcher 54 | namespace: config 55 | labels: 56 | app: git2kube-watcher 57 | spec: 58 | replicas: 1 59 | selector: 60 | matchLabels: 61 | app: git2kube-watcher 62 | template: 63 | metadata: 64 | labels: 65 | app: git2kube-watcher 66 | spec: 67 | serviceAccountName: git2kube-watcher 68 | containers: 69 | - name: git2kube-prometheus-rules 70 | image: ghcr.io/wandera/git2kube 71 | args: 72 | - 'watch' 73 | - 'configmap' 74 | - '--configmap=alert-rules' 75 | - '--namespace=monitoring' 76 | - '--healthcheck-file=/tmp/health' 77 | - '--branch=master' 78 | - '--git=https://github.com/prometheus/mysqld_exporter.git' 79 | - '--include=.*\.rules' 80 | - '--interval=30' 81 | - '--label=prometheus=k8s' 82 | - '--label=role=alert-rules' 83 | livenessProbe: 84 | exec: 85 | command: 86 | - /bin/sh 87 | - -c 88 | - "grep -Fxq OK /tmp/health" 89 | initialDelaySeconds: 10 90 | periodSeconds: 35 91 | failureThreshold: 3 92 | readinessProbe: 93 | exec: 94 | command: 95 | - /bin/sh 96 | - -c 97 | - "grep -Fxq OK /tmp/health" 98 | initialDelaySeconds: 10 99 | periodSeconds: 10 100 | resources: 101 | requests: 102 | cpu: 100m 103 | memory: 32Mi 104 | limits: 105 | cpu: 200m 106 | memory: 64Mi 107 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wandera/git2kube 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | dario.cat/mergo v1.0.1 7 | github.com/go-git/go-git/v5 v5.14.0 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/spf13/cobra v1.8.1 10 | golang.org/x/crypto v0.37.0 11 | k8s.io/api v0.32.3 12 | k8s.io/apimachinery v0.32.3 13 | k8s.io/client-go v0.32.3 14 | ) 15 | 16 | require ( 17 | github.com/Microsoft/go-winio v0.6.2 // indirect 18 | github.com/ProtonMail/go-crypto v1.1.5 // indirect 19 | github.com/cloudflare/circl v1.6.0 // indirect 20 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 21 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 23 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 24 | github.com/emirpasic/gods v1.18.1 // indirect 25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 27 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 28 | github.com/go-logr/logr v1.4.2 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/jsonreference v0.21.0 // indirect 31 | github.com/go-openapi/swag v0.23.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 34 | github.com/golang/protobuf v1.5.4 // indirect 35 | github.com/google/gnostic-models v0.6.9 // indirect 36 | github.com/google/go-cmp v0.7.0 // indirect 37 | github.com/google/gofuzz v1.2.0 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 40 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/json-iterator/go v1.1.12 // indirect 43 | github.com/kevinburke/ssh_config v1.2.0 // indirect 44 | github.com/mailru/easyjson v0.9.0 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/pjbgf/sha1cd v0.3.2 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 51 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 52 | github.com/skeema/knownhosts v1.3.1 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/x448/float16 v0.8.4 // indirect 55 | github.com/xanzy/ssh-agent v0.3.3 // indirect 56 | golang.org/x/net v0.38.0 // indirect 57 | golang.org/x/oauth2 v0.24.0 // indirect 58 | golang.org/x/sys v0.32.0 // indirect 59 | golang.org/x/term v0.31.0 // indirect 60 | golang.org/x/text v0.24.0 // indirect 61 | golang.org/x/time v0.8.0 // indirect 62 | golang.org/x/tools v0.28.0 // indirect 63 | google.golang.org/protobuf v1.36.0 // indirect 64 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/warnings.v0 v0.1.2 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/klog/v2 v2.130.1 // indirect 69 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect 70 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 71 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 72 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect 73 | sigs.k8s.io/yaml v1.4.0 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 | github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= 7 | github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= 13 | github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 17 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 18 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 24 | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 25 | github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= 26 | github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 27 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 28 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 29 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 30 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 31 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 32 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 33 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 35 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 36 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 37 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 39 | github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= 40 | github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 41 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 42 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 43 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 44 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 45 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 46 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 47 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 48 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 49 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 50 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 51 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 52 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 53 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 54 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 55 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 56 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 57 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 58 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 61 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 62 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 63 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 64 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 65 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 66 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 67 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 68 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 69 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 70 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 71 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 72 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 73 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 74 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 75 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 76 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 77 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 78 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 79 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 80 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 81 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 82 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 83 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 84 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 85 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 86 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 87 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 88 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 89 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 90 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 94 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 95 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 97 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 98 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 99 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 100 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 101 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 102 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 103 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 104 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 105 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 106 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 107 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 108 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 109 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 110 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 111 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 112 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 113 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 114 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 115 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 116 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 117 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 118 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 119 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 120 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 121 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 122 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 123 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 124 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 125 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 126 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 127 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 128 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 129 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 130 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 131 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 132 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 133 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 134 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 135 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 136 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 137 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 138 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 139 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 140 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 141 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 142 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 143 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 144 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 145 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 146 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 147 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 148 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 149 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 150 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 151 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 152 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 153 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 154 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 155 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 157 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 165 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 166 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 167 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 168 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 169 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 170 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 171 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 172 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 173 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 174 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 175 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 176 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 177 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 178 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 179 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 180 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 181 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 182 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 183 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= 188 | google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 189 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 190 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 192 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 193 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 194 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 195 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 196 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 197 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 198 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 199 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 201 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 203 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 205 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 206 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 207 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 208 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 209 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 210 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 211 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 212 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= 213 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= 214 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 215 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 216 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 217 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 218 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= 219 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 220 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 221 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 222 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/wandera/git2kube/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.Execute(); err != nil { 12 | log.Errorf("Command failed: %v", err) 13 | os.Exit(-1) 14 | } 15 | } 16 | 17 | func init() { 18 | log.SetFormatter(&log.TextFormatter{}) 19 | log.SetOutput(os.Stdout) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/cmd/common.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // ExpandArgs expands environment variables in a slice of arguments for a cmd. 10 | func ExpandArgs(cmd *cobra.Command, args []string) error { 11 | for i, arg := range args { 12 | args[i] = os.ExpandEnv(arg) 13 | } 14 | return cmd.Flags().Parse(args) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/cmd/common_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func TestExpandArgs(t *testing.T) { 12 | cases := []struct { 13 | name string 14 | args []string 15 | env map[string]string 16 | result []string 17 | }{ 18 | { 19 | name: "No Env", 20 | args: []string{ 21 | "arg1", 22 | }, 23 | result: []string{ 24 | "arg1", 25 | }, 26 | }, 27 | { 28 | name: "Simple Env", 29 | args: []string{ 30 | "$ENV", 31 | }, 32 | env: map[string]string{ 33 | "ENV": "test", 34 | }, 35 | result: []string{ 36 | "test", 37 | }, 38 | }, 39 | { 40 | name: "Simple Env Multiple", 41 | args: []string{ 42 | "$ENV", 43 | "${ENV}", 44 | "$ENV", 45 | }, 46 | env: map[string]string{ 47 | "ENV": "test", 48 | }, 49 | result: []string{ 50 | "test", 51 | "test", 52 | "test", 53 | }, 54 | }, 55 | { 56 | name: "Interpolation", 57 | args: []string{ 58 | "This is $ENV property", 59 | "This is ${ENV} property", 60 | "This is $ENV property", 61 | }, 62 | env: map[string]string{ 63 | "ENV": "test", 64 | }, 65 | result: []string{ 66 | "This is test property", 67 | "This is test property", 68 | "This is test property", 69 | }, 70 | }, 71 | { 72 | name: "Multiple Env", 73 | args: []string{ 74 | "This is $ENV property $ENV2", 75 | }, 76 | env: map[string]string{ 77 | "ENV": "test", 78 | "ENV2": "test2", 79 | }, 80 | result: []string{ 81 | "This is test property test2", 82 | }, 83 | }, 84 | } 85 | 86 | for _, c := range cases { 87 | setEnvFromMap(c.env) 88 | command := &cobra.Command{} 89 | ExpandArgs(command, c.args) 90 | res := command.Flags().Args() 91 | if !reflect.DeepEqual(res, c.result) { 92 | t.Errorf("%s case failed: result args mismatch expected %s but got %s instead", c.name, c.result, res) 93 | } 94 | unsetEnvFromMap(c.env) 95 | } 96 | } 97 | 98 | func setEnvFromMap(env map[string]string) { 99 | for k, v := range env { 100 | os.Setenv(k, v) 101 | } 102 | } 103 | 104 | func unsetEnvFromMap(env map[string]string) { 105 | for k := range env { 106 | os.Unsetenv(k) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/fetch/fetch.go: -------------------------------------------------------------------------------- 1 | package fetch 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/go-git/go-git/v5" 11 | "github.com/go-git/go-git/v5/plumbing" 12 | "github.com/go-git/go-git/v5/plumbing/object" 13 | "github.com/go-git/go-git/v5/plumbing/transport" 14 | githttp "github.com/go-git/go-git/v5/plumbing/transport/http" 15 | gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" 16 | log "github.com/sirupsen/logrus" 17 | "golang.org/x/crypto/ssh" 18 | ) 19 | 20 | // Fetcher fetching data from remote. 21 | type Fetcher interface { 22 | Fetch() (*object.Commit, error) 23 | } 24 | 25 | type fetcher struct { 26 | url string 27 | directory string 28 | branch string 29 | auth transport.AuthMethod 30 | } 31 | 32 | // NewFetcher creates new Fetcher. 33 | func NewFetcher(url string, directory string, branch string, auth transport.AuthMethod) Fetcher { 34 | fetcher := &fetcher{ 35 | url: url, 36 | directory: directory, 37 | branch: branch, 38 | auth: auth, 39 | } 40 | return fetcher 41 | } 42 | 43 | // Fetch from remote. 44 | func (f *fetcher) Fetch() (*object.Commit, error) { 45 | err := os.RemoveAll(f.directory) 46 | if err != nil { 47 | log.Errorf("Failed to clean the folder '%s': %v", f.branch, err) 48 | return nil, err 49 | } 50 | 51 | log.Infof("Cloning repository in folder '%s' ...", f.directory) 52 | r, err := git.PlainClone(f.directory, false, &git.CloneOptions{ 53 | URL: f.url, 54 | Auth: f.auth, 55 | Depth: 1, 56 | ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", f.branch)), 57 | }) 58 | if err != nil { 59 | log.Errorf("Failed to clone '%s': %v", f.branch, err) 60 | return nil, err 61 | } 62 | 63 | ref, err := r.Head() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | commit, err := r.CommitObject(ref.Hash()) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | log.Infof("HEAD ref hash '%s'", ref.Hash()) 74 | 75 | return commit, nil 76 | } 77 | 78 | // NewAuth creates new AuthMethod based on URI. 79 | func NewAuth(git string, sshkey string) (transport.AuthMethod, error) { 80 | var auth transport.AuthMethod 81 | 82 | ep, err := transport.NewEndpoint(git) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | if strings.HasPrefix(ep.Protocol, "ssh") && sshkey != "" { 88 | var signer ssh.Signer 89 | sshFile, err := os.Open(sshkey) // #nosec G304 90 | if err != nil { 91 | return nil, errors.New("Couldn't open SSH key: " + err.Error()) 92 | } 93 | sshB, err := io.ReadAll(sshFile) 94 | if err != nil { 95 | return nil, errors.New("Couldn't read SSH key: " + err.Error()) 96 | } 97 | 98 | signer, err = ssh.ParsePrivateKey(sshB) 99 | if err != nil { 100 | return nil, errors.New("Couldn't parse SSH key: " + err.Error()) 101 | } 102 | 103 | sshAuth := &gitssh.PublicKeys{User: "git", Signer: signer} 104 | return sshAuth, nil 105 | } 106 | 107 | if strings.HasPrefix(ep.Protocol, "http") && ep.User != "" && ep.Password != "" { 108 | auth = &githttp.BasicAuth{ 109 | Username: ep.User, 110 | Password: ep.Password, 111 | } 112 | } 113 | 114 | return auth, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/fetch/fetch_test.go: -------------------------------------------------------------------------------- 1 | package fetch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-git/go-git/v5/plumbing/transport" 7 | "github.com/go-git/go-git/v5/plumbing/transport/http" 8 | gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" 9 | ) 10 | 11 | func TestNewAuth(t *testing.T) { 12 | cases := []struct { 13 | name string 14 | git string 15 | key string 16 | result transport.AuthMethod 17 | }{ 18 | { 19 | name: "No Auth", 20 | git: "https://github.com/wandera/git2kube.git", 21 | result: nil, 22 | }, 23 | { 24 | name: "Basic Auth", 25 | git: "https://test:testpass@github.com/wandera/git2kube.git", 26 | result: &http.BasicAuth{ 27 | Username: "test", 28 | Password: "testpass", 29 | }, 30 | }, 31 | { 32 | name: "SSH wit private key", 33 | git: "git@github.com:wandera/git2kube.git", 34 | key: "testdata/dummy.key", 35 | result: &gitssh.PublicKeys{ 36 | User: "git", 37 | Signer: nil, 38 | }, 39 | }, 40 | { 41 | name: "HTTP url with private key", 42 | git: "https://github.com/wandera/git2kube.git", 43 | key: "/tmp/i_am_not_here.key", 44 | result: nil, 45 | }, 46 | { 47 | name: "SSH url without private key", 48 | git: "git@github.com:wandera/git2kube.git", 49 | result: nil, 50 | }, 51 | } 52 | 53 | for _, c := range cases { 54 | m, err := NewAuth(c.git, c.key) 55 | if err != nil { 56 | t.Errorf("%s case failed: %s", c.name, err) 57 | } 58 | 59 | if m == nil && c.result != nil { 60 | t.Errorf("%s case failed: result should have been %s but got nil instead", c.name, c.result) 61 | } else if m != nil && c.result == nil { 62 | t.Errorf("%s case failed: result should have been nil but got %s instead", c.name, c.result) 63 | } 64 | 65 | switch m.(type) { 66 | case *http.BasicAuth: 67 | g := m.(*http.BasicAuth) 68 | w := c.result.(*http.BasicAuth) 69 | if g.Username != w.Username || g.Password != w.Password { 70 | t.Errorf("%s case failed: result mismatch expected %s but got %s instead", c.name, c.result, m) 71 | } 72 | case *gitssh.PublicKeys: 73 | g := m.(*gitssh.PublicKeys) 74 | w := c.result.(*gitssh.PublicKeys) 75 | if g.User != w.User { 76 | t.Errorf("%s case failed: result mismatch expected %s but got %s instead", c.name, c.result, m) 77 | } else if g.Signer == nil { 78 | t.Errorf("%s case failed: result mismatch expected Signer but got nil instead", c.name) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/fetch/testdata/dummy.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWgIBAAKBgHyuRtf9CGOoCQ5gjFwnE8Ff6+ZKrdWFoTeCcHp6BVBPwuQa4EjV 3 | kNQQ+4LW0SmZALk+f3cJE0qaoGrZLwp5/ODANSPOuVHE+S5kDmZ3ENLrmYx9VNTH 4 | 4NekZq7heMPxZu6j3KSsag1F/+sTw2T/sC0NQxP7Q3VZoD2nJpTrCKTJAgMBAAEC 5 | gYBl4jv978VVOlD8MKVCAKTCFlI8w7W9kA8nvy2Ox/tjn2oQ4G161xocUSxVxJjz 6 | NxZCSfH7Ia1j/2srJQKtuvWKv8dJheN8eYXPtzrfhpYJ7lLzkC8+sLLl4ZEQR7E+ 7 | EFnSGBxHQwWLxlTZtziD3RIU8XynUrrDDIrhuboZY/SP6QJBAOAIHum21fKeld1Z 8 | j8CRCiYjecQdXq7GouIfMYPvgzOJiiaSF4W7nFClqwHK0V2OLXboY2SmE36E6h5Q 9 | chOpvQcCQQCOeN2Oqh1g+agTeLgTAhJO++aYvGeYWpVWFrj9gPcUlB6B3m9wykSU 10 | VIX5z4dNZhtfPX91zYx/sKsw+hGG7euvAkB/HBxt/o7lWZUuQeKOH7ziZr5vxzox 11 | cAJ6ybgOY5bNZvw7ZihyeD/4ggbEvY6Mxl6FWNUMBi6JWmB7Uqlm/qeBAkBSF26B 12 | /59B+1hPL+XQfgcSn661HwoKDVey7RDWcPLtVpNlfUd1E/cECNfRozRw8n6fGA5T 13 | UIa+JIvfgpeKyZhdAkAnpFqhWl7V3c0PZVuCzHKFRj2LKYlSQ+3t4x+7cE5NKwW/ 14 | cKIiKDBM+rGmnZjtir/8OL/myFbc+qHgSrAvxbbf 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /pkg/upload/testdata/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": 1 3 | } -------------------------------------------------------------------------------- /pkg/upload/testdata/test.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | some: 1 -------------------------------------------------------------------------------- /pkg/upload/upload.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "dario.cat/mergo" 16 | "github.com/go-git/go-git/v5/plumbing/object" 17 | log "github.com/sirupsen/logrus" 18 | corev1 "k8s.io/api/core/v1" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/types" 21 | "k8s.io/apimachinery/pkg/util/strategicpatch" 22 | "k8s.io/client-go/kubernetes" 23 | typedcore "k8s.io/client-go/kubernetes/typed/core/v1" 24 | "k8s.io/client-go/rest" 25 | "k8s.io/client-go/tools/clientcmd" 26 | ) 27 | 28 | const ( 29 | refAnnotation = "git2kube.github.com/ref" 30 | bufferSize = 1024 31 | ) 32 | 33 | // LoadType upload type. 34 | type LoadType int 35 | 36 | // MergeType how to merge ConfigMap data. 37 | type MergeType string 38 | 39 | const ( 40 | // Delete merge all keys (files) including removal of missing keys. 41 | Delete MergeType = "delete" 42 | // Upsert merge all keys (files) but don't remove missing keys from the repository. 43 | Upsert = "upsert" 44 | ) 45 | 46 | // LoadType options enum. 47 | const ( 48 | ConfigMap LoadType = iota 49 | Secret 50 | Folder 51 | ) 52 | 53 | // FileIter provides an iterator for the files in a tree. 54 | type FileIter interface { 55 | ForEach(cb func(*object.File) error) error 56 | } 57 | 58 | // UploaderFactory factory constructing Uploaders. 59 | type UploaderFactory func(o UploaderOptions) (Uploader, error) 60 | 61 | // Uploader uploading data to target. 62 | type Uploader interface { 63 | // Upload files into config map tagged by commitID 64 | Upload(commitID string, iter FileIter) error 65 | } 66 | 67 | type uploader struct { 68 | restconfig *rest.Config 69 | clientset kubernetes.Interface 70 | namespace string 71 | name string 72 | mergeType MergeType 73 | labels map[string]string 74 | annotations map[string]string 75 | includes []*regexp.Regexp 76 | excludes []*regexp.Regexp 77 | } 78 | 79 | type configmapUploader uploader 80 | 81 | type secretUploader uploader 82 | 83 | type folderUploader struct { 84 | name string 85 | includes []*regexp.Regexp 86 | excludes []*regexp.Regexp 87 | sourcePath string 88 | } 89 | 90 | // UploaderOptions uploader options. 91 | type UploaderOptions struct { 92 | Kubeconfig bool 93 | Source string 94 | Target string 95 | Namespace string 96 | MergeType MergeType 97 | Includes []string 98 | Excludes []string 99 | Labels []string 100 | Annotations []string 101 | } 102 | 103 | var uploaderFactories = make(map[LoadType]UploaderFactory) 104 | 105 | func register(loadType LoadType, factory UploaderFactory) { 106 | _, registered := uploaderFactories[loadType] 107 | if registered { 108 | log.Errorf("Uploader factory %d already registered. Ignoring.", loadType) 109 | } 110 | uploaderFactories[loadType] = factory 111 | } 112 | 113 | // NewUploader create uploader of specific type. 114 | func NewUploader(lt LoadType, o UploaderOptions) (Uploader, error) { 115 | engineFactory, ok := uploaderFactories[lt] 116 | if !ok { 117 | return nil, errors.New("invalid uploader name") 118 | } 119 | 120 | // Run the factory with the configuration. 121 | return engineFactory(o) 122 | } 123 | 124 | func newConfigMapUploader(o UploaderOptions) (Uploader, error) { 125 | restconfig, err := restConfig(o.Kubeconfig) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | clientset, err := kubernetes.NewForConfig(restconfig) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | includesRegex, err := stringsToRegExp(o.Includes) 136 | if err != nil { 137 | return nil, err 138 | } 139 | log.Infof("Loaded include rules %s", includesRegex) 140 | 141 | excludesRegex, err := stringsToRegExp(o.Excludes) 142 | if err != nil { 143 | return nil, err 144 | } 145 | log.Infof("Loaded exclude rules %s", excludesRegex) 146 | 147 | labelsParsed, err := stringsToMap(o.Labels) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | annotationsParsed, err := stringsToMap(o.Annotations) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return &configmapUploader{ 158 | mergeType: o.MergeType, 159 | includes: includesRegex, 160 | excludes: excludesRegex, 161 | labels: labelsParsed, 162 | annotations: annotationsParsed, 163 | restconfig: restconfig, 164 | clientset: clientset, 165 | namespace: o.Namespace, 166 | name: o.Target, 167 | }, nil 168 | } 169 | 170 | func (u *configmapUploader) Upload(commitID string, iter FileIter) error { 171 | configMaps := u.clientset.CoreV1().ConfigMaps(u.namespace) 172 | 173 | data, err := u.iterToConfigMapData(iter) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | oldMap, err := configMaps.Get(context.TODO(), u.name, metav1.GetOptions{}) 179 | if err == nil { 180 | err = u.patchConfigMap(oldMap, configMaps, data, commitID) 181 | if err != nil { 182 | return err 183 | } 184 | } else { 185 | err = u.createConfigMap(configMaps, data, commitID) 186 | if err != nil { 187 | return err 188 | } 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (u *configmapUploader) patchConfigMap(oldMap *corev1.ConfigMap, configMaps typedcore.ConfigMapInterface, data map[string]string, commitID string) error { 195 | log.Infof("Patching ConfigMap '%s.%s'", oldMap.Namespace, oldMap.Name) 196 | newMap := oldMap.DeepCopy() 197 | 198 | switch u.mergeType { 199 | case Delete: 200 | newMap.Data = data 201 | case Upsert: 202 | if err := mergo.Merge(&newMap.Data, data, mergo.WithOverride); err != nil { 203 | if err != nil { 204 | return err 205 | } 206 | } 207 | } 208 | 209 | if err := mergo.Merge(&newMap.Annotations, u.annotations, mergo.WithOverride); err != nil { 210 | if err != nil { 211 | return err 212 | } 213 | } 214 | newMap.Annotations[refAnnotation] = commitID 215 | 216 | if err := mergo.Merge(&newMap.Labels, u.labels, mergo.WithOverride); err != nil { 217 | if err != nil { 218 | return err 219 | } 220 | } 221 | 222 | oldData, err := json.Marshal(oldMap) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | newData, err := json.Marshal(newMap) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, corev1.ConfigMap{}) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | _, err = configMaps.Patch(context.TODO(), u.name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | log.Infof("Successfully patched ConfigMap '%s.%s'", oldMap.Namespace, oldMap.Name) 243 | return nil 244 | } 245 | 246 | func (u *configmapUploader) createConfigMap(configMaps typedcore.ConfigMapInterface, data map[string]string, commitID string) error { 247 | log.Infof("Creating ConfigMap '%s.%s'", u.namespace, u.name) 248 | 249 | annotations := u.annotations 250 | annotations[refAnnotation] = commitID 251 | 252 | _, err := configMaps.Create( 253 | context.TODO(), 254 | &corev1.ConfigMap{ 255 | ObjectMeta: metav1.ObjectMeta{ 256 | Name: u.name, 257 | Namespace: u.namespace, 258 | Annotations: annotations, 259 | Labels: u.labels, 260 | }, 261 | Data: data, 262 | }, 263 | metav1.CreateOptions{}, 264 | ) 265 | if err != nil { 266 | return err 267 | } 268 | 269 | log.Infof("Successfully created ConfigMap '%s.%s'", u.namespace, u.name) 270 | return nil 271 | } 272 | 273 | func (u *configmapUploader) iterToConfigMapData(iter FileIter) (map[string]string, error) { 274 | data := make(map[string]string) 275 | err := iter.ForEach(func(file *object.File) error { 276 | if filterFile(file, u.includes, u.excludes) { 277 | content, err := file.Contents() 278 | if err != nil { 279 | return err 280 | } 281 | data[strings.ReplaceAll(file.Name, "/", ".")] = content 282 | } 283 | return nil 284 | }) 285 | 286 | return data, err 287 | } 288 | 289 | func newSecretUploader(o UploaderOptions) (Uploader, error) { 290 | restconfig, err := restConfig(o.Kubeconfig) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | clientset, err := kubernetes.NewForConfig(restconfig) 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | includesRegex, err := stringsToRegExp(o.Includes) 301 | if err != nil { 302 | return nil, err 303 | } 304 | log.Infof("Loaded include rules %s", includesRegex) 305 | 306 | excludesRegex, err := stringsToRegExp(o.Excludes) 307 | if err != nil { 308 | return nil, err 309 | } 310 | log.Infof("Loaded exclude rules %s", excludesRegex) 311 | 312 | labelsParsed, err := stringsToMap(o.Labels) 313 | if err != nil { 314 | return nil, err 315 | } 316 | 317 | annotationsParsed, err := stringsToMap(o.Annotations) 318 | if err != nil { 319 | return nil, err 320 | } 321 | 322 | return &secretUploader{ 323 | mergeType: o.MergeType, 324 | includes: includesRegex, 325 | excludes: excludesRegex, 326 | labels: labelsParsed, 327 | annotations: annotationsParsed, 328 | restconfig: restconfig, 329 | clientset: clientset, 330 | namespace: o.Namespace, 331 | name: o.Target, 332 | }, nil 333 | } 334 | 335 | func (u *secretUploader) Upload(commitID string, iter FileIter) error { 336 | secrets := u.clientset.CoreV1().Secrets(u.namespace) 337 | 338 | data, err := u.iterToSecretData(iter) 339 | if err != nil { 340 | return err 341 | } 342 | 343 | oldSecret, err := secrets.Get(context.TODO(), u.name, metav1.GetOptions{}) 344 | if err == nil { 345 | err = u.patchSecret(oldSecret, secrets, data, commitID) 346 | if err != nil { 347 | return err 348 | } 349 | } else { 350 | err = u.createSecret(secrets, data, commitID) 351 | if err != nil { 352 | return err 353 | } 354 | } 355 | 356 | return nil 357 | } 358 | 359 | func (u *secretUploader) patchSecret(oldSecret *corev1.Secret, secrets typedcore.SecretInterface, data map[string][]byte, commitID string) error { 360 | log.Infof("Patching Secret '%s.%s'", oldSecret.Namespace, oldSecret.Name) 361 | newSecret := oldSecret.DeepCopy() 362 | 363 | switch u.mergeType { 364 | case Delete: 365 | newSecret.Data = data 366 | case Upsert: 367 | if err := mergo.Merge(&newSecret.Data, data, mergo.WithOverride); err != nil { 368 | if err != nil { 369 | return err 370 | } 371 | } 372 | } 373 | 374 | if err := mergo.Merge(&newSecret.Annotations, u.annotations, mergo.WithOverride); err != nil { 375 | if err != nil { 376 | return err 377 | } 378 | } 379 | newSecret.Annotations[refAnnotation] = commitID 380 | 381 | if err := mergo.Merge(&newSecret.Labels, u.labels, mergo.WithOverride); err != nil { 382 | if err != nil { 383 | return err 384 | } 385 | } 386 | 387 | oldData, err := json.Marshal(oldSecret) 388 | if err != nil { 389 | return err 390 | } 391 | 392 | newData, err := json.Marshal(newSecret) 393 | if err != nil { 394 | return err 395 | } 396 | 397 | patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, corev1.Secret{}) 398 | if err != nil { 399 | return err 400 | } 401 | 402 | _, err = secrets.Patch(context.TODO(), u.name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}) 403 | if err != nil { 404 | return err 405 | } 406 | 407 | log.Infof("Successfully patched ConfigMap '%s.%s'", oldSecret.Namespace, oldSecret.Name) 408 | return nil 409 | } 410 | 411 | func (u *secretUploader) createSecret(secrets typedcore.SecretInterface, data map[string][]byte, commitID string) error { 412 | log.Infof("Creating ConfigMap '%s.%s'", u.namespace, u.name) 413 | 414 | annotations := u.annotations 415 | annotations[refAnnotation] = commitID 416 | 417 | _, err := secrets.Create( 418 | context.TODO(), 419 | &corev1.Secret{ 420 | ObjectMeta: metav1.ObjectMeta{ 421 | Name: u.name, 422 | Namespace: u.namespace, 423 | Annotations: annotations, 424 | Labels: u.labels, 425 | }, 426 | Data: data, 427 | }, 428 | metav1.CreateOptions{}, 429 | ) 430 | if err != nil { 431 | return err 432 | } 433 | 434 | log.Infof("Successfully created ConfigMap '%s.%s'", u.namespace, u.name) 435 | return nil 436 | } 437 | 438 | func (u *secretUploader) iterToSecretData(iter FileIter) (map[string][]byte, error) { 439 | data := make(map[string][]byte) 440 | err := iter.ForEach(func(file *object.File) error { 441 | if filterFile(file, u.includes, u.excludes) { 442 | content, err := file.Contents() 443 | if err != nil { 444 | return err 445 | } 446 | data[strings.ReplaceAll(file.Name, "/", ".")] = []byte(content) 447 | } 448 | return nil 449 | }) 450 | 451 | return data, err 452 | } 453 | 454 | func newFolderUploader(o UploaderOptions) (Uploader, error) { 455 | err := os.RemoveAll(o.Target) 456 | if err != nil { 457 | return nil, err 458 | } 459 | err = os.MkdirAll(o.Target, os.ModePerm) // #nosec G301 460 | if err != nil { 461 | return nil, err 462 | } 463 | log.Infof("Created empty folder %s", o.Target) 464 | 465 | includesRegex, err := stringsToRegExp(o.Includes) 466 | if err != nil { 467 | return nil, err 468 | } 469 | log.Infof("Loaded include rules %s", includesRegex) 470 | 471 | excludesRegex, err := stringsToRegExp(o.Excludes) 472 | if err != nil { 473 | return nil, err 474 | } 475 | log.Infof("Loaded exclude rules %s", excludesRegex) 476 | 477 | return &folderUploader{ 478 | includes: includesRegex, 479 | excludes: excludesRegex, 480 | name: o.Target, 481 | sourcePath: o.Source, 482 | }, nil 483 | } 484 | 485 | func (u *folderUploader) Upload(commitID string, iter FileIter) error { 486 | filesToKeep := make(map[string]bool) 487 | err := iter.ForEach(func(file *object.File) error { 488 | if filterFile(file, u.includes, u.excludes) { 489 | src := path.Join(u.sourcePath, file.Name) 490 | if _, err := os.Lstat(src); err == nil { 491 | src, _ = filepath.Abs(src) // #nosec G104 492 | } 493 | dst := path.Join(u.name, file.Name) 494 | filesToKeep[dst] = true 495 | 496 | source, err := os.Open(src) // #nosec G304 497 | if err != nil { 498 | return err 499 | } 500 | defer source.Close() 501 | 502 | if dir, _ := filepath.Split(dst); dir != "" { 503 | err = os.MkdirAll(dir, 0o777) // #nosec G301 504 | if err != nil { 505 | return err 506 | } 507 | } 508 | 509 | destination, err := os.Create(dst) // #nosec G304 510 | if err != nil { 511 | return err 512 | } 513 | defer destination.Close() 514 | 515 | buf := make([]byte, bufferSize) 516 | for { 517 | n, err := source.Read(buf) 518 | if err != nil && err != io.EOF { 519 | return err 520 | } 521 | if n == 0 { 522 | break 523 | } 524 | if _, err := destination.Write(buf[:n]); err != nil { 525 | return err 526 | } 527 | } 528 | } 529 | return nil 530 | }) 531 | if err != nil { 532 | return err 533 | } 534 | 535 | err = filepath.Walk(u.name, func(path string, info os.FileInfo, err error) error { 536 | if _, exists := filesToKeep[path]; info != nil && !info.IsDir() && !exists { 537 | err := os.Remove(path) 538 | if err != nil { 539 | return err 540 | } 541 | } 542 | return nil 543 | }) 544 | 545 | return err 546 | } 547 | 548 | func filterFile(file *object.File, includes []*regexp.Regexp, excludes []*regexp.Regexp) bool { 549 | pass := false 550 | for _, inc := range includes { 551 | if inc.MatchString(file.Name) { 552 | pass = true 553 | break 554 | } 555 | } 556 | 557 | for _, exc := range excludes { 558 | if exc.MatchString(file.Name) { 559 | pass = false 560 | break 561 | } 562 | } 563 | 564 | log.Debugf("[%t] '%s'", pass, file.Name) 565 | return pass 566 | } 567 | 568 | func restConfig(kubeconfig bool) (*rest.Config, error) { 569 | if kubeconfig { 570 | log.Infof("Loading kubeconfig") 571 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 572 | configOverrides := &clientcmd.ConfigOverrides{} 573 | kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) 574 | return kubeConfig.ClientConfig() 575 | } 576 | log.Infof("Loading InCluster config") 577 | return rest.InClusterConfig() 578 | } 579 | 580 | func stringsToRegExp(strs []string) ([]*regexp.Regexp, error) { 581 | result := make([]*regexp.Regexp, len(strs)) 582 | for i, str := range strs { 583 | regex, err := regexp.Compile(str) 584 | if err != nil { 585 | return nil, err 586 | } 587 | result[i] = regex 588 | } 589 | 590 | return result, nil 591 | } 592 | 593 | func stringsToMap(strs []string) (map[string]string, error) { 594 | result := make(map[string]string) 595 | for _, str := range strs { 596 | if !strings.Contains(str, "=") { 597 | return nil, fmt.Errorf("argument '%s' does not contain required char '='", str) 598 | } 599 | split := strings.Split(str, "=") 600 | result[split[0]] = split[1] 601 | } 602 | 603 | return result, nil 604 | } 605 | 606 | func init() { 607 | register(ConfigMap, newConfigMapUploader) 608 | register(Secret, newSecretUploader) 609 | register(Folder, newFolderUploader) 610 | } 611 | -------------------------------------------------------------------------------- /pkg/upload/upload_test.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/go-git/go-git/v5/plumbing" 12 | "github.com/go-git/go-git/v5/plumbing/filemode" 13 | "github.com/go-git/go-git/v5/plumbing/object" 14 | "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | testclient "k8s.io/client-go/kubernetes/fake" 16 | testing2 "k8s.io/client-go/testing" 17 | ) 18 | 19 | type mockFileIter struct { 20 | files []*object.File 21 | } 22 | 23 | func (m *mockFileIter) ForEach(cb func(*object.File) error) error { 24 | for _, f := range m.files { 25 | obj := &plumbing.MemoryObject{} 26 | content, err := os.ReadFile(filepath.Join("testdata", f.Name)) 27 | if err != nil { 28 | panic(err) 29 | } 30 | obj.Write(content) 31 | obj.SetType(plumbing.BlobObject) 32 | blob := &object.Blob{} 33 | err = blob.Decode(obj) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | cb(object.NewFile(f.Name, f.Mode, blob)) 39 | } 40 | return nil 41 | } 42 | 43 | var basicCases = []struct { 44 | name string 45 | namespace string 46 | target string 47 | includes []*regexp.Regexp 48 | excludes []*regexp.Regexp 49 | labels map[string]string 50 | annotations map[string]string 51 | iter *mockFileIter 52 | contains []string 53 | }{ 54 | { 55 | name: "Empty JSON in default namespace no include", 56 | namespace: "default", 57 | target: "git2kube", 58 | labels: map[string]string{}, 59 | annotations: map[string]string{}, 60 | iter: &mockFileIter{ 61 | files: []*object.File{ 62 | object.NewFile("test.json", filemode.Regular, &object.Blob{}), 63 | }, 64 | }, 65 | }, 66 | { 67 | name: "Empty JSON in default namespace include all", 68 | namespace: "default", 69 | target: "git2kube", 70 | labels: map[string]string{}, 71 | annotations: map[string]string{}, 72 | includes: []*regexp.Regexp{ 73 | regexp.MustCompile(".*"), 74 | }, 75 | iter: &mockFileIter{ 76 | files: []*object.File{ 77 | object.NewFile("test.json", filemode.Regular, &object.Blob{}), 78 | object.NewFile("test.yaml", filemode.Regular, &object.Blob{}), 79 | }, 80 | }, 81 | contains: []string{"test.json", "test.yaml"}, 82 | }, 83 | { 84 | name: "Empty JSON in default namespace include all exclude yaml", 85 | namespace: "default", 86 | target: "git2kube", 87 | labels: map[string]string{}, 88 | annotations: map[string]string{}, 89 | includes: []*regexp.Regexp{ 90 | regexp.MustCompile(".*"), 91 | }, 92 | excludes: []*regexp.Regexp{ 93 | regexp.MustCompile(`.*\.yaml`), 94 | }, 95 | iter: &mockFileIter{ 96 | files: []*object.File{ 97 | object.NewFile("test.json", filemode.Regular, &object.Blob{}), 98 | object.NewFile("test.yaml", filemode.Regular, &object.Blob{}), 99 | }, 100 | }, 101 | contains: []string{"test.json"}, 102 | }, 103 | { 104 | name: "Empty JSON in default namespace no include", 105 | namespace: "default", 106 | target: "git2kube", 107 | labels: map[string]string{}, 108 | annotations: map[string]string{}, 109 | iter: &mockFileIter{ 110 | files: []*object.File{ 111 | object.NewFile("test.json", filemode.Regular, &object.Blob{}), 112 | }, 113 | }, 114 | }, 115 | { 116 | name: "No files in config namespace", 117 | namespace: "config", 118 | target: "git2kube", 119 | labels: map[string]string{}, 120 | annotations: map[string]string{}, 121 | iter: &mockFileIter{ 122 | files: []*object.File{}, 123 | }, 124 | }, 125 | { 126 | name: "No files in config namespace with annotations and labels", 127 | namespace: "config", 128 | target: "git2kube", 129 | labels: map[string]string{ 130 | "test1": "value1", 131 | }, 132 | annotations: map[string]string{ 133 | "test2": "value2", 134 | }, 135 | iter: &mockFileIter{ 136 | files: []*object.File{}, 137 | }, 138 | }, 139 | } 140 | 141 | func TestConfigmapUploader_Upload(t *testing.T) { 142 | for _, c := range basicCases { 143 | fakeclient := testclient.NewSimpleClientset() 144 | cu := &configmapUploader{ 145 | clientset: fakeclient, 146 | namespace: c.namespace, 147 | name: c.target, 148 | labels: c.labels, 149 | annotations: c.annotations, 150 | includes: c.includes, 151 | excludes: c.excludes, 152 | } 153 | err := cu.Upload("id", c.iter) 154 | if err != nil { 155 | t.Errorf("%s case failed: %v", c.name, err) 156 | } 157 | 158 | assertAction(fakeclient.Actions()[0], t, c.name, c.namespace, "get", "configmaps") 159 | assertAction(fakeclient.Actions()[1], t, c.name, c.namespace, "create", "configmaps") 160 | res, err := fakeclient.CoreV1().ConfigMaps(c.namespace).Get(context.TODO(), c.target, v1.GetOptions{}) 161 | if err != nil { 162 | t.Errorf("%s case failed: %v", c.name, err) 163 | } 164 | assertAnnotationsAndLabels(res.Annotations, res.Labels, t, c.name, c.annotations, c.labels) 165 | assertData(res.Data, t, c.name, c.contains) 166 | } 167 | } 168 | 169 | func TestSecretUploader_Upload(t *testing.T) { 170 | for _, c := range basicCases { 171 | fakeclient := testclient.NewSimpleClientset() 172 | cu := &secretUploader{ 173 | clientset: fakeclient, 174 | namespace: c.namespace, 175 | name: c.target, 176 | labels: c.labels, 177 | annotations: c.annotations, 178 | includes: c.includes, 179 | excludes: c.excludes, 180 | } 181 | err := cu.Upload("id", c.iter) 182 | if err != nil { 183 | t.Errorf("%s case failed: %v", c.name, err) 184 | } 185 | 186 | assertAction(fakeclient.Actions()[0], t, c.name, c.namespace, "get", "secrets") 187 | assertAction(fakeclient.Actions()[1], t, c.name, c.namespace, "create", "secrets") 188 | 189 | res, err := fakeclient.CoreV1().Secrets(c.namespace).Get(context.TODO(), c.target, v1.GetOptions{}) 190 | if err != nil { 191 | t.Errorf("%s case failed: %v", c.name, err) 192 | } 193 | assertAnnotationsAndLabels(res.Annotations, res.Labels, t, c.name, c.annotations, c.labels) 194 | 195 | data := make(map[string]string) 196 | for k, v := range res.Data { 197 | data[k] = string(v[:]) 198 | } 199 | assertData(data, t, c.name, c.contains) 200 | } 201 | } 202 | 203 | func TestFolderUploader_Upload(t *testing.T) { 204 | ex, err := os.Executable() 205 | if err != nil { 206 | panic(err) 207 | } 208 | exPath := filepath.Dir(ex) 209 | 210 | for _, c := range basicCases { 211 | cu := &folderUploader{ 212 | sourcePath: exPath, 213 | name: c.target, 214 | includes: c.includes, 215 | excludes: c.excludes, 216 | } 217 | err := cu.Upload("id", c.iter) 218 | if err != nil { 219 | t.Errorf("%s case failed: %v", c.name, err) 220 | } 221 | } 222 | } 223 | 224 | func assertAction(action testing2.Action, t *testing.T, name string, namespace string, verb string, resource string) { 225 | if action.GetNamespace() != namespace { 226 | t.Errorf("%s case failed: expected '%s' namespace but got '%s' instead", name, namespace, action.GetNamespace()) 227 | } 228 | if !action.Matches(verb, resource) { 229 | t.Errorf("%s case failed: expected action '[%s]%s' namespace but got '[%s]%s' instead", name, verb, resource, action.GetVerb(), action.GetResource().Resource) 230 | } 231 | } 232 | 233 | func assertAnnotationsAndLabels(annotations map[string]string, labels map[string]string, t *testing.T, name string, exannotations map[string]string, exlabels map[string]string) { 234 | if !reflect.DeepEqual(annotations, exannotations) { 235 | t.Errorf("%s case failed: expected annotations '%s' but got '%s' instead", name, exannotations, annotations) 236 | } 237 | 238 | if !reflect.DeepEqual(labels, exlabels) { 239 | t.Errorf("%s case failed: expected labels '%s' but got '%s' instead", name, exlabels, labels) 240 | } 241 | } 242 | 243 | func assertData(data map[string]string, t *testing.T, name string, contains []string) { 244 | if len(contains) != len(data) { 245 | t.Errorf("%s case failed: expected data '%s' but got '%s' instead", name, contains, reflect.ValueOf(data).MapKeys()) 246 | } 247 | 248 | for _, k := range contains { 249 | if v, ok := data[k]; ok { 250 | content, _ := os.ReadFile(filepath.Join("testdata", k)) 251 | if v != string(content) { 252 | t.Errorf("%s case failed: content mismatch expected '%s' but got '%s' instead", name, content, v) 253 | } 254 | } else { 255 | t.Errorf("%s case failed: expected data with key '%s' in '%s'", name, k, data) 256 | } 257 | } 258 | } 259 | --------------------------------------------------------------------------------