├── .circleci └── config.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── dind.go ├── readm.go ├── root.go ├── server.go └── version.go ├── config.md ├── examples ├── docker-compose │ ├── README.md │ └── docker-compose.yaml ├── quarkus-devservices │ ├── README.md │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── joyrex2001 │ │ │ │ └── kubedock │ │ │ │ └── examples │ │ │ │ └── GreetingResource.java │ │ └── resources │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── com │ │ └── joyrex2001 │ │ └── kubedock │ │ └── examples │ │ └── GreetingResourceTest.java ├── tekton │ ├── README.md │ ├── kustomization.yaml │ └── resources │ │ ├── example │ │ └── pplr_kubedock.yaml │ │ ├── git-clone.yaml │ │ ├── mvn-test.yaml │ │ └── pipeline.yaml └── testcontainers-java │ ├── README.md │ ├── pom.xml │ └── src │ ├── test │ ├── java │ │ └── com │ │ │ └── joyrex2001 │ │ │ └── kubedock │ │ │ └── examples │ │ │ └── testcontainers │ │ │ ├── NetworkAliasesTest.java │ │ │ ├── NginxTest.java │ │ │ └── Util.java │ └── resources │ │ ├── logback-test.properties │ │ ├── logback-test.xml │ │ └── nginx.conf │ └── www │ └── index.html ├── go.mod ├── go.sum ├── internal ├── backend │ ├── copy.go │ ├── delete.go │ ├── delete_test.go │ ├── deploy.go │ ├── deploy_test.go │ ├── exec.go │ ├── exec_test.go │ ├── image.go │ ├── logs.go │ ├── logs_test.go │ ├── main.go │ ├── util.go │ └── util_test.go ├── config │ ├── kubernetes.go │ ├── system.go │ └── version.go ├── dind │ └── dind.go ├── events │ ├── events.go │ ├── events_test.go │ └── types.go ├── main.go ├── main_test.go ├── model │ ├── database.go │ ├── database_test.go │ └── types │ │ ├── container.go │ │ ├── container_test.go │ │ ├── exec.go │ │ ├── file.go │ │ ├── image.go │ │ └── network.go ├── reaper │ ├── container.go │ ├── container_test.go │ ├── exec.go │ ├── exec_test.go │ ├── main.go │ └── main_test.go ├── server │ ├── filter │ │ ├── filter.go │ │ └── filter_test.go │ ├── httputil │ │ └── util.go │ ├── main.go │ └── routes │ │ ├── common │ │ ├── archive.go │ │ ├── containers.go │ │ ├── context.go │ │ ├── exec.go │ │ ├── images.go │ │ ├── logs.go │ │ ├── types.go │ │ └── util.go │ │ ├── docker.go │ │ ├── docker │ │ ├── containers.go │ │ ├── containers_test.go │ │ ├── images.go │ │ ├── networks.go │ │ ├── system.go │ │ ├── types.go │ │ ├── util.go │ │ ├── util_test.go │ │ └── volumes.go │ │ ├── libpod.go │ │ └── libpod │ │ ├── containers.go │ │ ├── images.go │ │ ├── system.go │ │ └── types.go └── util │ ├── exec │ └── exec.go │ ├── image │ └── image.go │ ├── ioproxy │ ├── ioproxy.go │ └── ioproxy_test.go │ ├── md2text │ └── md2text.go │ ├── myip │ └── myip.go │ ├── podtemplate │ ├── podtemplate.go │ ├── podtemplate_test.go │ └── test │ │ ├── test_container.yaml │ │ ├── test_invalid.yaml │ │ ├── test_invalid_kind.yaml │ │ └── test_pod.yaml │ ├── portforward │ ├── logger.go │ ├── portforward.go │ └── portforward_test.go │ ├── reverseproxy │ ├── tcpproxy.go │ └── tcpproxy_test.go │ ├── stringid │ ├── stringid.go │ └── stringid_test.go │ └── tar │ ├── concatreader.go │ ├── concatreader_test.go │ ├── reader.go │ ├── reader_test.go │ ├── tar.go │ └── tar_test.go └── main.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | test: 5 | docker: 6 | - image: cimg/go:1.24 7 | steps: 8 | - checkout 9 | - run: 10 | name: run unit tests 11 | command: | 12 | make test 13 | 14 | release: 15 | docker: 16 | - image: cimg/go:1.24 17 | steps: 18 | - checkout 19 | - run: 20 | name: cross compile 21 | command: | 22 | make deps 23 | make gox 24 | cd dist 25 | for r in *; do { t=`echo $r|sed 's/.exe$//'`; e="kubedock"; [[ $r == *.exe ]] && e="kubedock.exe"; mv $r $e; tar czf $t.tar.gz $e; rm $e; } done 26 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -n "kubedock-"$(git describe --tags) -delete $(git describe --tags) ./ 27 | 28 | workflows: 29 | version: 2 30 | main: 31 | jobs: 32 | - test 33 | release: 34 | jobs: 35 | - test: 36 | filters: 37 | branches: 38 | ignore: /.*/ 39 | tags: 40 | only: /^\d+\.\d+\.\d+$/ 41 | - release: 42 | requires: 43 | - test 44 | filters: 45 | branches: 46 | ignore: /.*/ 47 | tags: 48 | only: /^\d+\.\d+\.\d+$/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .vscode 4 | .settings 5 | .classpath 6 | .project 7 | *~ 8 | coverage.out 9 | _archive 10 | dist/ 11 | target/ 12 | *.iml 13 | node_modules/ 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #################### 2 | ## Build kubedock ## ---------------------------------------------------------- 3 | #################### 4 | 5 | FROM docker.io/golang:1.24 AS kubedock 6 | 7 | ARG CODE=github.com/joyrex2001/kubedock 8 | 9 | ADD . /go/src/${CODE}/ 10 | RUN cd /go/src/${CODE} \ 11 | && make test build \ 12 | && mkdir /app \ 13 | && cp kubedock /app 14 | 15 | ################# 16 | ## Final image ## ------------------------------------------------------------ 17 | ################# 18 | 19 | FROM alpine:3 20 | 21 | RUN apk add --no-cache ca-certificates \ 22 | && update-ca-certificates 23 | 24 | COPY --from=kubedock /app /usr/local/bin 25 | 26 | ENTRYPOINT ["/usr/local/bin/kubedock"] 27 | CMD [ "server" ] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Vincent van Dam 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS="-X github.com/joyrex2001/kubedock/internal/config.Date=`date -u +%Y%m%d-%H%M%S` \ 2 | -X github.com/joyrex2001/kubedock/internal/config.Build=`git rev-list -1 HEAD` \ 3 | -X github.com/joyrex2001/kubedock/internal/config.Version=`git describe --tags` \ 4 | -X github.com/joyrex2001/kubedock/internal/config.Image=joyrex2001/kubedock:`git describe --tags | cut -d- -f1`" 5 | 6 | run: 7 | CGO_ENABLED=0 go run main.go server -P -v 2 --port-forward 8 | 9 | build: 10 | CGO_ENABLED=0 go build -ldflags $(LDFLAGS) -o kubedock 11 | 12 | gox: 13 | CGO_ENABLED=0 gox -os="linux darwin windows" -arch="amd64" \ 14 | -output="dist/kubedock_`git describe --tags`_{{.OS}}_{{.Arch}}" -ldflags $(LDFLAGS) 15 | 16 | docker: 17 | docker build . -t joyrex2001/kubedock:latest 18 | 19 | clean: 20 | rm -f kubedock 21 | rm -rf dist 22 | go mod tidy 23 | rm -f coverage.out 24 | go clean -testcache 25 | 26 | cloc: 27 | cloc --exclude-dir=vendor,node_modules,dist,_notes,_archive . 28 | 29 | fmt: 30 | find ./internal -type f -name \*.go -exec gofmt -s -w {} \; 31 | go fmt ./... 32 | 33 | test: 34 | CGO_ENABLED=0 go vet ./... 35 | CGO_ENABLED=0 go test ./... -cover 36 | 37 | lint: 38 | golint ./internal/... 39 | # errcheck ./internal/... ./cmd/... 40 | 41 | cover: 42 | CGO_ENABLED=0 go test ./... -cover -coverprofile=coverage.out 43 | go tool cover -html=coverage.out 44 | 45 | deps: 46 | go install golang.org/x/lint/golint@latest 47 | go install github.com/kisielk/errcheck@latest 48 | go install github.com/mitchellh/gox@latest 49 | go install github.com/tcnksm/ghr@latest 50 | 51 | .PHONY: run build gox docker clean cloc fmt test lint cover deps 52 | -------------------------------------------------------------------------------- /cmd/dind.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/joyrex2001/kubedock/internal/dind" 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | "k8s.io/klog" 10 | ) 11 | 12 | var dindCmd = &cobra.Command{ 13 | Use: "dind", 14 | Short: "Start the kubedock docker-in-docker proxy", 15 | Run: startDind, 16 | } 17 | 18 | func init() { 19 | rootCmd.AddCommand(dindCmd) 20 | 21 | dindCmd.PersistentFlags().String("unix-socket", "/var/run/docker.sock", "Unix socket to listen to") 22 | dindCmd.PersistentFlags().String("kubedock-url", "", "Kubedock url to proxy requests to") 23 | dindCmd.PersistentFlags().StringP("verbosity", "v", "1", "Log verbosity level") 24 | 25 | viper.BindPFlag("dind.socket", dindCmd.PersistentFlags().Lookup("unix-socket")) 26 | viper.BindPFlag("dind.kubedock-url", dindCmd.PersistentFlags().Lookup("kubedock-url")) 27 | viper.BindPFlag("verbosity", dindCmd.PersistentFlags().Lookup("verbosity")) 28 | } 29 | 30 | func startDind(cmd *cobra.Command, args []string) { 31 | flag.Set("v", viper.GetString("verbosity")) 32 | dprox := dind.New(viper.GetString("dind.socket"), viper.GetString("dind.kubedock-url")) 33 | if err := dprox.Run(); err != nil { 34 | klog.Errorf("error running dind proxy: %s", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/readm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/joyrex2001/kubedock/internal/util/md2text" 9 | ) 10 | 11 | var README string 12 | var CONFIG string 13 | var LICENSE string 14 | 15 | func init() { 16 | rootCmd.AddCommand(readmeCmd) 17 | readmeCmd.AddCommand(licenseCmd) 18 | readmeCmd.AddCommand(configCmd) 19 | } 20 | 21 | var readmeCmd = &cobra.Command{ 22 | Use: "readme", 23 | Short: "Display project readme", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | fmt.Println(md2text.ToText(README, 80)) 26 | }, 27 | } 28 | 29 | var configCmd = &cobra.Command{ 30 | Use: "config", 31 | Short: "Display project configuration reference", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | fmt.Println(md2text.ToText(CONFIG, 80)) 34 | }, 35 | } 36 | 37 | var licenseCmd = &cobra.Command{ 38 | Use: "license", 39 | Short: "Display project license", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | fmt.Println(LICENSE) 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | "k8s.io/klog" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "kubedock", 13 | Short: "Kubedock is a docker api implementation that orchestrate containers on kubernetes.", 14 | } 15 | 16 | func Execute() { 17 | if err := rootCmd.Execute(); err != nil { 18 | fmt.Println(err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func init() { 24 | klog.InitFlags(nil) 25 | // pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/joyrex2001/kubedock/internal/config" 9 | ) 10 | 11 | func init() { 12 | rootCmd.AddCommand(versionCmd) 13 | } 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Display kubedock version details", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Printf("-------------------------------------------------\n") 20 | fmt.Printf("kubedock\n") 21 | fmt.Printf("-------------------------------------------------\n") 22 | fmt.Printf("version: %s\n", config.Version) 23 | fmt.Printf("date: %s\n", config.Date) 24 | fmt.Printf("build: %s\n", config.Build) 25 | fmt.Printf("-------------------------------------------------\n") 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | # Configuration reference 2 | 3 | The kubedock binary has the following commands available: 4 | * `server` Start the kubedock api server 5 | * `dind` Start the kubedock docker-in-docker proxy 6 | * `readme` Display project readme 7 | * `version` Display kubedock version details 8 | 9 | The `server` command is the actual kubedock server, and is the command to start kubedock. The table below shows all possible commands and possible arguments. Some commands are also configurable via environment variables, as shown in the environment variable column. 10 | 11 | |command|argument|default|environment variable|description| 12 | |---|---|---|---|---| 13 | |server|--listen-addr|:2475|SERVER_LISTEN_ADDR|Webserver listen address| 14 | |server|--unix-socket|||Unix socket to listen to (instead of port)| 15 | |server|--tls-enable|false|SERVER_TLS_ENABLE|Enable TLS on api server| 16 | |server|--tls-key-file||SERVER_TLS_CERT_FILE|TLS keyfile| 17 | |server|--tls-cert-file||SERVER_TLS_CERT_FILE|TLS certificate file| 18 | |server|--namespace / -n||NAMESPACE|Namespace in which containers should be orchestrated| 19 | |server|--initimage|joyrex2001/kubedock:version|INIT_IMAGE|Image to use as initcontainer for volume setup| 20 | |server|--dindimage|joyrex2001/kubedock:version|DIND_IMAGE|Image to use as sidecar container for docker-in-docker support| 21 | |server|--disable-dind|false|DISABLE_DIND|Disable docker-in-docker support| 22 | |server|--pull-policy|ifnotpresent|PULL_POLICY|Pull policy that should be applied (ifnotpresent,never,always)| 23 | |server|--service-account|default|SERVICE_ACCOUNT|Service account that should be used for deployed pods| 24 | |server|--image-pull-secrets||IMAGE_PULL_SECRETS|Comma separated list of image pull secrets that should be used| 25 | |server|--pod-template||POD_TEMPLATE|Pod file that should be used as the base for creating pods| 26 | |server|--pod-name-prefix||POD_NAME_PREFIX|The prefix of the name to be used in the created pods| 27 | |server|--inspector / -i|false||Enable image inspect to fetch container port config from a registry| 28 | |server|--timeout / -t|1m|TIME_OUT|Container creating/deletion timeout| 29 | |server|--reapmax / -r|60m|REAPER_REAPMAX|Reap all resources older than this time| 30 | |server|--request-cpu||K8S_REQUEST_CPU|Default k8s cpu resource request (optionally add ,limit)| 31 | |server|--request-memory||K8S_REQUEST_MEMORY|Default k8s memory resource request (optionally add ,limit)| 32 | |server|--node-selector||K8S_NODE_SELECTOR|Default k8s node selector in the form of key1=value1[,key2=value2]| 33 | |server|--runas-user||K8S_RUNAS_USER|Numeric UID to run pods as (defaults to UID in image)| 34 | |server|--lock|false||Lock namespace for this instance| 35 | |server|--lock-timeout|15m||Max time trying to acquire namespace lock| 36 | |server|--verbosity / -v|1|VERBOSITY|Log verbosity level| 37 | |server|--prune-start / -P|false||Prune all existing kubedock resources before starting| 38 | |server|--port-forward|false||Open port-forwards for all services| 39 | |server|--reverse-proxy|false||Reverse proxy all services via 0.0.0.0 on the kubedock host as well| 40 | |server|--pre-archive|false||Enable support for copying single files to containers without starting them| 41 | |server|--annotation||K8S_ANNOTATION_annotation|annotation that need to be added to every k8s resource (key=value)| 42 | |server|--label||K8S_LABEL_label|label that need to be added to every k8s resource (key=value)| 43 | |server|--active-deadline-seconds|-1|K8S_ACTIVE_DEADLINE_SECONDS|Default value for pod deadline, in seconds (a negative value means no deadline)| 44 | |server|--ignore-container-memory|false||Ignore container memory setting and use requests/limits from gobal settings or container labels| 45 | |dind|--unix-socket|/var/run/docker.sock||Unix socket to listen to| 46 | |dind|--kubedock-url|||Kubedock url to proxy requests to| 47 | |dind|--verbosity / -v|1|VERBOSITY|Log verbosity level| 48 | |readme||||Display project readme| 49 | |readme|config|||Display configuration reference| 50 | |readme|licence|||Display project licence| 51 | |version||||Display kubedock version details| 52 | 53 | ## Labels and annotations 54 | 55 | Labels added to container images are added as annotations and labels to the created kubernetes pods. Additional labels and annotations can be added with the `--annotation` and `--label` cli argument. Environment variables that start with `K8S_ANNOTATION_` and `K8S_LABEL_` will be added as a kubernetes annotation or label as well. For example `K8S_ANNOTATION_FOO` will create an annotation `foo` with the value of the environment variable. Note that annotations and labels added via environment variables or cli will not be processed by kubedock if they have a specific control function. For these occasions specific environment variables and cli arguments are present. -------------------------------------------------------------------------------- /examples/docker-compose/README.md: -------------------------------------------------------------------------------- 1 | # Example: docker compose 2 | 3 | This folder contains an example docker-compose wordpress setup. Note that this is not a typical use-case for kubedock, however, it does demonstrate some of the nuances you might encounter using kubedock. To run this locally, make sure kubedock is running with port-forwarding enabled (`kubedock server --port-forward`). 4 | 5 | ```bash 6 | docker compose up -d 7 | docker compose ps 8 | curl -v localhost:8000 9 | docker compose rm -f 10 | ``` 11 | 12 | Building images is not supported, as kubedock is not able to do this. -------------------------------------------------------------------------------- /examples/docker-compose/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | volumes: 7 | ## predefined volumes are not supported, hence a directory mapping is 8 | ## used. 9 | - ./db_data:/var/lib/mysql 10 | ports: 11 | ## explicitly set the ports, to make sure the service contains the port 12 | ## mapping as well. 13 | - "3306:3306" 14 | restart: always 15 | environment: 16 | MYSQL_ROOT_PASSWORD: somewordpress 17 | MYSQL_DATABASE: wordpress 18 | MYSQL_USER: wordpress 19 | MYSQL_PASSWORD: wordpress 20 | 21 | wordpress: 22 | depends_on: 23 | - db 24 | image: wordpress:latest 25 | labels: 26 | com.joyrex2001.kubedock.pull-policy: always 27 | volumes: 28 | - ./wordpress_data:/var/www/html 29 | ports: 30 | - "8000:80" 31 | restart: always 32 | environment: 33 | WORDPRESS_DB_HOST: db:3306 34 | WORDPRESS_DB_USER: wordpress 35 | WORDPRESS_DB_PASSWORD: wordpress 36 | WORDPRESS_DB_NAME: wordpress 37 | -------------------------------------------------------------------------------- /examples/quarkus-devservices/README.md: -------------------------------------------------------------------------------- 1 | # Example: quarkus-devservices 2 | 3 | This folder contains an example which is using [Quarkus dev-services](https://quarkus.io/guides/dev-services). To run this locally, make sure kubedock is running with port-forwarding enabled (`kubedock server --port-forward`). 4 | 5 | ```bash 6 | export TESTCONTAINERS_RYUK_DISABLED=true 7 | export TESTCONTAINERS_CHECKS_DISABLE=true 8 | export DOCKER_HOST=tcp://127.0.0.1:2475 9 | mvn test 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/quarkus-devservices/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.joyrex2001.kubedock.examples 6 | quarkus-devservices 7 | 1.0.0-SNAPSHOT 8 | 9 | 3.8.1 10 | 17 11 | UTF-8 12 | UTF-8 13 | quarkus-bom 14 | io.quarkus.platform 15 | 2.13.0.Final 16 | true 17 | 3.0.0-M7 18 | 19 | 20 | 21 | 22 | ${quarkus.platform.group-id} 23 | ${quarkus.platform.artifact-id} 24 | ${quarkus.platform.version} 25 | pom 26 | import 27 | 28 | 29 | 30 | 31 | 32 | io.quarkus 33 | quarkus-resteasy-reactive 34 | 35 | 36 | io.quarkus 37 | quarkus-oidc 38 | 39 | 40 | io.quarkus 41 | quarkus-arc 42 | 43 | 44 | io.quarkus 45 | quarkus-junit5 46 | test 47 | 48 | 49 | io.quarkus 50 | quarkus-test-keycloak-server 51 | test 52 | 53 | 54 | io.rest-assured 55 | rest-assured 56 | test 57 | 58 | 59 | 60 | 61 | 62 | ${quarkus.platform.group-id} 63 | quarkus-maven-plugin 64 | ${quarkus.platform.version} 65 | true 66 | 67 | 68 | 69 | build 70 | generate-code 71 | generate-code-tests 72 | 73 | 74 | 75 | 76 | 77 | maven-compiler-plugin 78 | ${compiler-plugin.version} 79 | 80 | 81 | -parameters 82 | 83 | 84 | 85 | 86 | maven-surefire-plugin 87 | ${surefire-plugin.version} 88 | 89 | 90 | org.jboss.logmanager.LogManager 91 | ${maven.home} 92 | 93 | 94 | 95 | 96 | maven-failsafe-plugin 97 | ${surefire-plugin.version} 98 | 99 | 100 | 101 | integration-test 102 | verify 103 | 104 | 105 | 106 | ${project.build.directory}/${project.build.finalName}-runner 107 | org.jboss.logmanager.LogManager 108 | ${maven.home} 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | native 119 | 120 | 121 | native 122 | 123 | 124 | 125 | false 126 | native 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/quarkus-devservices/src/main/java/com/joyrex2001/kubedock/examples/GreetingResource.java: -------------------------------------------------------------------------------- 1 | package com.joyrex2001.kubedock.examples; 2 | 3 | import io.quarkus.security.Authenticated; 4 | 5 | import javax.annotation.security.RolesAllowed; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.Produces; 9 | import javax.ws.rs.core.MediaType; 10 | 11 | @Authenticated 12 | @Path("/") 13 | public class GreetingResource { 14 | 15 | @GET 16 | @Produces(MediaType.TEXT_PLAIN) 17 | @RolesAllowed({"user","admin"}) 18 | @Path("/hello") 19 | public String say() { 20 | return "Hello world!"; 21 | } 22 | 23 | @GET 24 | @Produces(MediaType.TEXT_PLAIN) 25 | @RolesAllowed({"admin"}) 26 | @Path("/admin") 27 | public String admin() { 28 | return "Admin page"; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /examples/quarkus-devservices/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joyrex2001/kubedock/6399f1da7a97cbd20a39b4901127fbda73f1d5b5/examples/quarkus-devservices/src/main/resources/application.properties -------------------------------------------------------------------------------- /examples/quarkus-devservices/src/test/java/com/joyrex2001/kubedock/examples/GreetingResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.joyrex2001.kubedock.examples; 2 | 3 | import io.quarkus.test.junit.QuarkusTest; 4 | import io.quarkus.test.keycloak.client.KeycloakTestClient; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static io.restassured.RestAssured.given; 8 | import static org.hamcrest.CoreMatchers.is; 9 | 10 | @QuarkusTest 11 | public class GreetingResourceTest { 12 | 13 | KeycloakTestClient keycloakClient = new KeycloakTestClient(); 14 | 15 | @Test 16 | public void testHelloEndpoint() { 17 | given() 18 | .auth().oauth2(getAccessToken("alice")) 19 | .when().get("/hello") 20 | .then() 21 | .statusCode(200) 22 | .body(is("Hello world!")); 23 | } 24 | 25 | @Test 26 | public void testAdminEndpoint() { 27 | given() 28 | .auth().oauth2(getAccessToken("bob")) 29 | .when().get("/admin") 30 | .then() 31 | .statusCode(403); 32 | } 33 | 34 | protected String getAccessToken(String userName) { 35 | return keycloakClient.getAccessToken(userName); 36 | } 37 | } -------------------------------------------------------------------------------- /examples/tekton/README.md: -------------------------------------------------------------------------------- 1 | # Example: Tekton 2 | 3 | This folder contains an example tekton task (and a pipeline using this task) that will use kubedock to run the tests of the testcontainers-java example. 4 | 5 | Apply the resources: 6 | 7 | ```bash 8 | kustomize build . | kubectl apply -f - 9 | ``` 10 | Start a pipelinerun via cmd: 11 | 12 | ```bash 13 | tkn pipeline start kubedock-example 14 | -p git-url=https://github.com/joyrex2001/kubedock.git \ 15 | -p context-dir=examples/testcontainers-java \ 16 | -p git-revision=master 17 | ``` 18 | 19 | Or start a pipelinerun via the provided yaml-file: 20 | 21 | ```bash 22 | kubectl create -f ./resources/example/pplr_kubedock.yaml 23 | ``` 24 | 25 | The task is using a sidecar container in which kubedock is running. Note that this sidecar container is also mounting the workspace volume. This is required when volumemounts or file copies are used in the tests. If the sidecar is not able to access the workspace, kubedock will not be able to access these files. -------------------------------------------------------------------------------- /examples/tekton/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ./resources/mvn-test.yaml 6 | - ./resources/pipeline.yaml 7 | - ./resources/git-clone.yaml -------------------------------------------------------------------------------- /examples/tekton/resources/example/pplr_kubedock.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | generateName: kubedock-example 5 | spec: 6 | params: 7 | - name: git-url 8 | value: "https://github.com/joyrex2001/kubedock.git" 9 | - name: git-revision 10 | value: "master" 11 | - name: context-dir 12 | value: "examples/testcontainers-java" 13 | pipelineRef: 14 | name: kubedock-example 15 | workspaces: 16 | - name: source 17 | volumeClaimTemplate: 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: 1Gi -------------------------------------------------------------------------------- /examples/tekton/resources/mvn-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Task 3 | metadata: 4 | name: mvn-test 5 | spec: 6 | params: 7 | - name: contextDir 8 | type: string 9 | workspaces: 10 | - name: source 11 | steps: 12 | - name: step-mvn-test 13 | image: gcr.io/cloud-builders/mvn 14 | workingDir: $(workspaces.source.path)/$(params.contextDir) 15 | command: 16 | - /usr/bin/mvn 17 | args: 18 | - test 19 | env: 20 | - name: TESTCONTAINERS_RYUK_DISABLED 21 | value: "true" 22 | - name: TESTCONTAINERS_CHECKS_DISABLE 23 | value: "true" 24 | resources: {} 25 | volumeMounts: 26 | - name: kubedock-socket 27 | mountPath: /var/run/ 28 | sidecars: 29 | - name: kubedock 30 | image: joyrex2001/kubedock:latest 31 | args: 32 | - server 33 | - --reverse-proxy 34 | - --unix-socket 35 | - /var/run/docker.sock 36 | env: 37 | - name: NAMESPACE 38 | valueFrom: 39 | fieldRef: 40 | fieldPath: metadata.namespace 41 | volumeMounts: 42 | - name: $(workspaces.source.volume) 43 | mountPath: $(workspaces.source.path) 44 | - name: kubedock-socket 45 | mountPath: /var/run/ 46 | volumes: 47 | - name: kubedock-socket 48 | emptyDir: {} 49 | -------------------------------------------------------------------------------- /examples/tekton/resources/pipeline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: Pipeline 3 | metadata: 4 | name: kubedock-example 5 | spec: 6 | params: 7 | - name: git-url 8 | - name: git-revision 9 | - name: context-dir 10 | workspaces: 11 | - name: shared-workspace 12 | tasks: 13 | - name: clone 14 | taskRef: 15 | name: git-clone 16 | workspaces: 17 | - name: output 18 | workspace: shared-workspace 19 | params: 20 | - name: url 21 | value: $(params.git-url) 22 | - name: revision 23 | value: $(params.git-revision) 24 | - name: test 25 | taskRef: 26 | name: mvn-test 27 | kind: Task 28 | runAfter: 29 | - clone 30 | workspaces: 31 | - name: source 32 | workspace: shared-workspace 33 | params: 34 | - name: contextDir 35 | value: $(params.context-dir) -------------------------------------------------------------------------------- /examples/testcontainers-java/README.md: -------------------------------------------------------------------------------- 1 | # Example: testcontainers-java 2 | 3 | This folder contains an example using the test-containers framework. To run this locally, make sure kubedock is running with port-forwarding enabled (`kubedock server --port-forward`). 4 | 5 | ```bash 6 | export TESTCONTAINERS_RYUK_DISABLED=true 7 | export TESTCONTAINERS_CHECKS_DISABLE=true 8 | export DOCKER_HOST=tcp://127.0.0.1:2475 9 | mvn test 10 | ``` 11 | 12 | The example includes: 13 | 14 | * `NginxTest.java` which demonstrates how to use volumes in combination with kubedock. 15 | * `NetworkAliasesTest.java` which demonstrates how to use network aliases in combination with kubedock. 16 | 17 | When kubedock is running in a cluster, it can return the actual cluster IPs of the services. However, the testcontainers-java framework will not use the actual cluster IP and returns the IP of the docker API instead (see [this](https://github.com/testcontainers/testcontainers-java/issues/452) issue). Therefore you either need to reverse-proxy or port-forward from inside the cluster as well. In this case you might want to use `--reverse-proxy`, which will enable local reverse proxies to the cluster IPs of the services and is more stable as `--port-forward`. -------------------------------------------------------------------------------- /examples/testcontainers-java/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.joyrex2001.kubedock.examples 7 | testcontainers 8 | 1.0.0-SNAPSHOT 9 | jar 10 | 11 | 12 | UTF-8 13 | 17 14 | 17 15 | 1.19.6 16 | 5.10.2 17 | 18 | 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-compiler-plugin 24 | 3.12.1 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-surefire-plugin 29 | 3.2.5 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.assertj 37 | assertj-core 38 | 3.25.3 39 | test 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter-api 44 | ${junit-jupiter.version} 45 | test 46 | 47 | 48 | org.junit.jupiter 49 | junit-jupiter-engine 50 | ${junit-jupiter.version} 51 | test 52 | 53 | 54 | org.slf4j 55 | slf4j-api 56 | 2.0.12 57 | test 58 | 59 | 60 | ch.qos.logback 61 | logback-classic 62 | 1.5.0 63 | test 64 | 65 | 66 | org.testcontainers 67 | testcontainers 68 | ${testcontainers.version} 69 | test 70 | 71 | 72 | org.testcontainers 73 | junit-jupiter 74 | ${testcontainers.version} 75 | test 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/java/com/joyrex2001/kubedock/examples/testcontainers/NetworkAliasesTest.java: -------------------------------------------------------------------------------- 1 | package com.joyrex2001.kubedock.examples.testcontainers; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.testcontainers.containers.GenericContainer; 5 | import org.testcontainers.containers.Network; 6 | import org.testcontainers.junit.jupiter.Testcontainers; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @Testcontainers 13 | public class NetworkAliasesTest { 14 | 15 | private static final String ALPINE_IMAGE = "library/alpine"; 16 | private static final int TEST_PORT = 8080; 17 | 18 | @Test 19 | void testNetworkAliases() throws IOException, InterruptedException { 20 | Network network = Network.newNetwork(); 21 | 22 | GenericContainer foo = new GenericContainer(ALPINE_IMAGE) 23 | .withNetwork(network) 24 | .withNetworkAliases("foo") 25 | // we need to explicitly define the ports we are exposing 26 | // otherwise the k8s service will not be created. 27 | .withExposedPorts(TEST_PORT) 28 | .withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"); 29 | 30 | GenericContainer bar = new GenericContainer(ALPINE_IMAGE) 31 | .withNetwork(network) 32 | .withCommand("top"); 33 | 34 | foo.start(); 35 | bar.start(); 36 | 37 | String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout(); 38 | assertThat(response).contains("yay"); 39 | 40 | foo.stop(); 41 | bar.stop(); 42 | } 43 | } -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/java/com/joyrex2001/kubedock/examples/testcontainers/NginxTest.java: -------------------------------------------------------------------------------- 1 | package com.joyrex2001.kubedock.examples.testcontainers; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import org.testcontainers.containers.BindMode; 7 | import org.testcontainers.containers.GenericContainer; 8 | import org.testcontainers.containers.wait.strategy.Wait; 9 | import org.testcontainers.containers.output.Slf4jLogConsumer; 10 | 11 | import org.testcontainers.junit.jupiter.Testcontainers; 12 | import org.testcontainers.utility.DockerImageName; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.slf4j.LoggerFactory.getLogger; 16 | 17 | import java.net.URI; 18 | import java.net.URL; 19 | import java.io.IOException; 20 | 21 | @Testcontainers 22 | public class NginxTest { 23 | 24 | private static final int NGINX_PORT = 8080; 25 | private static final String NGINX_IMAGE = "nginxinc/nginx-unprivileged"; // "library/nginx" 26 | 27 | @Test 28 | @SuppressWarnings("unchecked") 29 | void testNginx() throws IOException { 30 | GenericContainer nginx = new GenericContainer(DockerImageName.parse(NGINX_IMAGE)) 31 | //.withFileSystemBind to a folder will copy the folder before the container starts 32 | .withFileSystemBind("./src/www", "/www", BindMode.READ_ONLY) 33 | //.withFileSystemBind to a file results into creation of a configmap before the container runs 34 | .withFileSystemBind("./src/test/resources/nginx.conf", "/etc/nginx/conf.d/default.conf", BindMode.READ_ONLY) 35 | //.withClasspathResourceMapping results into a copy in a running container (unless kubedock runs with --pre-archive) 36 | //.withClasspathResourceMapping("nginx.conf", "/etc/nginx/conf.d/default.conf", BindMode.READ_ONLY) 37 | .withLogConsumer(new Slf4jLogConsumer(getLogger("nginx"))) 38 | .waitingFor(Wait.forHttp("/")) 39 | .withExposedPorts(NGINX_PORT); 40 | 41 | nginx.start(); 42 | 43 | URL serviceUrl = URI.create(String.format("http://%s:%d/", 44 | nginx.getContainerIpAddress(), 45 | nginx.getMappedPort(NGINX_PORT))).toURL(); 46 | 47 | assertThat(Util.readFromUrl(serviceUrl)) 48 | .contains("Hello!") 49 | .contains("

Hello!

"); 50 | 51 | nginx.stop(); 52 | } 53 | } -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/java/com/joyrex2001/kubedock/examples/testcontainers/Util.java: -------------------------------------------------------------------------------- 1 | package com.joyrex2001.kubedock.examples.testcontainers; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import java.net.URLConnection; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | final class Util { 13 | private Util() { 14 | } 15 | 16 | static String readFromUrl(URL url) throws IOException { 17 | String content; 18 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 19 | try { 20 | int responseCode = connection.getResponseCode(); 21 | assertThat(responseCode).isEqualTo(200); 22 | content = readContent(connection); 23 | } finally { 24 | connection.disconnect(); 25 | } 26 | return content; 27 | } 28 | 29 | private static String readContent(URLConnection connection) throws IOException { 30 | try (BufferedReader in = new BufferedReader( 31 | new InputStreamReader(connection.getInputStream()))) { 32 | String inputLine; 33 | StringBuilder content = new StringBuilder(); 34 | while ((inputLine = in.readLine()) != null) { 35 | content.append(inputLine); 36 | } 37 | return content.toString(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/resources/logback-test.properties: -------------------------------------------------------------------------------- 1 | # Log level for software under test 2 | config.sut.log.level=${sut.log.level:-info} 3 | # Log level for all test related information 4 | config.test.log.level=${test.log.level:-debug} 5 | # Threshold level for stdout (inclusive) 6 | config.stdout.log.level=${stdout.log.level:-info} 7 | config.stdout.log.pattern=%d{yyyy-MM-dd HH:mm:ss} %-7([%level]) %logger - %message%n%xEx{5} 8 | # Default root log level. Adjust by -Droot.log.level=debug 9 | config.root.log.level=${root.log.level:-warn} 10 | config.file.log.pattern=%d %-7([%level]) %logger - %message \\(%contextName, %thread\\)%n 11 | config.file.log.file=${log.dir:-target/logs}/${tests.name:-tests}.log -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ${config.stdout.log.pattern} 24 | 25 | 26 | ${config.stdout.log.level} 27 | 28 | 29 | 30 | 31 | ${config.file.log.file} 32 | 33 | ${config.file.log.pattern} 34 | 35 | 36 | 10 37 | ${config.file.log.file}.%i 38 | 39 | 40 | 4MB 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/testcontainers-java/src/test/resources/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | location / { 6 | root /www; 7 | index index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /examples/testcontainers-java/src/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello! 3 | 4 |

Hello!

5 | 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joyrex2001/kubedock 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/containers/image/v5 v5.35.0 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc 8 | github.com/dsnet/compress v0.0.1 9 | github.com/fsnotify/fsnotify v1.9.0 10 | github.com/gin-gonic/gin v1.10.0 11 | github.com/hashicorp/go-memdb v1.3.5 12 | github.com/opencontainers/image-spec v1.1.1 13 | github.com/spf13/cobra v1.9.1 14 | github.com/spf13/viper v1.20.1 15 | github.com/ulikunitz/xz v0.5.12 16 | golang.org/x/time v0.11.0 17 | k8s.io/api v0.33.0 18 | k8s.io/apimachinery v0.33.0 19 | k8s.io/client-go v0.33.0 20 | k8s.io/klog v1.0.0 21 | ) 22 | 23 | require ( 24 | dario.cat/mergo v1.0.1 // indirect 25 | github.com/BurntSushi/toml v1.5.0 // indirect 26 | github.com/Microsoft/go-winio v0.6.2 // indirect 27 | github.com/Microsoft/hcsshim v0.12.9 // indirect 28 | github.com/bytedance/sonic v1.13.2 // indirect 29 | github.com/bytedance/sonic/loader v0.2.4 // indirect 30 | github.com/cloudwego/base64x v0.1.5 // indirect 31 | github.com/containerd/cgroups/v3 v3.0.5 // indirect 32 | github.com/containerd/errdefs v1.0.0 // indirect 33 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 34 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 35 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 36 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 37 | github.com/containers/ocicrypt v1.2.1 // indirect 38 | github.com/containers/storage v1.58.0 // indirect 39 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 40 | github.com/distribution/reference v0.6.0 // indirect 41 | github.com/docker/distribution v2.8.3+incompatible // indirect 42 | github.com/docker/docker v28.1.1+incompatible // indirect 43 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 44 | github.com/docker/go-connections v0.5.0 // indirect 45 | github.com/docker/go-units v0.5.0 // indirect 46 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 47 | github.com/felixge/httpsnoop v1.0.4 // indirect 48 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 49 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 50 | github.com/gin-contrib/sse v1.1.0 // indirect 51 | github.com/go-logr/logr v1.4.2 // indirect 52 | github.com/go-logr/stdr v1.2.2 // indirect 53 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 54 | github.com/go-openapi/jsonreference v0.21.0 // indirect 55 | github.com/go-openapi/swag v0.23.1 // indirect 56 | github.com/go-playground/locales v0.14.1 // indirect 57 | github.com/go-playground/universal-translator v0.18.1 // indirect 58 | github.com/go-playground/validator/v10 v10.26.0 // indirect 59 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 60 | github.com/goccy/go-json v0.10.5 // indirect 61 | github.com/gogo/protobuf v1.3.2 // indirect 62 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 63 | github.com/google/gnostic-models v0.6.9 // indirect 64 | github.com/google/go-cmp v0.7.0 // indirect 65 | github.com/google/go-containerregistry v0.20.3 // indirect 66 | github.com/google/go-intervals v0.0.2 // indirect 67 | github.com/google/uuid v1.6.0 // indirect 68 | github.com/gorilla/mux v1.8.1 // indirect 69 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 70 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 71 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 72 | github.com/hashicorp/golang-lru v1.0.2 // indirect 73 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 74 | github.com/josharian/intern v1.0.0 // indirect 75 | github.com/json-iterator/go v1.1.12 // indirect 76 | github.com/klauspost/compress v1.18.0 // indirect 77 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 78 | github.com/klauspost/pgzip v1.2.6 // indirect 79 | github.com/leodido/go-urn v1.4.0 // indirect 80 | github.com/mailru/easyjson v0.9.0 // indirect 81 | github.com/mattn/go-isatty v0.0.20 // indirect 82 | github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect 83 | github.com/moby/docker-image-spec v1.3.1 // indirect 84 | github.com/moby/spdystream v0.5.0 // indirect 85 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 86 | github.com/moby/sys/capability v0.4.0 // indirect 87 | github.com/moby/sys/mountinfo v0.7.2 // indirect 88 | github.com/moby/sys/user v0.4.0 // indirect 89 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 90 | github.com/modern-go/reflect2 v1.0.2 // indirect 91 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 92 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 93 | github.com/opencontainers/go-digest v1.0.0 // indirect 94 | github.com/opencontainers/runtime-spec v1.2.1 // indirect 95 | github.com/opencontainers/selinux v1.12.0 // indirect 96 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect 97 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 98 | github.com/pkg/errors v0.9.1 // indirect 99 | github.com/sagikazarmark/locafero v0.9.0 // indirect 100 | github.com/sirupsen/logrus v1.9.3 // indirect 101 | github.com/sourcegraph/conc v0.3.0 // indirect 102 | github.com/spf13/afero v1.14.0 // indirect 103 | github.com/spf13/cast v1.7.1 // indirect 104 | github.com/spf13/pflag v1.0.6 // indirect 105 | github.com/subosito/gotenv v1.6.0 // indirect 106 | github.com/sylabs/sif/v2 v2.21.1 // indirect 107 | github.com/tchap/go-patricia/v2 v2.3.2 // indirect 108 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 109 | github.com/ugorji/go/codec v1.2.12 // indirect 110 | github.com/vbatts/tar-split v0.12.1 // indirect 111 | github.com/x448/float16 v0.8.4 // indirect 112 | go.opencensus.io v0.24.0 // indirect 113 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 114 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 115 | go.opentelemetry.io/otel v1.35.0 // indirect 116 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 117 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 118 | go.uber.org/multierr v1.11.0 // indirect 119 | golang.org/x/arch v0.16.0 // indirect 120 | golang.org/x/crypto v0.37.0 // indirect 121 | golang.org/x/net v0.39.0 // indirect 122 | golang.org/x/oauth2 v0.29.0 // indirect 123 | golang.org/x/sync v0.13.0 // indirect 124 | golang.org/x/sys v0.32.0 // indirect 125 | golang.org/x/term v0.31.0 // indirect 126 | golang.org/x/text v0.24.0 // indirect 127 | golang.org/x/tools v0.32.0 // indirect 128 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect 129 | google.golang.org/grpc v1.72.0 // indirect 130 | google.golang.org/protobuf v1.36.6 // indirect 131 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 132 | gopkg.in/inf.v0 v0.9.1 // indirect 133 | gopkg.in/yaml.v3 v3.0.1 // indirect 134 | k8s.io/klog/v2 v2.130.1 // indirect 135 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 136 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 137 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 138 | sigs.k8s.io/randfill v1.0.0 // indirect 139 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 140 | sigs.k8s.io/yaml v1.4.0 // indirect 141 | ) 142 | -------------------------------------------------------------------------------- /internal/backend/copy.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | "io/fs" 9 | "path" 10 | "strings" 11 | 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/klog" 14 | 15 | "github.com/joyrex2001/kubedock/internal/model/types" 16 | "github.com/joyrex2001/kubedock/internal/util/exec" 17 | ) 18 | 19 | // CopyToContainer will copy given (tar) archive to given path of the container. 20 | func (in *instance) CopyToContainer(tainr *types.Container, reader io.Reader, target string) error { 21 | pod, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if target != "/" && strings.HasSuffix(string(target[len(target)-1]), "/") { 27 | target = target[:len(target)-1] 28 | } 29 | 30 | klog.Infof("copy archive to %s:%s", tainr.ShortID, target) 31 | 32 | return exec.RemoteCmd(exec.Request{ 33 | Client: in.cli, 34 | RestConfig: in.cfg, 35 | Pod: *pod, 36 | Container: "main", 37 | Cmd: []string{"tar", "-xf", "-", "-C", target}, 38 | Stdin: reader, 39 | }) 40 | } 41 | 42 | // CopyFromContainer will copy given path from the container and return the 43 | // contents as a tar archive through the given writer. Note that this requires 44 | // tar to be present on the container. 45 | func (in *instance) CopyFromContainer(tainr *types.Container, target string, writer io.Writer) error { 46 | pod, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | klog.Infof("copy archive from %s to %s", tainr.ShortID, target) 52 | 53 | return exec.RemoteCmd(exec.Request{ 54 | Client: in.cli, 55 | RestConfig: in.cfg, 56 | Pod: *pod, 57 | Container: "main", 58 | Cmd: []string{"tar", "-cf", "-", "-C", path.Dir(target), path.Base(target)}, 59 | Stdout: writer, 60 | }) 61 | } 62 | 63 | // GetFileModeInContainer will return the file mode (directory or file) of a given path 64 | // inside the container. 65 | func (in *instance) GetFileModeInContainer(tainr *types.Container, target string) (fs.FileMode, error) { 66 | pod, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 67 | if err != nil { 68 | return 0, err 69 | } 70 | 71 | var b bytes.Buffer 72 | writer := bufio.NewWriter(&b) 73 | 74 | err = exec.RemoteCmd(exec.Request{ 75 | Client: in.cli, 76 | RestConfig: in.cfg, 77 | Pod: *pod, 78 | Container: "main", 79 | Cmd: []string{"sh", "-c", "if [ -d \"" + sanitizeFilename(target) + "\" ]; then echo folder; else echo file; fi"}, 80 | Stdout: writer, 81 | }) 82 | if err != nil { 83 | return 0, err 84 | } 85 | 86 | mode := fs.FileMode(fs.ModePerm) 87 | if strings.Contains(string(b.Bytes()), "folder") { 88 | mode |= fs.ModeDir 89 | } 90 | 91 | return mode, nil 92 | } 93 | 94 | // FileExistsInContainer will check if the file exists in the container. 95 | func (in *instance) FileExistsInContainer(tainr *types.Container, target string) (bool, error) { 96 | pod, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 97 | if err != nil { 98 | return false, err 99 | } 100 | 101 | var b bytes.Buffer 102 | writer := bufio.NewWriter(&b) 103 | 104 | err = exec.RemoteCmd(exec.Request{ 105 | Client: in.cli, 106 | RestConfig: in.cfg, 107 | Pod: *pod, 108 | Container: "main", 109 | Cmd: []string{"sh", "-c", "if [ -e \"" + sanitizeFilename(target) + "\" ]; then echo true; else echo false; fi"}, 110 | Stdout: writer, 111 | }) 112 | 113 | if err != nil { 114 | return false, err 115 | } 116 | 117 | exists := false 118 | if strings.Contains(string(b.Bytes()), "true") { 119 | exists = true 120 | } 121 | 122 | return exists, nil 123 | } 124 | 125 | // sanitizeFilename will clean up unwanted characters from the filename to 126 | // prevent injection attacks. 127 | func sanitizeFilename(file string) string { 128 | file = strings.ReplaceAll(file, "`", "") 129 | file = strings.ReplaceAll(file, "$", "") 130 | file = strings.ReplaceAll(file, "\"", "\\\"") 131 | return file 132 | } 133 | -------------------------------------------------------------------------------- /internal/backend/exec.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/joyrex2001/kubedock/internal/model/types" 13 | "github.com/joyrex2001/kubedock/internal/util/exec" 14 | "github.com/joyrex2001/kubedock/internal/util/ioproxy" 15 | ) 16 | 17 | // ExecContainer will execute given exec object in kubernetes. 18 | func (in *instance) ExecContainer(tainr *types.Container, ex *types.Exec, stdin io.Reader, stdout io.Writer) (int, error) { 19 | pod, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 20 | if err != nil { 21 | return 0, err 22 | } 23 | 24 | req := exec.Request{ 25 | Client: in.cli, 26 | RestConfig: in.cfg, 27 | Pod: *pod, 28 | Container: "main", 29 | Cmd: ex.Cmd, 30 | TTY: ex.TTY, 31 | } 32 | 33 | if ex.Stdin { 34 | req.Stdin = stdin 35 | } 36 | if ex.TTY { 37 | req.Stdout = stdout 38 | req.Stderr = io.Discard 39 | } else { 40 | lock := sync.Mutex{} 41 | if ex.Stdout { 42 | iop := ioproxy.New(stdout, ioproxy.Stdout, &lock) 43 | req.Stdout = iop 44 | defer iop.Flush() 45 | } 46 | if ex.Stderr { 47 | iop := ioproxy.New(stdout, ioproxy.Stderr, &lock) 48 | req.Stderr = iop 49 | defer iop.Flush() 50 | } 51 | } 52 | 53 | err = exec.RemoteCmd(req) 54 | return in.parseExecResponse(err) 55 | } 56 | 57 | // parseExecResponse will take the given error and will parse the string to 58 | // get an exit code from it. if no exit code is found, it will return 0 and 59 | // the original error. 60 | func (in *instance) parseExecResponse(err error) (int, error) { 61 | if err == nil { 62 | return 0, err 63 | } 64 | 65 | const eterm = "command terminated with exit code" 66 | if !strings.Contains(err.Error(), eterm) { 67 | return 0, err 68 | } 69 | 70 | cod, cerr := strconv.Atoi(strings.TrimPrefix(err.Error(), eterm+" ")) 71 | if cerr != nil { 72 | return 0, err 73 | } 74 | 75 | return cod, nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/backend/exec_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParseExecResponse(t *testing.T) { 9 | tests := []struct { 10 | in error 11 | cod int 12 | suc bool 13 | }{ 14 | {nil, 0, true}, 15 | {fmt.Errorf("some generic error"), 0, false}, 16 | {fmt.Errorf("command terminated with exit code 2"), 2, true}, 17 | } 18 | 19 | for i, tst := range tests { 20 | kub := &instance{} 21 | cod, err := kub.parseExecResponse(tst.in) 22 | if cod != tst.cod { 23 | t.Errorf("failed test %d - expected %d, but got %d", i, tst.cod, cod) 24 | } 25 | if err != nil && tst.suc { 26 | t.Errorf("failed test %d - unexpected error: %s", i, err) 27 | } 28 | if err == nil && !tst.suc { 29 | t.Errorf("failed test %d - expected error, but succeeded instead", i) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/backend/image.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/joyrex2001/kubedock/internal/util/image" 5 | ) 6 | 7 | // GetImageExposedPorts will inspect the image in the registry and return the 8 | // configured exposed ports from the image, or will return an error if failed. 9 | func (in *instance) GetImageExposedPorts(img string) (map[string]struct{}, error) { 10 | cfg, err := image.InspectConfig("docker://" + img) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return cfg.Config.ExposedPorts, nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/backend/logs.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/joyrex2001/kubedock/internal/model/types" 13 | "github.com/joyrex2001/kubedock/internal/util/ioproxy" 14 | ) 15 | 16 | // LogOptions describe the supported log options 17 | type LogOptions struct { 18 | // Keep connection after returning logs. 19 | Follow bool 20 | // Only return logs since this time, as a UNIX timestamp 21 | SinceTime *time.Time 22 | // Add timestamps to every log line 23 | Timestamps bool 24 | // Number of lines to show from the end of the logs 25 | TailLines *uint64 26 | } 27 | 28 | // GetLogs will write the logs for given container to given writer. 29 | func (in *instance) GetLogs(tainr *types.Container, opts *LogOptions, stop chan struct{}, w io.Writer) error { 30 | options := newPodLogOptions(opts) 31 | 32 | _, err := in.cli.CoreV1().Pods(in.namespace).Get(context.Background(), tainr.GetPodName(), metav1.GetOptions{}) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | req := in.cli.CoreV1().Pods(in.namespace).GetLogs(tainr.GetPodName(), &options) 38 | stream, err := req.Stream(context.Background()) 39 | if err != nil { 40 | return err 41 | } 42 | defer stream.Close() 43 | 44 | stopL := make(chan struct{}, 1) 45 | 46 | if opts.Follow { 47 | go func() { 48 | <-stop 49 | stopL <- struct{}{} 50 | stream.Close() 51 | }() 52 | } 53 | 54 | out := ioproxy.New(w, ioproxy.Stdout, &sync.Mutex{}) 55 | defer out.Flush() 56 | for { 57 | // close when container is done 58 | select { 59 | case <-stopL: 60 | close(stopL) 61 | return nil 62 | default: 63 | } 64 | // read log input (blocking read) 65 | buf := make([]byte, 255) 66 | n, err := stream.Read(buf) 67 | if err == io.EOF { 68 | break 69 | } 70 | if err != nil { 71 | return err 72 | } 73 | if n == 0 { 74 | if !opts.Follow { 75 | break 76 | } 77 | continue 78 | } 79 | // write log to output 80 | if n, err = out.Write(buf[:n]); n == 0 || err != nil { 81 | break 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func newPodLogOptions(opts *LogOptions) v1.PodLogOptions { 89 | var sinceTime *metav1.Time = nil 90 | if opts.SinceTime != nil { 91 | t := metav1.NewTime(*opts.SinceTime) 92 | sinceTime = &t 93 | } 94 | 95 | var tailLines *int64 = nil 96 | if opts.TailLines != nil { 97 | l := int64(*opts.TailLines) 98 | tailLines = &l 99 | } 100 | 101 | return v1.PodLogOptions{ 102 | Container: "main", 103 | Follow: opts.Follow, 104 | TailLines: tailLines, 105 | SinceTime: sinceTime, 106 | Timestamps: opts.Timestamps, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/backend/logs_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "k8s.io/client-go/kubernetes/fake" 8 | 9 | "github.com/joyrex2001/kubedock/internal/model/types" 10 | ) 11 | 12 | func TestGetLogs(t *testing.T) { 13 | tests := []struct { 14 | in *types.Container 15 | kub *instance 16 | out bool 17 | }{ 18 | { 19 | kub: &instance{ 20 | namespace: "default", 21 | cli: fake.NewSimpleClientset(), 22 | }, 23 | in: &types.Container{ID: "rc752", ShortID: "tb303", Name: "f1spirit"}, 24 | out: true, 25 | }, 26 | // { 27 | // kub: &instance{ 28 | // namespace: "default", 29 | // cli: fake.NewSimpleClientset(&corev1.Pod{ 30 | // ObjectMeta: metav1.ObjectMeta{ 31 | // Name: "f1spirit", 32 | // Namespace: "default", 33 | // Labels: map[string]string{"kubedock": "rc752"}, 34 | // }, 35 | // }), 36 | // }, 37 | // in: &types.Container{ID: "rc752", Name: "f1spirit"}, 38 | // out: false, 39 | // }, 40 | } 41 | 42 | count := uint64(100) 43 | logOpts := LogOptions{TailLines: &count} 44 | for i, tst := range tests { 45 | r, w := io.Pipe() 46 | stop := make(chan struct{}, 1) 47 | res := tst.kub.GetLogs(tst.in, &logOpts, stop, w) 48 | if (res != nil && !tst.out) || (res == nil && tst.out) { 49 | t.Errorf("failed test %d - unexpected return value %s", i, res) 50 | } 51 | r.Close() 52 | w.Close() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/backend/main.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "time" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | 13 | "github.com/joyrex2001/kubedock/internal/model/types" 14 | "github.com/joyrex2001/kubedock/internal/util/podtemplate" 15 | ) 16 | 17 | // Backend is the interface to orchestrate and manage kubernetes objects. 18 | type Backend interface { 19 | StartContainer(*types.Container) (DeployState, error) 20 | GetContainerStatus(*types.Container) (DeployState, error) 21 | CreatePortForwards(*types.Container) 22 | CreateReverseProxies(*types.Container) 23 | GetPodIP(*types.Container) (string, error) 24 | DeleteAll() error 25 | DeleteWithKubedockID(string) error 26 | DeleteContainer(*types.Container) error 27 | DeleteOlderThan(time.Duration) error 28 | WatchDeleteContainer(*types.Container) (chan struct{}, error) 29 | CopyFromContainer(*types.Container, string, io.Writer) error 30 | CopyToContainer(*types.Container, io.Reader, string) error 31 | GetFileModeInContainer(tainr *types.Container, path string) (fs.FileMode, error) 32 | FileExistsInContainer(tainr *types.Container, path string) (bool, error) 33 | ExecContainer(*types.Container, *types.Exec, io.Reader, io.Writer) (int, error) 34 | GetLogs(*types.Container, *LogOptions, chan struct{}, io.Writer) error 35 | GetImageExposedPorts(string) (map[string]struct{}, error) 36 | } 37 | 38 | // instance is the internal representation of the Backend object. 39 | type instance struct { 40 | cli kubernetes.Interface 41 | cfg *rest.Config 42 | podTemplate *corev1.Pod 43 | containerTemplate corev1.Container 44 | initImage string 45 | dindImage string 46 | disableDind bool 47 | imagePullSecrets []string 48 | namespace string 49 | timeOut int 50 | kuburl string 51 | disableServices bool 52 | } 53 | 54 | // Config is the structure to instantiate a Backend object 55 | type Config struct { 56 | // Client is the kubernetes clientset 57 | Client kubernetes.Interface 58 | // RestConfig is the kubernetes config 59 | RestConfig *rest.Config 60 | // Namespace is the namespace in which all actions are performed 61 | Namespace string 62 | // ImagePullSecrets is an optional list of image pull secrets that need 63 | // to be added to the used pod templates 64 | ImagePullSecrets []string 65 | // InitImage is the image that is used as init container to prepare vols 66 | InitImage string 67 | // DindImage is the image that is used as a sidecar container to 68 | // support docker-in-docker 69 | DindImage string 70 | // DisableDind will disable docker-in-docker support when set to true 71 | DisableDind bool 72 | // TimeOut is the max amount of time to wait until a container started 73 | // or deleted. 74 | TimeOut time.Duration 75 | // PodTemplate refers to an optional file containing a pod resource that 76 | // should be used as the base for creating pod resources. 77 | PodTemplate string 78 | // KubedockURL contains the url of this kubedock instance, to be used in 79 | // docker-in-docker instances/sidecars. 80 | KubedockURL string 81 | 82 | // Disable the creation of services. A networking solution such as kubedock-dns 83 | // should be used. 84 | DisableServices bool 85 | } 86 | 87 | // New will return a Backend instance. 88 | func New(cfg Config) (Backend, error) { 89 | pod := &corev1.Pod{} 90 | if cfg.PodTemplate != "" { 91 | var err error 92 | pod, err = podtemplate.PodFromFile(cfg.PodTemplate) 93 | if err != nil { 94 | return nil, fmt.Errorf("error opening podtemplate: %w", err) 95 | } 96 | } 97 | 98 | return &instance{ 99 | cli: cfg.Client, 100 | cfg: cfg.RestConfig, 101 | initImage: cfg.InitImage, 102 | dindImage: cfg.DindImage, 103 | disableDind: cfg.DisableDind, 104 | namespace: cfg.Namespace, 105 | imagePullSecrets: cfg.ImagePullSecrets, 106 | podTemplate: pod, 107 | containerTemplate: podtemplate.ContainerFromPod(pod), 108 | kuburl: cfg.KubedockURL, 109 | timeOut: int(cfg.TimeOut.Seconds()), 110 | disableServices: cfg.DisableServices, 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /internal/backend/util.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/joyrex2001/kubedock/internal/model/types" 10 | ) 11 | 12 | // toKubernetesValue will create a nice kubernetes string that can be used as a 13 | // key out of given random string. 14 | func (in *instance) toKubernetesKey(v string) string { 15 | return in.replaceValueWithPatterns(v, "", `^[^A-Za-z0-9]+`, `[^A-Za-z0-9-\./]`, `[-/]*$`) 16 | } 17 | 18 | // toKubernetesValue will create a nice kubernetes string that can be used as a 19 | // value out of given random string. 20 | func (in *instance) toKubernetesValue(v string) string { 21 | return in.replaceValueWithPatterns(v, "", `^[^A-Za-z0-9]+`, `[^A-Za-z0-9-\.]`, `-*$`) 22 | } 23 | 24 | // toKubernetesNamewill create a nice kubernetes string that can be used as a 25 | // value out of given random string. 26 | func (in *instance) toKubernetesName(v string) string { 27 | return in.replaceValueWithPatterns(v, "undef", `^[^A-Za-z0-9]+`, `[^A-Za-z0-9-]`, `-*$`) 28 | } 29 | 30 | func (in *instance) replaceValueWithPatterns(v, def string, pt ...string) string { 31 | for _, exp := range pt { 32 | re := regexp.MustCompile(exp) 33 | v = re.ReplaceAllString(v, ``) 34 | if len(v) > 63 { 35 | v = v[:63] 36 | } 37 | } 38 | if v == "" { 39 | v = def 40 | } 41 | return v 42 | } 43 | 44 | // readFile will read given file and return the contents as []byte. If 45 | // failed, it will return an error. 46 | func (in *instance) readFile(file string) ([]byte, error) { 47 | f, err := os.Open(file) 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer f.Close() 52 | return io.ReadAll(f) 53 | } 54 | 55 | // MapContainerTCPPorts will map random available ports to the ports 56 | // in the container. 57 | func (in *instance) MapContainerTCPPorts(tainr *types.Container) error { 58 | OUTER: 59 | for _, pp := range tainr.GetContainerTCPPorts() { 60 | // skip explicitly bound ports 61 | for src, dst := range tainr.HostPorts { 62 | if src > 0 && dst == pp { 63 | continue OUTER 64 | } 65 | } 66 | addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:0") 67 | if err != nil { 68 | return err 69 | } 70 | l, err := net.ListenTCP("tcp", addr) 71 | if err != nil { 72 | return err 73 | } 74 | tainr.MapPort(l.Addr().(*net.TCPAddr).Port, pp) 75 | defer l.Close() 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/backend/util_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/joyrex2001/kubedock/internal/model/types" 7 | ) 8 | 9 | func TestToKubernetesValue(t *testing.T) { 10 | tests := []struct { 11 | in string 12 | key string 13 | value string 14 | name string 15 | }{ 16 | {in: "__-abc", key: "abc", value: "abc", name: "abc"}, 17 | {in: "/a/b/c", key: "a/b/c", value: "abc", name: "abc"}, 18 | { 19 | in: "StrategicMars", 20 | key: "StrategicMars", 21 | value: "StrategicMars", 22 | name: "StrategicMars", 23 | }, 24 | { 25 | in: "2107007e-b7c8-df23-18fb-6a6f79726578", 26 | key: "2107007e-b7c8-df23-18fb-6a6f79726578", 27 | value: "2107007e-b7c8-df23-18fb-6a6f79726578", 28 | name: "2107007e-b7c8-df23-18fb-6a6f79726578", 29 | }, 30 | { 31 | in: "0123456789012345678901234567890123456789012345678901234567890123456789", 32 | key: "012345678901234567890123456789012345678901234567890123456789012", 33 | value: "012345678901234567890123456789012345678901234567890123456789012", 34 | name: "012345678901234567890123456789012345678901234567890123456789012", 35 | }, 36 | { 37 | in: "StrategicMars-", 38 | key: "StrategicMars", 39 | value: "StrategicMars", 40 | name: "StrategicMars", 41 | }, 42 | { 43 | in: "StrategicMars/-", 44 | key: "StrategicMars", 45 | value: "StrategicMars", 46 | name: "StrategicMars", 47 | }, 48 | { 49 | in: "2107007e-b7c8-df23-18fb-6a6f79726578", 50 | key: "2107007e-b7c8-df23-18fb-6a6f79726578", 51 | value: "2107007e-b7c8-df23-18fb-6a6f79726578", 52 | name: "2107007e-b7c8-df23-18fb-6a6f79726578", 53 | }, 54 | { 55 | in: "app.kubernetes.io/name", 56 | key: "app.kubernetes.io/name", 57 | value: "app.kubernetes.ioname", 58 | name: "appkubernetesioname", 59 | }, 60 | { 61 | in: "", 62 | key: "", 63 | value: "", 64 | name: "undef", 65 | }, 66 | } 67 | 68 | for i, tst := range tests { 69 | kub := &instance{} 70 | key := kub.toKubernetesKey(tst.in) 71 | if key != tst.key { 72 | t.Errorf("failed test %d - expected key %s, but got %s", i, tst.key, key) 73 | } 74 | value := kub.toKubernetesValue(tst.in) 75 | if value != tst.value { 76 | t.Errorf("failed test %d - expected value %s, but got %s", i, tst.value, value) 77 | } 78 | name := kub.toKubernetesName(tst.in) 79 | if name != tst.name { 80 | t.Errorf("failed test %d - expected name %s, but got %s", i, tst.name, name) 81 | } 82 | } 83 | } 84 | 85 | func TestMapContainerTCPPorts(t *testing.T) { 86 | tests := []struct { 87 | in *types.Container 88 | out map[int]int 89 | }{ 90 | { 91 | in: &types.Container{ExposedPorts: map[string]interface{}{ 92 | "303/tcp": 0, 93 | "909/tcp": 0, 94 | }, 95 | }, 96 | }, 97 | } 98 | kub := &instance{} 99 | for j := 0; j < 100; j++ { 100 | for i, tst := range tests { 101 | err := kub.MapContainerTCPPorts(tst.in) 102 | if err != nil { 103 | t.Errorf("failed test %d/%d - unexpected error: %s", i, j, err) 104 | } 105 | m := map[int]int{} 106 | for p := range tst.in.MappedPorts { 107 | if p < 1024 { 108 | t.Errorf("failed test %d/%d - invalid random port %d", i, j, p) 109 | break 110 | } 111 | if _, ok := m[p]; ok { 112 | t.Errorf("failed test %d/%d - tandom port collision, port %d already provided", i, j, p) 113 | break 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | func TestMapContainerTCPPortsSkipBoundPorts(t *testing.T) { 121 | kub := &instance{} 122 | c := &types.Container{ 123 | ExposedPorts: map[string]interface{}{ 124 | "303/tcp": 0, 125 | "80/tcp": 0, 126 | }, 127 | HostPorts: map[int]int{ 128 | -303: 303, 129 | 8080: 80, 130 | }, 131 | } 132 | if err := kub.MapContainerTCPPorts(c); err != nil { 133 | t.Fatalf("unexpected error: %v", err) 134 | } 135 | n := len(c.MappedPorts) 136 | if n != 1 { 137 | t.Errorf("expected 1 mapped port, but got %d", n) 138 | } 139 | for src, dst := range c.MappedPorts { 140 | if src == 0 { 141 | t.Errorf("expected non-zero source port, but got %d", src) 142 | } 143 | if dst != 303 { 144 | t.Errorf("expected destination port 303, but got %d", dst) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/config/kubernetes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "k8s.io/client-go/rest" 6 | "k8s.io/client-go/tools/clientcmd" 7 | 8 | // enable auth plugins 9 | _ "k8s.io/client-go/plugin/pkg/client/auth" 10 | 11 | "github.com/joyrex2001/kubedock/internal/util/stringid" 12 | ) 13 | 14 | // SystemLabels are the labels that are added to every kubedock 15 | // managed k8s resource and which should not be altered. 16 | var SystemLabels = map[string]string{ 17 | "kubedock": "true", 18 | "kubedock.id": "", 19 | } 20 | 21 | // DefaultLabels are the labels that are added to every kubedock 22 | // managed k8s resource. 23 | var DefaultLabels = map[string]string{} 24 | 25 | // DefaultAnnotations are the annotations that are added to every 26 | // kubedock managed k8s resource. 27 | var DefaultAnnotations = map[string]string{} 28 | 29 | // InstanceID contains an unique ID to identify this running instance. 30 | var InstanceID = "" 31 | 32 | // init will set an unique instance id in the default labels to identify 33 | // this speciffic instance of kubedock. 34 | func init() { 35 | InstanceID = stringid.TruncateID(stringid.GenerateRandomID()) 36 | SystemLabels["kubedock.id"] = InstanceID 37 | } 38 | 39 | // AddDefaultLabel will add a label that will be added to all containers 40 | // started by this kubedock instance. 41 | func AddDefaultLabel(key, value string) { 42 | DefaultLabels[key] = value 43 | } 44 | 45 | // AddDefaultAnnotation will add an annotation that will be added to all 46 | // containers started by this kubedock instance. 47 | func AddDefaultAnnotation(key, value string) { 48 | DefaultAnnotations[key] = value 49 | } 50 | 51 | // GetKubernetes will return a kubernetes config object. 52 | func GetKubernetes() (*rest.Config, error) { 53 | var err error 54 | config := &rest.Config{} 55 | kubeconfig := viper.GetString("kubernetes.kubeconfig") 56 | if kubeconfig != "" { 57 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 58 | } 59 | if kubeconfig == "" || err != nil { 60 | config, err = rest.InClusterConfig() 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | return config, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/config/system.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | const ( 8 | // ID is the id as advertised when calling /info 9 | ID = "com.joyrex2001.kubedock" 10 | // Name is the name as advertised when calling /info 11 | Name = "kubedock" 12 | // OS is the operating system as advertised when calling /info 13 | OS = "kubernetes" 14 | // DockerVersion is the docker version as advertised when calling /version 15 | DockerVersion = "1.25" 16 | // DockerMinAPIVersion is the minimum docker version as advertised when calling /version 17 | DockerMinAPIVersion = "1.25" 18 | // DockerAPIVersion is the api version as advertised when calling /version 19 | DockerAPIVersion = "1.25" 20 | // LibpodAPIVersion is the api version as advertised in libpod rest calls 21 | LibpodAPIVersion = "4.2.0" 22 | ) 23 | 24 | var ( 25 | // GoVersion is the version of go as advertised when calling /version 26 | GoVersion = runtime.Version() 27 | // GOOS is the runtime operating system as advertised when calling /version 28 | GOOS = runtime.GOOS 29 | // GOARCH is runtime architecture of go as advertised when calling /version 30 | GOARCH = runtime.GOARCH 31 | ) 32 | -------------------------------------------------------------------------------- /internal/config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | // Version as injected during buildtime. 9 | Version = "" 10 | // Build id asinjected during buildtime. 11 | Build = "" 12 | // Date of build as injected during buildtime. 13 | Date = "" 14 | // Image is the current image as injected during buildtime. 15 | Image = "joyrex2001/kubedock:latest" 16 | ) 17 | 18 | // VersionString will return a string with details of the current version. 19 | func VersionString() string { 20 | return fmt.Sprintf("kubedock %s (%s)", Version, Date) 21 | } 22 | -------------------------------------------------------------------------------- /internal/dind/dind.go: -------------------------------------------------------------------------------- 1 | package dind 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httputil" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/gin-gonic/gin" 13 | "k8s.io/klog" 14 | ) 15 | 16 | // Dind is the docker-in-docker proxy server. 17 | type Dind struct { 18 | kuburl string 19 | sock string 20 | } 21 | 22 | // New will instantiate a Dind object. 23 | func New(sock, kuburl string) *Dind { 24 | return &Dind{ 25 | kuburl: kuburl, 26 | sock: sock, 27 | } 28 | } 29 | 30 | // shutDownHandler will watch the path where the docker socket resides (in the 31 | // background). It will terminates the daemon (exit) when a file called 'shutdown' 32 | // is created/remove/touched. 33 | func (d *Dind) shutdownHandler() error { 34 | path := filepath.Dir(d.sock) 35 | shutdown := "shutdown" 36 | 37 | watcher, err := fsnotify.NewWatcher() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if err := watcher.Add(path); err != nil { 43 | return err 44 | } 45 | 46 | klog.Infof("watching %s/%s for activity", path, shutdown) 47 | 48 | go func() { 49 | for event := range watcher.Events { 50 | if strings.HasSuffix(event.Name, shutdown) { 51 | klog.Infof("exit signal received...") 52 | os.Exit(0) 53 | } 54 | } 55 | }() 56 | 57 | return nil 58 | } 59 | 60 | // proxy forwards the request to the configured kubedock endpoint. 61 | func (d *Dind) proxy(c *gin.Context) { 62 | remote, err := url.Parse(d.kuburl) 63 | if err != nil { 64 | klog.Errorf("error parsing kubedock url `%s`: %s", d.kuburl, err) 65 | return 66 | } 67 | 68 | proxy := httputil.NewSingleHostReverseProxy(remote) 69 | proxy.Director = func(req *http.Request) { 70 | req.Header = c.Request.Header 71 | req.Host = remote.Host 72 | req.URL.Scheme = remote.Scheme 73 | req.URL.Host = remote.Host 74 | req.URL.Path = c.Param("proxyPath") 75 | } 76 | 77 | proxy.ServeHTTP(c.Writer, c.Request) 78 | } 79 | 80 | // Run will initialize the http api server and start the proxy. 81 | func (d *Dind) Run() error { 82 | if !klog.V(2) { 83 | gin.SetMode(gin.ReleaseMode) 84 | } 85 | 86 | if err := d.shutdownHandler(); err != nil { 87 | return err 88 | } 89 | 90 | r := gin.Default() 91 | 92 | r.Any("/*proxyPath", d.proxy) 93 | 94 | klog.Infof("start listening on %s", d.sock) 95 | if err := r.RunUnix(d.sock); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "k8s.io/klog" 8 | 9 | "github.com/joyrex2001/kubedock/internal/util/stringid" 10 | ) 11 | 12 | // Events is the interface to publish and consume events. 13 | type Events interface { 14 | Subscribe() (<-chan Message, string) 15 | Unsubscribe(string) 16 | Publish(string, string, string) 17 | } 18 | 19 | // instance is the internal representation of the Events object. 20 | type instance struct { 21 | mu sync.Mutex 22 | observers map[string]chan Message 23 | } 24 | 25 | var singleton *instance 26 | var once sync.Once 27 | 28 | // New will create return the singleton Events instance. 29 | func New() Events { 30 | once.Do(func() { 31 | singleton = &instance{} 32 | singleton.observers = map[string]chan Message{} 33 | }) 34 | return singleton 35 | } 36 | 37 | // Publish will publish an event for given resource id and type for given action. 38 | func (e *instance) Publish(id, typ, action string) { 39 | msg := Message{ID: id, Type: typ, Action: action} 40 | msg.Time = time.Now().Unix() 41 | msg.TimeNano = time.Now().UnixNano() 42 | for _, ob := range e.observers { 43 | ob <- msg 44 | } 45 | } 46 | 47 | // Subscribe will subscribe to the events and will return a channel and an 48 | // unique identifier than can be used to unsubscribe when done. 49 | func (e *instance) Subscribe() (<-chan Message, string) { 50 | e.mu.Lock() 51 | defer e.mu.Unlock() 52 | out := make(chan Message, 1) 53 | id := stringid.GenerateRandomID() 54 | e.observers[id] = out 55 | klog.V(5).Infof("subscribing %s to events", id) 56 | return out, id 57 | } 58 | 59 | // Unsubscribe will unsubscribe given subscriber id from the events. 60 | func (e *instance) Unsubscribe(id string) { 61 | e.mu.Lock() 62 | defer e.mu.Unlock() 63 | klog.V(5).Infof("unsubscribing %s from events", id) 64 | delete(e.observers, id) 65 | } 66 | 67 | // Match will match given event filter conditions. 68 | func (m *Message) Match(typ string, key string, val string) bool { 69 | klog.V(5).Infof("match %s: %s = %s", typ, key, val) 70 | if typ == Type { 71 | return m.Type == key 72 | } 73 | if m.Type == typ { 74 | return m.ID == key 75 | } 76 | return true 77 | } 78 | -------------------------------------------------------------------------------- /internal/events/events_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/joyrex2001/kubedock/internal/server/filter" 7 | ) 8 | 9 | func TestEvents(t *testing.T) { 10 | events := New() 11 | msgid := "1234-5678" 12 | events.Publish(msgid, Container, Create) 13 | el, id := events.Subscribe() 14 | events.Publish(msgid, Container, Start) 15 | msg := <-el 16 | if msg.ID != msgid { 17 | t.Errorf("invalid msg-id %s - expected %s", msg.ID, msgid) 18 | } 19 | if msg.Type != Container { 20 | t.Errorf("invalid type %s - expected %s", msg.Type, Container) 21 | } 22 | if msg.Action != Start { 23 | t.Errorf("invalid type %s - expected %s", msg.Action, Start) 24 | } 25 | events.Unsubscribe(id) 26 | events.Publish(msgid, Container, Die) 27 | } 28 | 29 | func TestMatch(t *testing.T) { 30 | tests := []struct { 31 | filter string 32 | msg Message 33 | match bool 34 | }{ 35 | { 36 | filter: `{"type":{"image":true}}`, 37 | msg: Message{ID: "1234-5678", Type: "image", Action: "pull"}, 38 | match: true, 39 | }, 40 | { 41 | filter: `{"type":{"image":false}}`, 42 | msg: Message{ID: "1234-5678", Type: "image", Action: "pull"}, 43 | match: false, 44 | }, 45 | { 46 | filter: `{"type":{"container":true},"container":{"1234-5678":true}}`, 47 | msg: Message{ID: "1234-5678", Type: "container", Action: "create"}, 48 | match: true, 49 | }, 50 | { 51 | filter: `{"type":{"container":true},"container":{"1234-5678":true}}`, 52 | msg: Message{ID: "5678-1234", Type: "container", Action: "create"}, 53 | match: false, 54 | }, 55 | } 56 | for i, tst := range tests { 57 | filtr, _ := filter.New(tst.filter) 58 | if filtr.Match(&tst.msg) != tst.match { 59 | t.Errorf("failed test %d - unexpected match", i) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/events/types.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // Message is the structure that defines the details of the event. 4 | type Message struct { 5 | ID string 6 | Type string 7 | Action string 8 | Time int64 9 | TimeNano int64 10 | } 11 | 12 | const ( 13 | // Image defines the event/filter type image 14 | Image = "image" 15 | // Container defines the event/filter type container 16 | Container = "container" 17 | // Type defines the filter type Type 18 | Type = "type" 19 | // Create defines the event action create (container) 20 | Create = "create" 21 | // Start defines the event action start (container) 22 | Start = "start" 23 | // Die defines the event action die (container) 24 | Die = "die" 25 | // Detach defines the event action detach (container) 26 | Detach = "detach" 27 | // Pull defines the event action image (container) 28 | Pull = "pull" 29 | ) 30 | -------------------------------------------------------------------------------- /internal/main.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/spf13/viper" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/rest" 16 | "k8s.io/client-go/tools/leaderelection" 17 | "k8s.io/client-go/tools/leaderelection/resourcelock" 18 | "k8s.io/klog" 19 | 20 | "github.com/joyrex2001/kubedock/internal/backend" 21 | "github.com/joyrex2001/kubedock/internal/config" 22 | "github.com/joyrex2001/kubedock/internal/reaper" 23 | "github.com/joyrex2001/kubedock/internal/server" 24 | "github.com/joyrex2001/kubedock/internal/util/myip" 25 | ) 26 | 27 | // Main is the main entry point for starting this service. 28 | func Main() { 29 | klog.Infof("%s / kubedock.id=%s", config.VersionString(), config.InstanceID) 30 | 31 | cfg, err := config.GetKubernetes() 32 | if err != nil { 33 | klog.Fatalf("error instantiating kubernetes client: %s", err) 34 | } 35 | 36 | cli, err := kubernetes.NewForConfig(cfg) 37 | if err != nil { 38 | klog.Fatalf("error instantiating kubernetes client: %s", err) 39 | } 40 | 41 | kub, err := getBackend(cfg, cli) 42 | if err != nil { 43 | klog.Fatalf("error instantiating backend: %s", err) 44 | } 45 | 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | defer cancel() 48 | exitHandler(kub, cancel) 49 | 50 | // check if this instance requires locking of the namespace, if not 51 | // just start the show... 52 | if !viper.GetBool("lock.enabled") { 53 | run(ctx, kub) 54 | select {} 55 | } 56 | 57 | // exclusive mode, use the k8s leader election as a locking mechanism 58 | lock := &resourcelock.LeaseLock{ 59 | LeaseMeta: metav1.ObjectMeta{ 60 | Name: "kubedock-lock", 61 | Namespace: viper.GetString("kubernetes.namespace"), 62 | }, 63 | Client: cli.CoordinationV1(), 64 | LockConfig: resourcelock.ResourceLockConfig{ 65 | Identity: config.InstanceID, 66 | }, 67 | } 68 | 69 | ready := lockTimeoutHandler() 70 | leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ 71 | Lock: lock, 72 | ReleaseOnCancel: true, 73 | LeaseDuration: 60 * time.Second, 74 | RenewDeadline: 15 * time.Second, 75 | RetryPeriod: 5 * time.Second, 76 | Callbacks: leaderelection.LeaderCallbacks{ 77 | OnStartedLeading: func(ctx context.Context) { 78 | ready <- struct{}{} 79 | run(ctx, kub) 80 | }, 81 | OnStoppedLeading: func() { 82 | klog.V(3).Infof("lost lock on namespace %s", viper.GetString("kubernetes.namespace")) 83 | }, 84 | OnNewLeader: func(identity string) { 85 | klog.V(3).Infof("new leader elected: %s", identity) 86 | }, 87 | }, 88 | }) 89 | select {} 90 | } 91 | 92 | // getBackend will instantiate the kubedock kubernetes object. 93 | func getBackend(cfg *rest.Config, cli kubernetes.Interface) (backend.Backend, error) { 94 | ns := viper.GetString("kubernetes.namespace") 95 | initimg := viper.GetString("kubernetes.initimage") 96 | dindimg := viper.GetString("kubernetes.dindimage") 97 | disdind := viper.GetBool("kubernetes.disable-dind") 98 | timeout := viper.GetDuration("kubernetes.timeout") 99 | podtmpl := viper.GetString("kubernetes.pod-template") 100 | imgpsr := strings.ReplaceAll(viper.GetString("kubernetes.image-pull-secrets"), " ", "") 101 | dissvcs := viper.GetBool("disable-services") 102 | 103 | optlog := "" 104 | imgps := []string{} 105 | if imgpsr != "" { 106 | optlog = fmt.Sprintf(", pull secrets=%s", imgpsr) 107 | imgps = strings.Split(imgpsr, ",") 108 | } 109 | 110 | klog.Infof("kubernetes config: namespace=%s, initimage=%s, dindimage=%s, ready timeout=%s%s", ns, initimg, dindimg, timeout, optlog) 111 | if disdind { 112 | klog.Infof("docker-in-docker support disabled") 113 | } 114 | 115 | kuburl, err := getKubedockURL() 116 | if err != nil { 117 | return nil, err 118 | } 119 | klog.V(3).Infof("kubedock url: %s", kuburl) 120 | 121 | return backend.New(backend.Config{ 122 | Client: cli, 123 | RestConfig: cfg, 124 | Namespace: ns, 125 | InitImage: initimg, 126 | DindImage: dindimg, 127 | DisableDind: disdind, 128 | ImagePullSecrets: imgps, 129 | PodTemplate: podtmpl, 130 | KubedockURL: kuburl, 131 | TimeOut: timeout, 132 | DisableServices: dissvcs, 133 | }) 134 | } 135 | 136 | // getKubedockURL returns the uri that can be used externally to reach 137 | // this kubedock instance. 138 | func getKubedockURL() (string, error) { 139 | ip, err := myip.Get() 140 | if err != nil { 141 | return "", err 142 | } 143 | 144 | port := strings.Split(viper.GetString("server.listen-addr")+":", ":")[1] 145 | if port == "" { 146 | return "", fmt.Errorf("expected a port to be configured for listen-addr") 147 | } 148 | 149 | proto := "http" 150 | if viper.GetBool("server.tls-enable") { 151 | proto = "https" 152 | } 153 | return fmt.Sprintf("%s://%s:%s", proto, ip, port), nil 154 | } 155 | 156 | // run will start all components, based the settings initiated by cmd. 157 | func run(ctx context.Context, kub backend.Backend) { 158 | reapmax := viper.GetDuration("reaper.reapmax") 159 | rpr, err := reaper.New(reaper.Config{ 160 | KeepMax: reapmax, 161 | Backend: kub, 162 | }) 163 | if err != nil { 164 | klog.Fatalf("error instantiating reaper: %s", err) 165 | } 166 | 167 | klog.Infof("reaper started with max container age %s", reapmax) 168 | rpr.Start() 169 | 170 | if viper.GetBool("prune-start") { 171 | klog.Info("pruning all existing kubedock resources from namespace") 172 | if err := kub.DeleteAll(); err != nil { 173 | klog.Errorf("error pruning resources: %s", err) 174 | } 175 | } 176 | 177 | svr := server.New(kub) 178 | if err := svr.Run(ctx); err != nil { 179 | klog.Errorf("error instantiating server: %s", err) 180 | } 181 | } 182 | 183 | // lockTimeoutHandler will wait until the return channel recieved a message, 184 | // if this is not done within configured lock.timeout, it will exit the 185 | // process. 186 | func lockTimeoutHandler() chan struct{} { 187 | ready := make(chan struct{}, 1) 188 | go func() { 189 | for { 190 | tmr := time.NewTimer(viper.GetDuration("lock.timeout")) 191 | select { 192 | case <-ready: 193 | return 194 | case <-tmr.C: 195 | klog.Errorf("timeout acquiring lock") 196 | // no cleanup required, as nothing was done yet... 197 | os.Exit(1) 198 | } 199 | } 200 | }() 201 | return ready 202 | } 203 | 204 | // exitHandler will clean up resources before actually stopping kubedock. 205 | func exitHandler(kub backend.Backend, cancel context.CancelFunc) { 206 | sigc := make(chan os.Signal, 1) 207 | signal.Notify(sigc, 208 | syscall.SIGINT, 209 | syscall.SIGTERM, 210 | syscall.SIGQUIT) 211 | go func() { 212 | c := getExitCode(<-sigc) 213 | cancel() 214 | klog.Info("exit signal recieved, removing pods, configmaps and services") 215 | if err := kub.DeleteWithKubedockID(config.InstanceID); err != nil { 216 | klog.Errorf("error pruning resources: %s", err) 217 | } 218 | os.Exit(c) 219 | }() 220 | } 221 | 222 | // getExitCode will map signal to a meaningfull exit code. 223 | func getExitCode(sig os.Signal) int { 224 | c := 0 225 | switch sig := sig.(type) { 226 | case syscall.Signal: 227 | c = 128 + int(sig) 228 | } 229 | return c 230 | } 231 | -------------------------------------------------------------------------------- /internal/main_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/joyrex2001/kubedock/internal/util/myip" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | func TestGetKubedockURL(t *testing.T) { 12 | tests := []struct { 13 | listen string 14 | tls bool 15 | res string 16 | suc bool 17 | }{ 18 | {":1234", false, "http://{{IP}}:1234", true}, 19 | {":1234", true, "https://{{IP}}:1234", true}, 20 | {"1234", false, "", false}, 21 | } 22 | 23 | ip, _ := myip.Get() 24 | for i, tst := range tests { 25 | viper.Set("server.listen-addr", tst.listen) 26 | viper.Set("server.tls-enable", tst.tls) 27 | res, err := getKubedockURL() 28 | if tst.suc && err != nil { 29 | t.Errorf("failed test %d - unexpected error %s", i, err) 30 | } 31 | if !tst.suc && err == nil { 32 | t.Errorf("failed test %d - expected error, but succeeded instead", i) 33 | } 34 | tst.res = strings.ReplaceAll(tst.res, "{{IP}}", ip) 35 | if err == nil && res != tst.res { 36 | t.Errorf("failed test %d - expected %s, but got %s", i, tst.res, res) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/model/types/exec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Exec describes the details of an execute command. 8 | type Exec struct { 9 | ID string 10 | ContainerID string 11 | Cmd []string 12 | TTY bool 13 | Stdin bool 14 | Stdout bool 15 | Stderr bool 16 | ExitCode int 17 | Created time.Time 18 | } 19 | -------------------------------------------------------------------------------- /internal/model/types/file.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | ) 7 | 8 | // File describes the details of a file (content and mode). 9 | type File struct { 10 | FileMode os.FileMode 11 | Data bytes.Buffer 12 | } 13 | -------------------------------------------------------------------------------- /internal/model/types/image.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Image describes the details of an image. 8 | type Image struct { 9 | ID string 10 | ShortID string 11 | Name string 12 | ExposedPorts map[string]struct{} 13 | Created time.Time 14 | } 15 | -------------------------------------------------------------------------------- /internal/model/types/network.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Network describes the details of a network. 8 | type Network struct { 9 | ID string 10 | ShortID string 11 | Name string 12 | Labels map[string]string 13 | Created time.Time 14 | } 15 | 16 | // IsPredefined will return if the network is a pre-defined system network. 17 | func (nw *Network) IsPredefined() bool { 18 | return nw.Name == "bridge" || nw.Name == "null" || nw.Name == "host" 19 | } 20 | 21 | // Match will match given type with given key value pair. 22 | func (nw *Network) Match(typ string, key string, val string) bool { 23 | if typ == "name" { 24 | return nw.Name == key 25 | } 26 | if typ != "label" { 27 | return true 28 | } 29 | v, ok := nw.Labels[key] 30 | if !ok { 31 | return false 32 | } 33 | return v == val 34 | } 35 | -------------------------------------------------------------------------------- /internal/reaper/container.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "time" 5 | 6 | "k8s.io/klog" 7 | ) 8 | 9 | // CleanContainers will clean all lingering containers that are 10 | // older than the configured keepMax duration, and stored locally 11 | // in the in memory database. 12 | func (in *Reaper) CleanContainers() error { 13 | tainrs, err := in.db.GetContainers() 14 | if err != nil { 15 | return err 16 | } 17 | for _, tainr := range tainrs { 18 | if tainr.Created.Before(time.Now().Add(-in.keepMax)) { 19 | klog.V(3).Infof("deleting container: %s", tainr.ID) 20 | if err := in.kub.DeleteContainer(tainr); err != nil { 21 | // inform only, if deleting somehow failed, the 22 | // CleanContainersKubernetes will pick it up anyways 23 | klog.Warningf("error deleting deployment: %s", err) 24 | } 25 | if err := in.db.DeleteContainer(tainr); err != nil { 26 | return err 27 | } 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | // CleanContainersKubernetes will clean all lingering containers 34 | // that are older than the configured keepMax duration, and stored 35 | // not stored in the local in memory database. 36 | func (in *Reaper) CleanContainersKubernetes() error { 37 | return in.kub.DeleteOlderThan(in.keepMax + 15*time.Minute) 38 | } 39 | -------------------------------------------------------------------------------- /internal/reaper/container_test.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/spf13/viper" 8 | "k8s.io/client-go/kubernetes/fake" 9 | 10 | "github.com/joyrex2001/kubedock/internal/backend" 11 | "github.com/joyrex2001/kubedock/internal/model/types" 12 | ) 13 | 14 | func TestCleanContainers(t *testing.T) { 15 | kub, _ := backend.New(backend.Config{ 16 | Client: fake.NewSimpleClientset(), 17 | Namespace: viper.GetString("kubernetes.namespace"), 18 | InitImage: viper.GetString("kubernetes.initimage"), 19 | }) 20 | rp, _ := New(Config{ 21 | KeepMax: 20 * time.Millisecond, 22 | Backend: kub, 23 | }) 24 | rp.db.SaveContainer(&types.Container{}) 25 | if err := rp.CleanContainers(); err != nil { 26 | t.Errorf("unexpected error while cleaning containers: %s", err) 27 | } 28 | if excs, err := rp.db.GetContainers(); err != nil { 29 | t.Errorf("unexpected error while retrieving containers: %s", err) 30 | } else { 31 | if len(excs) != 1 { 32 | t.Errorf("expected 1 container, but got %d", len(excs)) 33 | } 34 | } 35 | time.Sleep(100 * time.Millisecond) 36 | if err := rp.CleanContainers(); err != nil { 37 | t.Errorf("unexpected error while cleaning containers: %s", err) 38 | } 39 | if excs, err := rp.db.GetContainers(); err != nil { 40 | t.Errorf("unexpected error while retrieving containers: %s", err) 41 | } else { 42 | if len(excs) != 0 { 43 | t.Errorf("expected 0 container, but got %d", len(excs)) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/reaper/exec.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "time" 5 | 6 | "k8s.io/klog" 7 | ) 8 | 9 | var execReapMax = 5 * time.Minute 10 | 11 | // CleanExecs will clean all lingering execs that are older than 5 minutes. 12 | func (in *Reaper) CleanExecs() error { 13 | excs, err := in.db.GetExecs() 14 | if err != nil { 15 | return err 16 | } 17 | for _, exc := range excs { 18 | if exc.Created.Before(time.Now().Add(-execReapMax)) { 19 | klog.V(3).Infof("deleting exec: %s", exc.ID) 20 | if err := in.db.DeleteExec(exc); err != nil { 21 | return err 22 | } 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/reaper/exec_test.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/joyrex2001/kubedock/internal/model/types" 8 | ) 9 | 10 | func TestCleanExecs(t *testing.T) { 11 | rp, _ := New(Config{}) 12 | execReapMax = 20 * time.Millisecond 13 | rp.db.SaveExec(&types.Exec{}) 14 | if err := rp.CleanExecs(); err != nil { 15 | t.Errorf("unexpected error while cleaning execs: %s", err) 16 | } 17 | if excs, err := rp.db.GetExecs(); err != nil { 18 | t.Errorf("unexpected error while retrieving execs: %s", err) 19 | } else { 20 | if len(excs) != 1 { 21 | t.Errorf("expected 1 exec, but got %d", len(excs)) 22 | } 23 | } 24 | time.Sleep(100 * time.Millisecond) 25 | if err := rp.CleanExecs(); err != nil { 26 | t.Errorf("unexpected error while cleaning execs: %s", err) 27 | } 28 | if excs, err := rp.db.GetExecs(); err != nil { 29 | t.Errorf("unexpected error while retrieving execs: %s", err) 30 | } else { 31 | if len(excs) != 0 { 32 | t.Errorf("expected 0 exec, but got %d", len(excs)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/reaper/main.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "k8s.io/klog" 8 | 9 | "github.com/joyrex2001/kubedock/internal/backend" 10 | "github.com/joyrex2001/kubedock/internal/model" 11 | ) 12 | 13 | // Reaper is the object handles reaping of resources. 14 | type Reaper struct { 15 | db *model.Database 16 | keepMax time.Duration 17 | kub backend.Backend 18 | quit chan struct{} 19 | } 20 | 21 | var instance *Reaper 22 | var once sync.Once 23 | 24 | // Config is the configuration to be used for the Reaper proces. 25 | type Config struct { 26 | // KeepMax is the maximum age of resources, older resources are deleted. 27 | KeepMax time.Duration 28 | // Backend is the kubedock backend object. 29 | Backend backend.Backend 30 | } 31 | 32 | // New will create return the singleton Reaper instance. 33 | func New(cfg Config) (*Reaper, error) { 34 | var err error 35 | var db *model.Database 36 | once.Do(func() { 37 | instance = &Reaper{} 38 | db, err = model.New() 39 | instance.db = db 40 | instance.kub = cfg.Backend 41 | instance.keepMax = cfg.KeepMax 42 | }) 43 | return instance, err 44 | } 45 | 46 | // Start will start the reaper background process. 47 | func (in *Reaper) Start() { 48 | in.quit = make(chan struct{}) 49 | in.runloop() 50 | } 51 | 52 | // Stop will stop the reaper process. 53 | func (in *Reaper) Stop() { 54 | in.quit <- struct{}{} 55 | } 56 | 57 | // runloop will reap all lingering resources at a steady interval. 58 | func (in *Reaper) runloop() { 59 | go func() { 60 | for { 61 | tmr := time.NewTimer(time.Minute) 62 | select { 63 | case <-in.quit: 64 | return 65 | case <-tmr.C: 66 | klog.V(2).Info("start cleaning lingering objects...") 67 | in.clean() 68 | klog.V(2).Info("finished cleaning lingering objects...") 69 | } 70 | } 71 | }() 72 | } 73 | 74 | // clean will run all cleaners. 75 | func (in *Reaper) clean() { 76 | if err := in.CleanExecs(); err != nil { 77 | klog.Errorf("error cleaning execs: %s", err) 78 | } 79 | if err := in.CleanContainers(); err != nil { 80 | klog.Errorf("error cleaning containers: %s", err) 81 | } 82 | if err := in.CleanContainersKubernetes(); err != nil { 83 | klog.Errorf("error cleaning k8s containers: %s", err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /internal/reaper/main_test.go: -------------------------------------------------------------------------------- 1 | package reaper 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNew(t *testing.T) { 8 | in, _ := New(Config{}) 9 | for i := 0; i < 2; i++ { 10 | _in, _ := New(Config{}) 11 | if _in != in && in != nil { 12 | t.Errorf("New failed %d - got different instance", i) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/server/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | // Request is the json filter argument. 9 | type Request map[string]map[string]bool 10 | 11 | // Matcher is the interface for a Match method to test the filter. 12 | type Matcher interface { 13 | Match(string, string, string) bool 14 | } 15 | 16 | // Filter is the instace of this filter object 17 | type Filter struct { 18 | filters map[string][]keyval 19 | } 20 | 21 | // keyval contains a key value pair for matching 22 | type keyval struct { 23 | K string 24 | V string 25 | P bool 26 | } 27 | 28 | // New will return a new filter instance 29 | func New(f string) (*Filter, error) { 30 | in := &Filter{ 31 | filters: map[string][]keyval{}, 32 | } 33 | 34 | rq := Request{} 35 | if f != "" { 36 | if err := unmarshal(f, &rq); err != nil { 37 | return in, err 38 | } 39 | } 40 | 41 | for typ, filtrs := range rq { 42 | if _, ok := in.filters[typ]; !ok { 43 | in.filters[typ] = []keyval{} 44 | } 45 | for f, p := range filtrs { 46 | flds := strings.Split(f, "=") 47 | if len(flds) != 2 { 48 | in.filters[typ] = append(in.filters[typ], keyval{flds[0], "", p}) 49 | } else { 50 | in.filters[typ] = append(in.filters[typ], keyval{flds[0], flds[1], p}) 51 | } 52 | } 53 | } 54 | return in, nil 55 | } 56 | 57 | // unmarshal will unmarshal the given json to a Request type. Unfortunately, 58 | // depending on which docker-compose or "docker compose" you run, the request 59 | // may actually differ :-/ This method detects the format and marshalls either 60 | // to the same Request format. 61 | func unmarshal(dat string, rq *Request) error { 62 | if err := json.Unmarshal([]byte(dat), &rq); err == nil { 63 | return nil 64 | } 65 | 66 | // convert legacy format to new format... 67 | rql := map[string][]string{} 68 | if err := json.Unmarshal([]byte(dat), &rql); err != nil { 69 | return err 70 | } 71 | 72 | for typ, filtrs := range rql { 73 | (*rq)[typ] = map[string]bool{} 74 | for _, f := range filtrs { 75 | (*rq)[typ][f] = true 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // Match will call the matcher function and test if the object matches the 83 | // given key values. 84 | func (in *Filter) Match(matcher Matcher) bool { 85 | res := true 86 | for typ, filtrs := range in.filters { 87 | for _, f := range filtrs { 88 | if matcher.Match(typ, f.K, f.V) != f.P { 89 | res = false 90 | } 91 | } 92 | } 93 | return res 94 | } 95 | -------------------------------------------------------------------------------- /internal/server/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type matcher struct { 8 | res bool 9 | } 10 | 11 | func (m *matcher) Match(t, k, v string) bool { 12 | return m.res 13 | } 14 | 15 | func TestFilter(t *testing.T) { 16 | tests := []struct { 17 | filter string 18 | matcher *matcher 19 | suc bool 20 | match bool 21 | }{ 22 | { 23 | filter: `{"label": ["com.docker.compose.project=timesheet", "com.docker.compose.oneoff=False"]}`, 24 | suc: true, 25 | matcher: &matcher{false}, 26 | match: false, 27 | }, 28 | { 29 | filter: `{"label": ["com.docker.compose.project=timesheet", "com.docker.compose.oneoff=False"]}`, 30 | suc: true, 31 | matcher: &matcher{true}, 32 | match: true, 33 | }, 34 | { 35 | filter: `{el": ["com.docker.compose.project=timesheet", "com.docker.compose.oneoff=False"]}`, 36 | matcher: &matcher{false}, 37 | suc: false, 38 | match: true, 39 | }, 40 | { 41 | filter: `{"status": ["created", "exited"], "label": ["com.docker.compose.project=timesheet", "com.docker.compose.service=keycloak", "com.docker.compose.oneoff=False"]}`, 42 | matcher: &matcher{false}, 43 | suc: true, 44 | match: false, 45 | }, 46 | { 47 | filter: `{"label":{"com.docker.compose.project=timesheet":true}}`, 48 | matcher: &matcher{false}, 49 | suc: true, 50 | match: false, 51 | }, 52 | { 53 | filter: `{"label":{"com.docker.compose.project=timesheet":true}}`, 54 | matcher: &matcher{true}, 55 | suc: true, 56 | match: true, 57 | }, 58 | { 59 | filter: `{"label":{"com.docker.compose.project=timesheet":true},"name":{"mycontainer":true}}`, 60 | matcher: &matcher{true}, 61 | suc: true, 62 | match: true, 63 | }, 64 | { 65 | filter: `{"container":{"f577e780ec1756037235f0d5ba8081dfcdeb30327c75513f088953fa979b79b3":true},"type":{"container":true}}`, 66 | matcher: &matcher{true}, 67 | suc: true, 68 | match: true, 69 | }, 70 | { 71 | filter: ``, 72 | matcher: &matcher{false}, 73 | match: true, 74 | suc: true, 75 | }, 76 | } 77 | 78 | for i, tst := range tests { 79 | filtr, err := New(tst.filter) 80 | if tst.suc && err != nil { 81 | t.Errorf("failed test %d - unexpected error %s", i, err) 82 | } 83 | if !tst.suc && err == nil { 84 | t.Errorf("failed test %d - expected error, but succeeded instead", i) 85 | } 86 | if filtr != nil { 87 | if filtr.Match(tst.matcher) != tst.match { 88 | t.Errorf("failed test %d - unexpected match", i) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/server/httputil/util.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | "k8s.io/klog" 13 | ) 14 | 15 | // Error will return an error response in json. 16 | func Error(c *gin.Context, status int, err error) { 17 | klog.Errorf("error during request[%d]: %s", status, err) 18 | c.JSON(status, gin.H{ 19 | "message": err.Error(), 20 | }) 21 | } 22 | 23 | // NotImplemented will return a not implented response. 24 | func NotImplemented(c *gin.Context) { 25 | c.Writer.WriteHeader(http.StatusNotImplemented) 26 | } 27 | 28 | // NoContent will return a no content response. 29 | func NoContent(c *gin.Context) { 30 | c.Writer.WriteHeader(http.StatusNoContent) 31 | } 32 | 33 | // HijackConnection interrupts the http response writer to get the 34 | // underlying connection and operate with it. 35 | func HijackConnection(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { 36 | conn, _, err := w.(http.Hijacker).Hijack() 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | // Flush the options to make sure the client sets the raw mode 41 | _, _ = conn.Write([]byte{}) 42 | return conn, conn, nil 43 | } 44 | 45 | // UpgradeConnection will upgrade the Hijacked connection. 46 | func UpgradeConnection(r *http.Request, out io.Writer) { 47 | if _, ok := r.Header["Upgrade"]; ok { 48 | fmt.Fprint(out, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n") 49 | } else { 50 | fmt.Fprint(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n") 51 | } 52 | fmt.Fprint(out, "\r\n") 53 | } 54 | 55 | // CloseStreams ensures that a list for http streams are properly closed. 56 | func CloseStreams(streams ...interface{}) { 57 | for _, stream := range streams { 58 | if tcpc, ok := stream.(interface { 59 | CloseWrite() error 60 | }); ok { 61 | _ = tcpc.CloseWrite() 62 | } else if closer, ok := stream.(io.Closer); ok { 63 | _ = closer.Close() 64 | } 65 | } 66 | } 67 | 68 | // RequestLoggerMiddleware is a gin-gonic middleware that will log the 69 | // raw request. 70 | func RequestLoggerMiddleware() gin.HandlerFunc { 71 | return func(c *gin.Context) { 72 | var buf bytes.Buffer 73 | tee := io.TeeReader(c.Request.Body, &buf) 74 | body, _ := io.ReadAll(tee) 75 | c.Request.Body = io.NopCloser(&buf) 76 | klog.V(5).Infof("Request Headers: %#v", c.Request.Header) 77 | klog.V(4).Infof("Request Body: %s", string(body)) 78 | c.Next() 79 | } 80 | } 81 | 82 | // reponseWriter is the writer interface used by the ResponseLoggerMiddleware 83 | type reponseWriter struct { 84 | gin.ResponseWriter 85 | body *bytes.Buffer 86 | } 87 | 88 | // Write is the writer implementation used by the ResponseLoggerMiddleware 89 | func (w reponseWriter) Write(b []byte) (int, error) { 90 | w.body.Write(b) 91 | return w.ResponseWriter.Write(b) 92 | } 93 | 94 | // ResponseLoggerMiddleware is a gin-gonic middleware that will the raw response. 95 | func ResponseLoggerMiddleware() gin.HandlerFunc { 96 | return func(c *gin.Context) { 97 | w := &reponseWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 98 | c.Writer = w 99 | c.Next() 100 | klog.V(4).Infof("Response Body: %s", w.body.String()) 101 | } 102 | } 103 | 104 | // VersionAliasMiddleware is a gin-gonic middleware that will remove /v1.xx 105 | // and /v4.x.y from the url path (ignoring versioned apis). 106 | func VersionAliasMiddleware(router *gin.Engine) gin.HandlerFunc { 107 | red := regexp.MustCompile(`^/v1.[0-9]+`) 108 | rep := regexp.MustCompile(`^/v[1-9]+.[0-9]+.[0-9]+`) 109 | return func(c *gin.Context) { 110 | if strings.HasPrefix(c.Request.URL.Path, "/v1.") { 111 | c.Request.URL.Path = red.ReplaceAllString(c.Request.URL.Path, ``) 112 | router.HandleContext(c) 113 | c.Abort() 114 | return 115 | } 116 | if matched, _ := regexp.MatchString(`^/v[1-9]+.[0-9]+.[0-9]+`, c.Request.URL.Path); matched { 117 | c.Request.URL.Path = rep.ReplaceAllString(c.Request.URL.Path, ``) 118 | router.HandleContext(c) 119 | c.Abort() 120 | return 121 | } 122 | c.Next() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/server/main.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/spf13/viper" 9 | "k8s.io/klog" 10 | 11 | "github.com/joyrex2001/kubedock/internal/backend" 12 | "github.com/joyrex2001/kubedock/internal/server/httputil" 13 | "github.com/joyrex2001/kubedock/internal/server/routes" 14 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 15 | ) 16 | 17 | // Server is the API server. 18 | type Server struct { 19 | kub backend.Backend 20 | } 21 | 22 | // New will instantiate a Server object. 23 | func New(kub backend.Backend) *Server { 24 | return &Server{kub: kub} 25 | } 26 | 27 | // Run will initialize the http api server and configure all available 28 | // routers. 29 | func (s *Server) Run(ctx context.Context) error { 30 | if !klog.V(2) { 31 | gin.SetMode(gin.ReleaseMode) 32 | } 33 | 34 | router := s.getGinEngine() 35 | router.SetTrustedProxies(nil) 36 | 37 | socket := viper.GetString("server.socket") 38 | port := viper.GetString("server.listen-addr") 39 | 40 | tls := viper.GetBool("server.tls-enable") 41 | cert := viper.GetString("server.tls-cert-file") 42 | key := viper.GetString("server.tls-key-file") 43 | 44 | errch := make(chan error, 1) 45 | 46 | go func() { 47 | if tls { 48 | errch <- router.RunTLS(port, cert, key) 49 | } else { 50 | errch <- router.Run(port) 51 | } 52 | klog.Infof("api server started listening on %s", port) 53 | }() 54 | 55 | if socket != "" { 56 | go func() { 57 | errch <- router.RunUnix(socket) 58 | }() 59 | klog.Infof("api server started listening on %s", socket) 60 | } 61 | 62 | var err error 63 | select { 64 | case err = <-errch: 65 | break 66 | case <-ctx.Done(): 67 | break 68 | } 69 | 70 | if socket != "" { 71 | if err := os.Remove(socket); err != nil { 72 | klog.Errorf("error removing socket: %s", err) 73 | } 74 | } 75 | 76 | return err 77 | } 78 | 79 | // getGinEngine will return a gin.Engine router and configure the 80 | // appropriate middleware. 81 | func (s *Server) getGinEngine() *gin.Engine { 82 | router := gin.New() 83 | router.Use(httputil.VersionAliasMiddleware(router)) 84 | router.Use(gin.Logger()) 85 | router.Use(httputil.RequestLoggerMiddleware()) 86 | router.Use(httputil.ResponseLoggerMiddleware()) 87 | router.Use(gin.Recovery()) 88 | 89 | insp := viper.GetBool("registry.inspector") 90 | if insp { 91 | klog.Infof("image inspector enabled") 92 | } 93 | 94 | pfwrd := viper.GetBool("port-forward") 95 | if pfwrd { 96 | klog.Infof("port-forwarding services to 127.0.0.1") 97 | } 98 | 99 | revprox := viper.GetBool("reverse-proxy") 100 | if revprox && !pfwrd { 101 | klog.Infof("enabled reverse-proxy services via 0.0.0.0 on the kubedock host") 102 | } 103 | if revprox && pfwrd { 104 | klog.Infof("ignored reverse-proxy as port-forward is enabled") 105 | revprox = false 106 | } 107 | 108 | prea := viper.GetBool("pre-archive") 109 | if prea { 110 | klog.Infof("copying archives without starting containers enabled") 111 | } 112 | 113 | reqcpu := viper.GetString("kubernetes.request-cpu") 114 | if reqcpu != "" { 115 | klog.Infof("default cpu request: %s", reqcpu) 116 | } 117 | reqmem := viper.GetString("kubernetes.request-memory") 118 | if reqmem != "" { 119 | klog.Infof("default memory request: %s", reqmem) 120 | } 121 | 122 | runasuid := viper.GetString("kubernetes.runas-user") 123 | if runasuid != "" { 124 | klog.Infof("default runas user: %s", runasuid) 125 | } 126 | 127 | nodesel := viper.GetString("kubernetes.node-selector") 128 | if nodesel != "" { 129 | klog.Infof("default node selector: %s", nodesel) 130 | } 131 | 132 | pulpol := viper.GetString("kubernetes.pull-policy") 133 | klog.Infof("default image pull policy: %s", pulpol) 134 | 135 | sa := viper.GetString("kubernetes.service-account") 136 | klog.Infof("service account used in deployments: %s", sa) 137 | 138 | podprfx := viper.GetString("kubernetes.pod-name-prefix") 139 | klog.Infof("pod name prefix: %s", podprfx) 140 | 141 | ads := viper.GetInt64("kubernetes.active-deadline-seconds") 142 | 143 | icm := viper.GetBool("ignore-container-memory") 144 | 145 | klog.Infof("using namespace: %s", viper.GetString("kubernetes.namespace")) 146 | 147 | cr, err := common.NewContextRouter(s.kub, common.Config{ 148 | Inspector: insp, 149 | RequestCPU: reqcpu, 150 | RequestMemory: reqmem, 151 | ServiceAccount: sa, 152 | RunasUser: runasuid, 153 | NodeSelector: nodesel, 154 | PullPolicy: pulpol, 155 | PortForward: pfwrd, 156 | ReverseProxy: revprox, 157 | PreArchive: prea, 158 | NamePrefix: podprfx, 159 | ActiveDeadlineSeconds: ads, 160 | IgnoreContainerMemory: icm, 161 | }) 162 | if err != nil { 163 | klog.Errorf("error setting up context: %s", err) 164 | } 165 | 166 | routes.RegisterDockerRoutes(router, cr) 167 | routes.RegisterLibpodRoutes(router, cr) 168 | 169 | return router 170 | } 171 | -------------------------------------------------------------------------------- /internal/server/routes/common/archive.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "net/http" 12 | "strconv" 13 | 14 | "github.com/gin-gonic/gin" 15 | "k8s.io/klog" 16 | 17 | "github.com/joyrex2001/kubedock/internal/model/types" 18 | "github.com/joyrex2001/kubedock/internal/server/httputil" 19 | "github.com/joyrex2001/kubedock/internal/util/tar" 20 | ) 21 | 22 | // PutArchive - extract an archive of files or folders to a directory in a container. 23 | // https://docs.docker.com/engine/api/v1.41/#operation/PutContainerArchive 24 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/containers/operation/PutContainerArchiveLibpod 25 | // PUT "/containers/:id/archive" 26 | // PUT "/libpod/containers/:id/archive" 27 | func PutArchive(cr *ContextRouter, c *gin.Context) { 28 | id := c.Param("id") 29 | 30 | path := c.Query("path") 31 | if path == "" { 32 | httputil.Error(c, http.StatusBadRequest, fmt.Errorf("missing required path parameter")) 33 | return 34 | } 35 | 36 | ovw, _ := strconv.ParseBool(c.Query("noOverwriteDirNonDir")) 37 | if ovw { 38 | klog.Warning("noOverwriteDirNonDir is not supported, ignoring setting.") 39 | } 40 | 41 | cgid, _ := strconv.ParseBool(c.Query("copyUIDGID")) 42 | if cgid { 43 | klog.Warning("copyUIDGID is not supported, ignoring setting.") 44 | } 45 | 46 | tainr, err := cr.DB.GetContainer(id) 47 | if err != nil { 48 | httputil.Error(c, http.StatusNotFound, err) 49 | return 50 | } 51 | 52 | archive, err := io.ReadAll(c.Request.Body) 53 | if err != nil { 54 | httputil.Error(c, http.StatusNotFound, err) 55 | return 56 | } 57 | 58 | if !tainr.Running && !tainr.Completed && cr.Config.PreArchive && tar.IsSingleFileArchive(archive) { 59 | tainr.PreArchives = append(tainr.PreArchives, types.PreArchive{Path: path, Archive: archive}) 60 | klog.V(2).Infof("adding prearchive: %v", tainr.PreArchives) 61 | if err := cr.DB.SaveContainer(tainr); err != nil { 62 | httputil.Error(c, http.StatusInternalServerError, err) 63 | return 64 | } 65 | c.JSON(http.StatusOK, gin.H{ 66 | "message": "planned archive to be copied to container", 67 | }) 68 | return 69 | } 70 | 71 | if !tainr.Running && !tainr.Completed && !cr.Config.PreArchive { 72 | if err := StartContainer(cr, tainr); err != nil { 73 | httputil.Error(c, http.StatusInternalServerError, err) 74 | return 75 | } 76 | } 77 | 78 | reader, writer := io.Pipe() 79 | go func() { 80 | writer.Write(archive) 81 | writer.Close() 82 | }() 83 | 84 | if err := cr.Backend.CopyToContainer(tainr, reader, path); err != nil { 85 | httputil.Error(c, http.StatusInternalServerError, err) 86 | return 87 | } 88 | 89 | c.JSON(http.StatusOK, gin.H{ 90 | "message": "archive copied succesfully to container", 91 | }) 92 | } 93 | 94 | // HeadArchive - get information about files in a container. 95 | // https://docs.docker.com/engine/api/v1.41/#operation/ContainerArchiveInfo 96 | // HEAD "/containers/:id/archive" 97 | // HEAD "/libpod/containers/:id/archive" 98 | func HeadArchive(cr *ContextRouter, c *gin.Context) { 99 | id := c.Param("id") 100 | tainr, err := cr.DB.GetContainer(id) 101 | if err != nil { 102 | httputil.Error(c, http.StatusNotFound, err) 103 | return 104 | } 105 | 106 | path := c.Query("path") 107 | if path == "" { 108 | httputil.Error(c, http.StatusBadRequest, fmt.Errorf("missing required path parameter")) 109 | return 110 | } 111 | 112 | exists, err := cr.Backend.FileExistsInContainer(tainr, path) 113 | if err != nil { 114 | httputil.Error(c, http.StatusInternalServerError, err) 115 | return 116 | } 117 | 118 | if !exists { 119 | httputil.Error(c, http.StatusNotFound, fmt.Errorf("file not found")) 120 | return 121 | } 122 | 123 | mode, err := cr.Backend.GetFileModeInContainer(tainr, path) 124 | if err != nil { 125 | httputil.Error(c, http.StatusInternalServerError, err) 126 | return 127 | } 128 | 129 | stat, _ := json.Marshal(gin.H{"name": path, "linkTarget": path, "mode": mode}) 130 | 131 | c.Writer.WriteHeader(http.StatusOK) 132 | c.Writer.Header().Set("X-Docker-Container-Path-Stat", base64.StdEncoding.EncodeToString(stat)) 133 | } 134 | 135 | // GetArchive - get a tar archive of a resource in the filesystem of container id. 136 | // https://docs.docker.com/engine/api/v1.41/#operation/ContainerArchive 137 | // GET "/containers/:id/archive" 138 | // GET "/libpod/containers/:id/archive" 139 | func GetArchive(cr *ContextRouter, c *gin.Context) { 140 | id := c.Param("id") 141 | tainr, err := cr.DB.GetContainer(id) 142 | if err != nil { 143 | httputil.Error(c, http.StatusNotFound, err) 144 | return 145 | } 146 | 147 | path := c.Query("path") 148 | if path == "" { 149 | httputil.Error(c, http.StatusBadRequest, fmt.Errorf("missing required path parameter")) 150 | return 151 | } 152 | 153 | exists, err := cr.Backend.FileExistsInContainer(tainr, path) 154 | if err != nil { 155 | httputil.Error(c, http.StatusInternalServerError, err) 156 | return 157 | } 158 | 159 | if !exists { 160 | httputil.Error(c, http.StatusNotFound, fmt.Errorf("file not found")) 161 | return 162 | } 163 | 164 | var b bytes.Buffer 165 | if err := cr.Backend.CopyFromContainer(tainr, path, bufio.NewWriter(&b)); err != nil { 166 | httputil.Error(c, http.StatusInternalServerError, err) 167 | return 168 | } 169 | 170 | dat := b.Bytes() 171 | size, err := tar.GetTarSize(dat) 172 | if err != nil { 173 | httputil.Error(c, http.StatusInternalServerError, err) 174 | return 175 | } 176 | 177 | stat, _ := json.Marshal(gin.H{"name": path, "size": size, "mode": fs.ModePerm, "linkTarget": path, "mtime": "2021-01-01T20:00:00Z"}) 178 | 179 | c.Writer.WriteHeader(http.StatusOK) 180 | c.Writer.Header().Set("Content-Type", "application/x-tar") 181 | c.Writer.Header().Set("X-Docker-Container-Path-Stat", base64.StdEncoding.EncodeToString(stat)) 182 | c.Writer.Write(dat[:size]) 183 | } 184 | -------------------------------------------------------------------------------- /internal/server/routes/common/context.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "golang.org/x/time/rate" 5 | 6 | "github.com/joyrex2001/kubedock/internal/backend" 7 | "github.com/joyrex2001/kubedock/internal/events" 8 | "github.com/joyrex2001/kubedock/internal/model" 9 | ) 10 | 11 | const ( 12 | // PollRate defines maximum polling request per second towards the backend 13 | PollRate = 1 14 | // PollBurst defines maximum burst poll requests towards the backend 15 | PollBurst = 3 16 | ) 17 | 18 | // Config is the structure to instantiate a Router object 19 | type Config struct { 20 | // Inspector specifies if the image inspect feature is enabled 21 | Inspector bool 22 | // PortForward specifies if the the services should be port-forwarded 23 | PortForward bool 24 | // ReverseProxy enables a reverse-proxy to the services via 0.0.0.0 on the kubedock host 25 | ReverseProxy bool 26 | // RequestCPU contains an optional default k8s cpu request 27 | RequestCPU string 28 | // RequestMemory contains an optional default k8s memory request 29 | RequestMemory string 30 | // RunasUser contains the UID to run pods as 31 | RunasUser string 32 | // PullPolicy contains the default pull policy for images 33 | PullPolicy string 34 | // PreArchive will enable copying files without starting containers 35 | PreArchive bool 36 | // ServiceAccount contains the service account name to be used for running containers 37 | ServiceAccount string 38 | // ActiveDeadlineSeconds contains the active deadline seconds to be used for running containers 39 | ActiveDeadlineSeconds int64 40 | // NamePrefix contains a prefix for the names used for the container deployments (optional). 41 | NamePrefix string 42 | // NodeSelector contains a comma-separated list of key=value pairs that is used to schedule pods to specific nodes 43 | NodeSelector string 44 | // IgnoreContainerMemory is used to ignore Docker memory settings and use requests/limits from Kubedock config 45 | IgnoreContainerMemory bool 46 | } 47 | 48 | // ContextRouter is the object that contains shared context for the kubedock API endpoints. 49 | type ContextRouter struct { 50 | Config Config 51 | DB *model.Database 52 | Backend backend.Backend 53 | Events events.Events 54 | Limiter *rate.Limiter 55 | } 56 | 57 | // NewContextRouter will instantiate a ContextRouter object. 58 | func NewContextRouter(kub backend.Backend, cfg Config) (*ContextRouter, error) { 59 | db, err := model.New() 60 | if err != nil { 61 | return nil, err 62 | } 63 | cr := &ContextRouter{ 64 | Config: cfg, 65 | DB: db, 66 | Backend: kub, 67 | Events: events.New(), 68 | Limiter: rate.NewLimiter(PollRate, PollBurst), 69 | } 70 | return cr, nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/server/routes/common/exec.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | "k8s.io/klog" 11 | 12 | "github.com/joyrex2001/kubedock/internal/model/types" 13 | "github.com/joyrex2001/kubedock/internal/server/httputil" 14 | ) 15 | 16 | // ContainerExec - create an exec instance. 17 | // https://docs.docker.com/engine/api/v1.41/#operation/ContainerExec 18 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/exec/operation/ContainerExecLibpod 19 | // POST "/containers/:id/exec" 20 | // POST "/libpod/containers/:id/exec" 21 | func ContainerExec(cr *ContextRouter, c *gin.Context) { 22 | in := &ContainerExecRequest{} 23 | if err := json.NewDecoder(c.Request.Body).Decode(&in); err != nil { 24 | httputil.Error(c, http.StatusInternalServerError, err) 25 | return 26 | } 27 | 28 | if in.Env != nil && len(in.Env) > 0 { 29 | httputil.Error(c, http.StatusBadRequest, fmt.Errorf("env variables not supported")) 30 | return 31 | } 32 | 33 | if !in.Stdout && !in.Stderr { 34 | in.Stdout = true 35 | } 36 | 37 | id := c.Param("id") 38 | _, err := cr.DB.GetContainer(id) 39 | if err != nil { 40 | httputil.Error(c, http.StatusNotFound, err) 41 | return 42 | } 43 | 44 | exec := &types.Exec{ 45 | ContainerID: id, 46 | Cmd: in.Cmd, 47 | TTY: in.Tty, 48 | Stderr: in.Stderr, 49 | Stdout: in.Stdout, 50 | Stdin: in.Stdin, 51 | } 52 | if err := cr.DB.SaveExec(exec); err != nil { 53 | httputil.Error(c, http.StatusInternalServerError, err) 54 | return 55 | } 56 | 57 | c.JSON(http.StatusCreated, gin.H{ 58 | "Id": exec.ID, 59 | }) 60 | } 61 | 62 | // ExecInfo - return low-level information about an exec instance. 63 | // https://docs.docker.com/engine/api/v1.41/#operation/ExecInspect 64 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/exec/operation/ExecInspectLibpod 65 | // GET "/exec/:id/json" 66 | // GET "/libpod/exec/:id/json" 67 | func ExecInfo(cr *ContextRouter, c *gin.Context) { 68 | id := c.Param("id") 69 | exec, err := cr.DB.GetExec(id) 70 | if err != nil { 71 | httputil.Error(c, http.StatusNotFound, err) 72 | return 73 | } 74 | 75 | c.JSON(http.StatusOK, gin.H{ 76 | "ID": id, 77 | "OpenStderr": exec.Stderr, 78 | "OpenStdin": exec.Stdin, 79 | "OpenStdout": exec.Stdout, 80 | "Running": false, 81 | "ExitCode": exec.ExitCode, 82 | "ProcessConfig": gin.H{ 83 | "tty": exec.TTY, 84 | "arguments": exec.Cmd, 85 | "entrypoint": "", 86 | }, 87 | }) 88 | } 89 | 90 | // ExecStart - start an exec instance. 91 | // https://docs.docker.com/engine/api/v1.41/#operation/ExecStart 92 | // POST "/exec/:id/start" 93 | func ExecStart(cr *ContextRouter, c *gin.Context) { 94 | req := &ExecStartRequest{} 95 | if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil { 96 | httputil.Error(c, http.StatusInternalServerError, err) 97 | return 98 | } 99 | 100 | id := c.Param("id") 101 | exec, err := cr.DB.GetExec(id) 102 | if err != nil { 103 | httputil.Error(c, http.StatusNotFound, err) 104 | return 105 | } 106 | 107 | tainr, err := cr.DB.GetContainer(exec.ContainerID) 108 | if err != nil { 109 | httputil.Error(c, http.StatusNotFound, err) 110 | return 111 | } 112 | 113 | if req.Detach { 114 | go func() { 115 | code, err := cr.Backend.ExecContainer(tainr, exec, nil, io.Discard) 116 | if err != nil { 117 | klog.Errorf("error during exec: %s", err) 118 | return 119 | } 120 | exec.ExitCode = code 121 | if err := cr.DB.SaveExec(exec); err != nil { 122 | klog.Errorf("error during exec: %s", err) 123 | } 124 | }() 125 | c.JSON(http.StatusOK, gin.H{}) 126 | return 127 | } 128 | 129 | r := c.Request 130 | w := c.Writer 131 | w.WriteHeader(http.StatusOK) 132 | 133 | in, out, err := httputil.HijackConnection(w) 134 | if err != nil { 135 | klog.Errorf("error during hijack connection: %s", err) 136 | return 137 | } 138 | defer httputil.CloseStreams(in, out) 139 | httputil.UpgradeConnection(r, out) 140 | 141 | code, err := cr.Backend.ExecContainer(tainr, exec, in, out) 142 | if err != nil { 143 | klog.Errorf("error during exec: %s", err) 144 | return 145 | } 146 | exec.ExitCode = code 147 | if err := cr.DB.SaveExec(exec); err != nil { 148 | httputil.Error(c, http.StatusInternalServerError, err) 149 | return 150 | } 151 | } 152 | 153 | // ExecResize - start an exec instance. 154 | // https://docs.docker.com/engine/api/v1.41/#operation/ExecResize 155 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/exec/operation/ExecResizeLibpod 156 | // POST "/exec/:id/resize" 157 | // POST "/libpod/exec/:id/resize" 158 | func ExecResize(cr *ContextRouter, c *gin.Context) { 159 | id := c.Param("id") 160 | _, err := cr.DB.GetExec(id) 161 | if err != nil { 162 | httputil.Error(c, http.StatusNotFound, err) 163 | return 164 | } 165 | c.JSON(http.StatusOK, gin.H{}) 166 | } 167 | -------------------------------------------------------------------------------- /internal/server/routes/common/images.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/joyrex2001/kubedock/internal/config" 10 | "github.com/joyrex2001/kubedock/internal/model/types" 11 | "github.com/joyrex2001/kubedock/internal/server/httputil" 12 | ) 13 | 14 | // ImageList - list Images. Stubbed, not relevant on k8s. 15 | // https://docs.docker.com/engine/api/v1.41/#operation/ImageList 16 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/images/operation/ImageListLibpod 17 | // GET "/images/json" 18 | // GET "/libpod/images/json" 19 | func ImageList(cr *ContextRouter, c *gin.Context) { 20 | imgs, err := cr.DB.GetImages() 21 | if err != nil { 22 | httputil.Error(c, http.StatusInternalServerError, err) 23 | return 24 | } 25 | res := []gin.H{} 26 | for _, img := range imgs { 27 | name := img.Name 28 | if !strings.Contains(name, ":") { 29 | name = name + ":latest" 30 | } 31 | res = append(res, gin.H{"ID": img.ID, "Size": 0, "Created": img.Created.Unix(), "RepoTags": []string{name}}) 32 | } 33 | c.JSON(http.StatusOK, res) 34 | } 35 | 36 | // ImageJSON - return low-level information about an image. 37 | // https://docs.docker.com/engine/api/v1.41/#operation/ImageInspect 38 | // GET "/images/:image/json" 39 | func ImageJSON(cr *ContextRouter, c *gin.Context) { 40 | id := strings.TrimSuffix(c.Param("image")+c.Param("json"), "/json") 41 | img, err := cr.DB.GetImageByNameOrID(id) 42 | if err != nil { 43 | img = &types.Image{Name: id} 44 | if cr.Config.Inspector { 45 | pts, err := cr.Backend.GetImageExposedPorts(id) 46 | if err != nil { 47 | httputil.Error(c, http.StatusInternalServerError, err) 48 | return 49 | } 50 | img.ExposedPorts = pts 51 | } 52 | if err := cr.DB.SaveImage(img); err != nil { 53 | httputil.Error(c, http.StatusNotFound, err) 54 | return 55 | } 56 | } 57 | c.JSON(http.StatusOK, gin.H{ 58 | "Id": img.Name, 59 | "Architecture": config.GOARCH, 60 | "Created": img.Created.Format("2006-01-02T15:04:05Z"), 61 | "Size": 0, 62 | "ContainerConfig": gin.H{ 63 | "Image": img.Name, 64 | }, 65 | "Config": gin.H{ 66 | "Env": []string{}, 67 | }, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /internal/server/routes/common/logs.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "k8s.io/klog" 11 | 12 | "github.com/joyrex2001/kubedock/internal/backend" 13 | "github.com/joyrex2001/kubedock/internal/server/httputil" 14 | ) 15 | 16 | // ContainerLogs - get container logs. 17 | // https://docs.docker.com/engine/api/v1.41/#operation/ContainerLogs 18 | // POST "/containers/:id/logs" 19 | func ContainerLogs(cr *ContextRouter, c *gin.Context) { 20 | id := c.Param("id") 21 | // TODO: implement until 22 | 23 | tainr, err := cr.DB.GetContainer(id) 24 | if err != nil { 25 | httputil.Error(c, http.StatusNotFound, err) 26 | return 27 | } 28 | 29 | if !tainr.Running && !tainr.Completed { 30 | httputil.Error(c, http.StatusNotFound, fmt.Errorf("container %s is not running", tainr.ShortID)) 31 | return 32 | } 33 | 34 | r := c.Request 35 | w := c.Writer 36 | w.WriteHeader(http.StatusOK) 37 | 38 | follow, _ := strconv.ParseBool(c.Query("follow")) 39 | tailLines, _ := parseUint64(c.Query("tail")) 40 | sinceTime, _ := parseUnix(c.Query("since")) 41 | timestamps, _ := strconv.ParseBool(c.Query("timestamps")) 42 | 43 | logOpts := backend.LogOptions{ 44 | Follow: follow, 45 | SinceTime: sinceTime, 46 | Timestamps: timestamps, 47 | TailLines: tailLines, 48 | } 49 | 50 | if !follow { 51 | stop := make(chan struct{}, 1) 52 | if err := cr.Backend.GetLogs(tainr, &logOpts, stop, w); err != nil { 53 | httputil.Error(c, http.StatusInternalServerError, err) 54 | return 55 | } 56 | close(stop) 57 | return 58 | } 59 | 60 | in, out, err := httputil.HijackConnection(w) 61 | if err != nil { 62 | klog.Errorf("error during hijack connection: %s", err) 63 | return 64 | } 65 | defer httputil.CloseStreams(in, out) 66 | httputil.UpgradeConnection(r, out) 67 | 68 | stop := make(chan struct{}, 1) 69 | tainr.AddStopChannel(stop) 70 | 71 | if err := cr.Backend.GetLogs(tainr, &logOpts, stop, out); err != nil { 72 | klog.V(3).Infof("error retrieving logs: %s", err) 73 | return 74 | } 75 | } 76 | 77 | // Parses the input expecting an uint64 number as a string. 78 | func parseUint64(input string) (*uint64, error) { 79 | num, err := strconv.ParseUint(input, 10, 32) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return &num, nil 84 | } 85 | 86 | // Parses the input expecting a string representing number of seconds since the Epoch. 87 | func parseUnix(input string) (*time.Time, error) { 88 | num, err := strconv.ParseInt(input, 10, 32) 89 | if err != nil { 90 | return nil, err 91 | } 92 | result := time.Unix(num, 0) 93 | return &result, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/server/routes/common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // ContainerExecRequest represents the json structure that 4 | // is used for the /conteiner/:id/exec request. 5 | type ContainerExecRequest struct { 6 | Cmd []string `json:"Cmd"` 7 | Stdin bool `json:"AttachStdin"` 8 | Stdout bool `json:"AttachStdout"` 9 | Stderr bool `json:"AttachStderr"` 10 | Tty bool `json:"Tty"` 11 | Env []string `json:"Env"` 12 | } 13 | 14 | // ExecStartRequest represents the json structure that is 15 | // used for the /exec/:id/start request. 16 | type ExecStartRequest struct { 17 | Detach bool `json:"Detach"` 18 | Tty bool `json:"Tty"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/server/routes/common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "time" 5 | 6 | "k8s.io/klog" 7 | 8 | "github.com/joyrex2001/kubedock/internal/backend" 9 | "github.com/joyrex2001/kubedock/internal/model/types" 10 | ) 11 | 12 | // StartContainer will start given container and saves the appropriate state 13 | // in the database. 14 | func StartContainer(cr *ContextRouter, tainr *types.Container) error { 15 | state, err := cr.Backend.StartContainer(tainr) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | tainr.HostIP = "0.0.0.0" 21 | if cr.Config.PortForward { 22 | cr.Backend.CreatePortForwards(tainr) 23 | } else { 24 | if len(tainr.GetServicePorts()) > 0 { 25 | ip, err := cr.Backend.GetPodIP(tainr) 26 | if err != nil { 27 | return err 28 | } 29 | tainr.HostIP = ip 30 | if cr.Config.ReverseProxy { 31 | cr.Backend.CreateReverseProxies(tainr) 32 | } 33 | } 34 | } 35 | 36 | tainr.Stopped = false 37 | tainr.Killed = false 38 | tainr.Failed = (state == backend.DeployFailed) 39 | tainr.Completed = (state == backend.DeployCompleted) 40 | tainr.Running = (state == backend.DeployRunning) 41 | 42 | return cr.DB.SaveContainer(tainr) 43 | } 44 | 45 | // UpdateContainerStatus will check if the started container is finished and will 46 | // update the container database record accordingly. 47 | func UpdateContainerStatus(cr *ContextRouter, tainr *types.Container) { 48 | if tainr.Completed { 49 | return 50 | } 51 | if !cr.Limiter.Allow() { 52 | klog.V(2).Infof("rate-limited status request for container: %s", tainr.ID) 53 | return 54 | } 55 | status, err := cr.Backend.GetContainerStatus(tainr) 56 | if err != nil { 57 | klog.Warningf("container status error: %s", err) 58 | tainr.Failed = true 59 | } 60 | if status == backend.DeployCompleted { 61 | tainr.Finished = time.Now() 62 | tainr.Completed = true 63 | tainr.Running = false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/server/routes/docker.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/joyrex2001/kubedock/internal/server/httputil" 7 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 8 | "github.com/joyrex2001/kubedock/internal/server/routes/docker" 9 | ) 10 | 11 | // RegisterDockerRoutes will add all suported docker routes. 12 | func RegisterDockerRoutes(router *gin.Engine, cr *common.ContextRouter) { 13 | wrap := func(fn func(*common.ContextRouter, *gin.Context)) gin.HandlerFunc { 14 | return func(c *gin.Context) { 15 | fn(cr, c) 16 | } 17 | } 18 | 19 | router.GET("/info", wrap(docker.Info)) 20 | router.GET("/events", wrap(docker.Events)) 21 | router.GET("/version", wrap(docker.Version)) 22 | router.GET("/_ping", wrap(docker.Ping)) 23 | router.HEAD("/_ping", wrap(docker.Ping)) 24 | 25 | router.POST("/containers/create", wrap(docker.ContainerCreate)) 26 | router.POST("/containers/:id/start", wrap(common.ContainerStart)) 27 | router.POST("/containers/:id/attach", wrap(common.ContainerAttach)) 28 | router.POST("/containers/:id/stop", wrap(common.ContainerStop)) 29 | router.POST("/containers/:id/restart", wrap(common.ContainerRestart)) 30 | router.POST("/containers/:id/kill", wrap(common.ContainerKill)) 31 | router.POST("/containers/:id/wait", wrap(docker.ContainerWait)) 32 | router.POST("/containers/:id/rename", wrap(common.ContainerRename)) 33 | router.POST("/containers/:id/resize", wrap(common.ContainerResize)) 34 | router.DELETE("/containers/:id", wrap(docker.ContainerDelete)) 35 | router.GET("/containers/json", wrap(docker.ContainerList)) 36 | router.GET("/containers/:id/json", wrap(docker.ContainerInfo)) 37 | router.GET("/containers/:id/logs", wrap(common.ContainerLogs)) 38 | 39 | router.HEAD("/containers/:id/archive", wrap(common.HeadArchive)) 40 | router.GET("/containers/:id/archive", wrap(common.GetArchive)) 41 | router.PUT("/containers/:id/archive", wrap(common.PutArchive)) 42 | 43 | router.POST("/containers/:id/exec", wrap(common.ContainerExec)) 44 | router.POST("/exec/:id/start", wrap(common.ExecStart)) 45 | router.POST("/exec/:id/resize", wrap(common.ExecResize)) 46 | router.GET("/exec/:id/json", wrap(common.ExecInfo)) 47 | 48 | router.POST("/networks/create", wrap(docker.NetworksCreate)) 49 | router.POST("/networks/:id/connect", wrap(docker.NetworksConnect)) 50 | router.POST("/networks/:id/disconnect", wrap(docker.NetworksDisconnect)) 51 | router.GET("/networks", wrap(docker.NetworksList)) 52 | router.GET("/networks/:id", wrap(docker.NetworksInfo)) 53 | router.DELETE("/networks/:id", wrap(docker.NetworksDelete)) 54 | router.POST("/networks/prune", wrap(docker.NetworksPrune)) 55 | 56 | router.POST("/images/create", wrap(docker.ImageCreate)) 57 | router.GET("/images/json", wrap(common.ImageList)) 58 | router.GET("/images/:image/*json", wrap(common.ImageJSON)) 59 | router.POST("/images/prune", wrap(docker.ImagesPrune)) 60 | 61 | router.POST("/volumes/prune", wrap(docker.VolumesPrune)) 62 | 63 | // not supported docker api at the moment 64 | router.GET("/containers/:id/top", httputil.NotImplemented) 65 | router.GET("/containers/:id/changes", httputil.NotImplemented) 66 | router.GET("/containers/:id/export", httputil.NotImplemented) 67 | router.GET("/containers/:id/stats", httputil.NotImplemented) 68 | router.POST("/containers/:id/update", httputil.NotImplemented) 69 | router.POST("/containers/:id/pause", httputil.NotImplemented) 70 | router.POST("/containers/:id/unpause", httputil.NotImplemented) 71 | router.GET("/containers/:id/attach/ws", httputil.NotImplemented) 72 | router.POST("/containers/prune", httputil.NotImplemented) 73 | router.POST("/build", httputil.NotImplemented) 74 | router.GET("/volumes", httputil.NotImplemented) 75 | router.GET("/volumes/:id", httputil.NotImplemented) 76 | router.DELETE("/volumes/:id", httputil.NotImplemented) 77 | router.POST("/volumes/create", httputil.NotImplemented) 78 | router.POST("/images/load", httputil.NotImplemented) 79 | router.POST("/images/:image/*tag", httputil.NotImplemented) 80 | } 81 | -------------------------------------------------------------------------------- /internal/server/routes/docker/images.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/joyrex2001/kubedock/internal/events" 9 | "github.com/joyrex2001/kubedock/internal/model/types" 10 | "github.com/joyrex2001/kubedock/internal/server/httputil" 11 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 12 | ) 13 | 14 | // ImageCreate - create an image. 15 | // https://docs.docker.com/engine/api/v1.41/#operation/ImageCreate 16 | // POST "/images/create" 17 | func ImageCreate(cr *common.ContextRouter, c *gin.Context) { 18 | from := c.Query("fromImage") 19 | tag := c.Query("tag") 20 | if tag != "" { 21 | from = from + ":" + tag 22 | } 23 | img := &types.Image{Name: from} 24 | if cr.Config.Inspector { 25 | pts, err := cr.Backend.GetImageExposedPorts(from) 26 | if err != nil { 27 | httputil.Error(c, http.StatusInternalServerError, err) 28 | return 29 | } 30 | img.ExposedPorts = pts 31 | } 32 | if err := cr.DB.SaveImage(img); err != nil { 33 | httputil.Error(c, http.StatusInternalServerError, err) 34 | return 35 | } 36 | 37 | cr.Events.Publish(from, events.Image, events.Pull) 38 | 39 | c.JSON(http.StatusOK, gin.H{ 40 | "status": "Download complete", 41 | }) 42 | } 43 | 44 | // ImagesPrune - Delete unused images. 45 | // https://docs.docker.com/engine/api/v1.41/#operation/ImagePrune 46 | // POST "/images/prune" 47 | func ImagesPrune(cr *common.ContextRouter, c *gin.Context) { 48 | c.JSON(http.StatusCreated, gin.H{ 49 | "ImagesDeleted": []string{}, 50 | "SpaceReclaimed": 0, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /internal/server/routes/docker/networks.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "k8s.io/klog" 10 | 11 | "github.com/joyrex2001/kubedock/internal/model/types" 12 | "github.com/joyrex2001/kubedock/internal/server/filter" 13 | "github.com/joyrex2001/kubedock/internal/server/httputil" 14 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 15 | ) 16 | 17 | // NetworksList - list networks. 18 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkList 19 | // GET "/networks" 20 | func NetworksList(cr *common.ContextRouter, c *gin.Context) { 21 | netws, err := cr.DB.GetNetworks() 22 | if err != nil { 23 | httputil.Error(c, http.StatusInternalServerError, err) 24 | return 25 | } 26 | filtr, err := filter.New(c.Query("filters")) 27 | if err != nil { 28 | klog.V(5).Infof("unsupported filter: %s", err) 29 | } 30 | res := []gin.H{} 31 | for _, netw := range netws { 32 | if filtr.Match(netw) { 33 | tainrs := getContainersInNetwork(cr, netw) 34 | res = append(res, gin.H{ 35 | "Name": netw.Name, 36 | "ID": netw.ID, 37 | "Driver": "bridge", 38 | "Scope": "local", 39 | "Attachable": true, 40 | "Containers": tainrs, 41 | "Labels": netw.Labels, 42 | }) 43 | } 44 | } 45 | c.JSON(http.StatusOK, res) 46 | } 47 | 48 | // NetworksInfo - inspect a network. 49 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkInspect 50 | // GET "/network/:id" 51 | func NetworksInfo(cr *common.ContextRouter, c *gin.Context) { 52 | id := c.Param("id") 53 | netw, err := cr.DB.GetNetworkByNameOrID(id) 54 | if err != nil { 55 | httputil.Error(c, http.StatusNotFound, err) 56 | return 57 | } 58 | tainrs := getContainersInNetwork(cr, netw) 59 | c.JSON(http.StatusOK, gin.H{ 60 | "Name": netw.Name, 61 | "ID": netw.ID, 62 | "Driver": "bridge", 63 | "Scope": "local", 64 | "Attachable": true, 65 | "Containers": tainrs, 66 | "Labels": netw.Labels, 67 | }) 68 | } 69 | 70 | // NetworksCreate - create a network. 71 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkCreate 72 | // POST "/networks/create" 73 | func NetworksCreate(cr *common.ContextRouter, c *gin.Context) { 74 | in := &NetworkCreateRequest{} 75 | if err := json.NewDecoder(c.Request.Body).Decode(&in); err != nil { 76 | httputil.Error(c, http.StatusInternalServerError, err) 77 | return 78 | } 79 | netw := &types.Network{ 80 | Name: in.Name, 81 | Labels: in.Labels, 82 | } 83 | if err := cr.DB.SaveNetwork(netw); err != nil { 84 | httputil.Error(c, http.StatusInternalServerError, err) 85 | return 86 | } 87 | c.JSON(http.StatusCreated, gin.H{ 88 | "Id": netw.ID, 89 | }) 90 | } 91 | 92 | // NetworksDelete - remove a network. 93 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkDelete 94 | // DELETE "/networks/:id" 95 | func NetworksDelete(cr *common.ContextRouter, c *gin.Context) { 96 | id := c.Param("id") 97 | netw, err := cr.DB.GetNetworkByNameOrID(id) 98 | if err != nil { 99 | httputil.Error(c, http.StatusNotFound, err) 100 | return 101 | } 102 | 103 | if netw.IsPredefined() { 104 | httputil.Error(c, http.StatusForbidden, fmt.Errorf("%s is a pre-defined network and cannot be removed", netw.Name)) 105 | return 106 | } 107 | 108 | if len(getContainersInNetwork(cr, netw)) != 0 { 109 | httputil.Error(c, http.StatusForbidden, fmt.Errorf("cannot delete network, containers attachd")) 110 | return 111 | } 112 | 113 | if err := cr.DB.DeleteNetwork(netw); err != nil { 114 | httputil.Error(c, http.StatusNotFound, err) 115 | return 116 | } 117 | c.Writer.WriteHeader(http.StatusNoContent) 118 | } 119 | 120 | // NetworksConnect - connect a container to a network. 121 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkConnect 122 | // POST "/networks/:id/connect" 123 | func NetworksConnect(cr *common.ContextRouter, c *gin.Context) { 124 | in := &NetworkConnectRequest{} 125 | if err := json.NewDecoder(c.Request.Body).Decode(&in); err != nil { 126 | httputil.Error(c, http.StatusInternalServerError, err) 127 | return 128 | } 129 | id := c.Param("id") 130 | netw, err := cr.DB.GetNetworkByNameOrID(id) 131 | if err != nil { 132 | httputil.Error(c, http.StatusNotFound, err) 133 | return 134 | } 135 | tainr, err := cr.DB.GetContainer(in.Container) 136 | if err != nil { 137 | httputil.Error(c, http.StatusNotFound, err) 138 | return 139 | } 140 | 141 | tainr.ConnectNetwork(netw.ID) 142 | n := len(tainr.NetworkAliases) 143 | addNetworkAliases(tainr, in.EndpointConfig) 144 | 145 | if tainr.Running && n != len(tainr.NetworkAliases) { 146 | klog.Warningf("adding networkaliases to a running container, will not create new services...") 147 | } 148 | if err := cr.DB.SaveContainer(tainr); err != nil { 149 | httputil.Error(c, http.StatusInternalServerError, err) 150 | return 151 | } 152 | c.JSON(http.StatusCreated, gin.H{ 153 | "ID": netw.ID, 154 | }) 155 | } 156 | 157 | // NetworksDisconnect - connect a container to a network. 158 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkDisconnect 159 | // POST "/networks/:id/disconnect" 160 | func NetworksDisconnect(cr *common.ContextRouter, c *gin.Context) { 161 | in := &NetworkDisconnectRequest{} 162 | if err := json.NewDecoder(c.Request.Body).Decode(&in); err != nil { 163 | httputil.Error(c, http.StatusInternalServerError, err) 164 | return 165 | } 166 | id := c.Param("id") 167 | netw, err := cr.DB.GetNetworkByNameOrID(id) 168 | if err != nil { 169 | httputil.Error(c, http.StatusNotFound, err) 170 | return 171 | } 172 | tainr, err := cr.DB.GetContainer(in.Container) 173 | if err != nil { 174 | httputil.Error(c, http.StatusNotFound, err) 175 | return 176 | } 177 | if netw.IsPredefined() { 178 | httputil.Error(c, http.StatusInternalServerError, fmt.Errorf("can not disconnect from predefined network")) 179 | return 180 | } 181 | if err := tainr.DisconnectNetwork(netw.ID); err != nil { 182 | httputil.Error(c, http.StatusNotFound, err) 183 | return 184 | } 185 | if err := cr.DB.SaveContainer(tainr); err != nil { 186 | httputil.Error(c, http.StatusInternalServerError, err) 187 | return 188 | } 189 | c.Writer.WriteHeader(http.StatusNoContent) 190 | } 191 | 192 | // NetworksPrune - delete unused networks. 193 | // https://docs.docker.com/engine/api/v1.41/#operation/NetworkPrune 194 | // POST "/networks/prune" 195 | func NetworksPrune(cr *common.ContextRouter, c *gin.Context) { 196 | netws, err := cr.DB.GetNetworks() 197 | if err != nil { 198 | httputil.Error(c, http.StatusInternalServerError, err) 199 | return 200 | } 201 | 202 | names := []string{} 203 | for _, netw := range netws { 204 | if netw.IsPredefined() || len(getContainersInNetwork(cr, netw)) != 0 { 205 | continue 206 | } 207 | if err := cr.DB.DeleteNetwork(netw); err != nil { 208 | httputil.Error(c, http.StatusNotFound, err) 209 | return 210 | } 211 | names = append(names, netw.Name) 212 | } 213 | 214 | c.JSON(http.StatusCreated, gin.H{ 215 | "NetworksDeleted": names, 216 | }) 217 | } 218 | 219 | // getContainersInNetwork will return an array of containers in an array 220 | // of gin.H structs, containing the details of the container. 221 | func getContainersInNetwork(cr *common.ContextRouter, netw *types.Network) map[string]gin.H { 222 | res := map[string]gin.H{} 223 | tainrs, err := cr.DB.GetContainers() 224 | if err == nil { 225 | for _, tainr := range tainrs { 226 | if _, ok := tainr.Networks[netw.ID]; ok { 227 | res[tainr.ID] = gin.H{ 228 | "Name": tainr.Name, 229 | } 230 | } 231 | } 232 | } else { 233 | klog.Errorf("error retrieving containers: %s", err) 234 | } 235 | return res 236 | } 237 | -------------------------------------------------------------------------------- /internal/server/routes/docker/system.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "k8s.io/klog" 9 | 10 | "github.com/joyrex2001/kubedock/internal/config" 11 | "github.com/joyrex2001/kubedock/internal/server/filter" 12 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 13 | ) 14 | 15 | // Info - get system information. 16 | // https://docs.docker.com/engine/api/v1.41/#operation/SystemInfo 17 | // GET "/info" 18 | func Info(cr *common.ContextRouter, c *gin.Context) { 19 | labels := []string{} 20 | for k, v := range config.DefaultLabels { 21 | labels = append(labels, k+"="+v) 22 | } 23 | c.JSON(http.StatusOK, gin.H{ 24 | "ID": config.ID, 25 | "Name": config.Name, 26 | "ServerVersion": config.Version, 27 | "OperatingSystem": config.OS, 28 | "MemTotal": 0, 29 | "Labels": labels, 30 | }) 31 | } 32 | 33 | // Version - get version. 34 | // https://docs.docker.com/engine/api/v1.41/#operation/SystemVersion 35 | // GET "/version" 36 | func Version(cr *common.ContextRouter, c *gin.Context) { 37 | c.JSON(http.StatusOK, gin.H{ 38 | "Version": config.DockerVersion, 39 | "ApiVersion": config.DockerAPIVersion, 40 | "MinAPIVersion": config.DockerAPIVersion, 41 | "GitCommit": config.Build, 42 | "BuildTime": config.Date, 43 | "GoVersion": config.GoVersion, 44 | "Os": config.GOOS, 45 | "Arch": config.GOARCH, 46 | }) 47 | } 48 | 49 | // Ping - dummy endpoint you can use to test if the server is accessible. 50 | // https://docs.docker.com/engine/api/v1.41/#operation/SystemPing 51 | // HEAD "/_ping" 52 | // GET "/_ping" 53 | func Ping(cr *common.ContextRouter, c *gin.Context) { 54 | w := c.Writer 55 | w.Header().Set("API-Version", config.DockerAPIVersion) 56 | c.String(http.StatusOK, "OK") 57 | } 58 | 59 | // Events - Stream real-time events from the server. 60 | // https://docs.docker.com/engine/api/v1.41/#tag/System/operation/SystemEvents 61 | // GET "/events" 62 | func Events(cr *common.ContextRouter, c *gin.Context) { 63 | w := c.Writer 64 | w.Header().Set("Content-Type", "application/json") 65 | w.WriteHeader(http.StatusOK) 66 | w.Flush() 67 | 68 | filtr, err := filter.New(c.Query("filters")) 69 | if err != nil { 70 | klog.V(5).Infof("unsupported filter: %s", err) 71 | } 72 | 73 | enc := json.NewEncoder(w) 74 | el, id := cr.Events.Subscribe() 75 | for { 76 | select { 77 | case <-c.Request.Context().Done(): 78 | cr.Events.Unsubscribe(id) 79 | return 80 | case msg := <-el: 81 | if filtr.Match(&msg) { 82 | klog.V(5).Infof("sending message to %s", id) 83 | enc.Encode(gin.H{ 84 | "id": msg.ID, 85 | "Type": msg.Type, 86 | "Status": msg.Action, 87 | "Action": msg.Action, 88 | "Actor": gin.H{ 89 | "ID": msg.ID, 90 | }, 91 | "scope": "local", 92 | "time": msg.Time, 93 | "timeNano": msg.TimeNano, 94 | }) 95 | w.Flush() 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/server/routes/docker/types.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | // ContainerCreateRequest represents the json structure that 4 | // is used for the /container/create post endpoint. 5 | type ContainerCreateRequest struct { 6 | Name string `json:"name"` 7 | Hostname string `json:"Hostname"` 8 | Image string `json:"image"` 9 | ExposedPorts map[string]interface{} `json:"ExposedPorts"` 10 | Labels map[string]string `json:"Labels"` 11 | Entrypoint []string `json:"Entrypoint"` 12 | Cmd []string `json:"Cmd"` 13 | Env []string `json:"Env"` 14 | User string `json:"User"` 15 | HostConfig HostConfig `json:"HostConfig"` 16 | NetworkConfig NetworkingConfig `json:"NetworkingConfig"` 17 | } 18 | 19 | // NetworkCreateRequest represents the json structure that 20 | // is used for the /networks/create post endpoint. 21 | type NetworkCreateRequest struct { 22 | Name string `json:"Name"` 23 | Labels map[string]string `json:"Labels"` 24 | } 25 | 26 | // NetworkConnectRequest represents the json structure that 27 | // is used for the /networks/:id/connect post endpoint. 28 | type NetworkConnectRequest struct { 29 | Container string `json:"container"` 30 | EndpointConfig EndpointConfig `json:"EndpointConfig"` 31 | } 32 | 33 | // NetworkDisconnectRequest represents the json structure that 34 | // is used for the /networks/:id/disconnect post endpoint. 35 | type NetworkDisconnectRequest struct { 36 | Container string `json:"container"` 37 | } 38 | 39 | // HostConfig contains to be mounted files from the host system. 40 | type HostConfig struct { 41 | Binds []string `json:"Binds"` 42 | Mounts []Mount `json:"Mounts"` 43 | PortBindings map[string][]PortBinding 44 | Memory int `json:"Memory"` 45 | NanoCpus int `json:"NanoCpus"` 46 | NetworkMode string `json:"NetworkMode"` 47 | } 48 | 49 | // PortBinding represents a binding between to a port 50 | type PortBinding struct { 51 | HostPort string `json:"HostPort"` 52 | } 53 | 54 | // NetworkingConfig contains network configuration 55 | type NetworkingConfig struct { 56 | EndpointsConfig map[string]EndpointConfig `json:"EndpointsConfig"` 57 | } 58 | 59 | // NetworkConfig contains network configuration 60 | type NetworkConfig struct { 61 | EndpointConfig EndpointConfig `json:"EndpointConfig"` 62 | } 63 | 64 | // EndpointConfig contains information about network endpoints 65 | type EndpointConfig struct { 66 | Aliases []string `json:"Aliases"` 67 | NetworkID string `json:"NetworkID"` 68 | } 69 | 70 | // Mount contains information about mounted volumes/bindings 71 | type Mount struct { 72 | Type string `json:"Type"` 73 | Source string `json:"Source"` 74 | Target string `json:"Target"` 75 | ReadOnly bool `json:"ReadOnly"` 76 | } 77 | -------------------------------------------------------------------------------- /internal/server/routes/docker/util.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/joyrex2001/kubedock/internal/model/types" 7 | ) 8 | 9 | // addNetworkAliases will add the networkaliases as defined in the provided 10 | // EndpointConfig to the container. 11 | func addNetworkAliases(tainr *types.Container, endp EndpointConfig) { 12 | aliases := []string{} 13 | done := map[string]string{tainr.ShortID: tainr.ShortID} 14 | for _, l := range [][]string{tainr.NetworkAliases, endp.Aliases} { 15 | for _, a := range l { 16 | if _, ok := done[a]; !ok { 17 | alias := strings.ToLower(a) 18 | aliases = append(aliases, alias) 19 | done[alias] = alias 20 | } 21 | } 22 | } 23 | tainr.NetworkAliases = aliases 24 | } 25 | -------------------------------------------------------------------------------- /internal/server/routes/docker/util_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/joyrex2001/kubedock/internal/model/types" 8 | ) 9 | 10 | func TestAddNetworkAliases(t *testing.T) { 11 | tests := []struct { 12 | tainr *types.Container 13 | endp EndpointConfig 14 | out []string 15 | portfw bool 16 | }{ 17 | { 18 | tainr: &types.Container{}, 19 | endp: EndpointConfig{Aliases: []string{"tb303"}}, 20 | out: []string{"tb303"}, 21 | }, 22 | { 23 | tainr: &types.Container{NetworkAliases: []string{"tb303"}}, 24 | endp: EndpointConfig{}, 25 | out: []string{"tb303"}, 26 | }, 27 | { 28 | tainr: &types.Container{NetworkAliases: []string{"tb303"}}, 29 | endp: EndpointConfig{Aliases: []string{"tb303"}}, 30 | out: []string{"tb303"}, 31 | }, 32 | { 33 | tainr: &types.Container{NetworkAliases: []string{"tb303", "tr909"}}, 34 | endp: EndpointConfig{Aliases: []string{"tb303"}}, 35 | out: []string{"tb303", "tr909"}, 36 | }, 37 | { 38 | tainr: &types.Container{NetworkAliases: []string{"tb303"}}, 39 | endp: EndpointConfig{Aliases: []string{"tb303", "tr909"}}, 40 | out: []string{"tb303", "tr909"}, 41 | }, 42 | } 43 | 44 | for i, tst := range tests { 45 | addNetworkAliases(tst.tainr, tst.endp) 46 | if !reflect.DeepEqual(tst.tainr.NetworkAliases, tst.out) { 47 | t.Errorf("failed test %d - expected %s, but got %s", i, tst.out, tst.tainr.NetworkAliases) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/server/routes/docker/volumes.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 9 | ) 10 | 11 | // VolumesPrune - Delete unused volumes. 12 | // https://docs.docker.com/engine/api/v1.41/#operation/VolumePrune 13 | // POST "/volumes/prune" 14 | func VolumesPrune(cr *common.ContextRouter, c *gin.Context) { 15 | c.JSON(http.StatusCreated, gin.H{ 16 | "VolumesDeleted": []string{}, 17 | "SpaceReclaimed": 0, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /internal/server/routes/libpod.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/joyrex2001/kubedock/internal/config" 9 | "github.com/joyrex2001/kubedock/internal/server/httputil" 10 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 11 | "github.com/joyrex2001/kubedock/internal/server/routes/libpod" 12 | ) 13 | 14 | // LibpodHeadersMiddleware is a gin-gonic middleware that will add http headers 15 | // that are relevant for libpod endpoints.` 16 | func LibpodHeadersMiddleware() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | if strings.Contains(c.Request.URL.Path, "/libpod/") { 19 | c.Writer.Header().Set("Libpod-API-Version", config.LibpodAPIVersion) 20 | } 21 | } 22 | } 23 | 24 | // RegisterLibpodRoutes will add all suported podman routes. 25 | func RegisterLibpodRoutes(router *gin.Engine, cr *common.ContextRouter) { 26 | wrap := func(fn func(*common.ContextRouter, *gin.Context)) gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | fn(cr, c) 29 | } 30 | } 31 | 32 | router.Use(LibpodHeadersMiddleware()) 33 | 34 | router.GET("/libpod/version", wrap(libpod.Version)) 35 | router.GET("/libpod/_ping", wrap(libpod.Ping)) 36 | router.HEAD("/libpod/_ping", wrap(libpod.Ping)) 37 | 38 | router.POST("/libpod/containers/create", wrap(libpod.ContainerCreate)) 39 | router.POST("/libpod/containers/:id/start", wrap(common.ContainerStart)) 40 | router.GET("/libpod/containers/:id/exists", wrap(libpod.ContainerExists)) 41 | router.POST("/libpod/containers/:id/attach", wrap(common.ContainerAttach)) 42 | router.POST("/libpod/containers/:id/stop", wrap(common.ContainerStop)) 43 | router.POST("/libpod/containers/:id/restart", wrap(common.ContainerRestart)) 44 | router.POST("/libpod/containers/:id/kill", wrap(common.ContainerKill)) 45 | router.POST("/libpod/containers/:id/wait", wrap(libpod.ContainerWait)) 46 | router.POST("/libpod/containers/:id/rename", wrap(common.ContainerRename)) 47 | router.POST("/libpod/containers/:id/resize", wrap(common.ContainerResize)) 48 | router.DELETE("/libpod/containers/:id", wrap(libpod.ContainerDelete)) 49 | router.GET("/libpod/containers/json", wrap(libpod.ContainerList)) 50 | router.GET("/libpod/containers/:id/json", wrap(libpod.ContainerInfo)) 51 | router.GET("/libpod/containers/:id/logs", wrap(common.ContainerLogs)) 52 | 53 | router.HEAD("/libpod/containers/:id/archive", wrap(common.HeadArchive)) 54 | router.GET("/libpod/containers/:id/archive", wrap(common.GetArchive)) 55 | router.PUT("/libpod/containers/:id/archive", wrap(common.PutArchive)) 56 | 57 | router.POST("/libpod/containers/:id/exec", wrap(common.ContainerExec)) 58 | router.POST("/libpod/exec/:id/start", wrap(common.ExecStart)) 59 | router.GET("/libpod/exec/:id/json", wrap(common.ExecInfo)) 60 | router.POST("/libpod/exec/:id/resize", wrap(common.ExecResize)) 61 | 62 | router.POST("/libpod/images/pull", wrap(libpod.ImagePull)) 63 | router.GET("/libpod/images/json", wrap(common.ImageList)) 64 | router.GET("/libpod/images/:image/*json", wrap(common.ImageJSON)) 65 | 66 | // not supported podman api at the moment 67 | router.GET("/libpod/info", httputil.NotImplemented) 68 | router.POST("/libpod/build", httputil.NotImplemented) 69 | router.POST("/libpod/images/load", httputil.NotImplemented) 70 | } 71 | -------------------------------------------------------------------------------- /internal/server/routes/libpod/images.go: -------------------------------------------------------------------------------- 1 | package libpod 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/joyrex2001/kubedock/internal/events" 9 | "github.com/joyrex2001/kubedock/internal/model/types" 10 | "github.com/joyrex2001/kubedock/internal/server/httputil" 11 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 12 | ) 13 | 14 | // ImagePull - pull one or more images from a container registry. 15 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/images/operation/ImagePullLibpod 16 | // POST "/libpod/images/pull" 17 | func ImagePull(cr *common.ContextRouter, c *gin.Context) { 18 | from := c.Query("reference") 19 | img := &types.Image{Name: from} 20 | if cr.Config.Inspector { 21 | pts, err := cr.Backend.GetImageExposedPorts(from) 22 | if err != nil { 23 | httputil.Error(c, http.StatusInternalServerError, err) 24 | return 25 | } 26 | img.ExposedPorts = pts 27 | } 28 | 29 | if err := cr.DB.SaveImage(img); err != nil { 30 | httputil.Error(c, http.StatusInternalServerError, err) 31 | return 32 | } 33 | 34 | cr.Events.Publish(from, events.Image, events.Pull) 35 | 36 | c.JSON(http.StatusOK, gin.H{ 37 | "Id": img.ID, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/server/routes/libpod/system.go: -------------------------------------------------------------------------------- 1 | package libpod 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/joyrex2001/kubedock/internal/config" 8 | "github.com/joyrex2001/kubedock/internal/server/routes/common" 9 | ) 10 | 11 | // Version - get version. 12 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/system/operation/SystemVersionLibpod 13 | // GET "/libpod/version" 14 | func Version(cr *common.ContextRouter, c *gin.Context) { 15 | c.JSON(http.StatusOK, gin.H{ 16 | "GitCommit": config.Build, 17 | "Os": config.OS, 18 | "Version": config.Version, 19 | }) 20 | } 21 | 22 | // Ping - dummy endpoint you can use to test if the server is accessible. 23 | // https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/system/operation/SystemPing 24 | // GET "/libpod/_ping" 25 | func Ping(cr *common.ContextRouter, c *gin.Context) { 26 | c.String(http.StatusOK, "OK") 27 | } 28 | -------------------------------------------------------------------------------- /internal/server/routes/libpod/types.go: -------------------------------------------------------------------------------- 1 | package libpod 2 | 3 | // ContainerCreateRequest represents the json structure that 4 | // is used for the /libpod/container/create post endpoint. 5 | type ContainerCreateRequest struct { 6 | Name string `json:"name"` 7 | Image string `json:"image"` 8 | Labels map[string]string `json:"Labels"` 9 | Entrypoint []string `json:"Entrypoint"` 10 | Command []string `json:"Command"` 11 | Env map[string]string `json:"Env"` 12 | User string `json:"User"` 13 | PortMappings []PortMapping `json:"portmappings"` 14 | Network map[string]NetworksProperty `json:"Networks"` 15 | Mounts []Mount `json:"mounts"` 16 | } 17 | 18 | // PortMapping describes how to map a port into the container. 19 | type PortMapping struct { 20 | ContainerPort int `json:"container_port"` 21 | HostIP string `json:"host_ip"` 22 | HostPort int `json:"host_port"` 23 | Protocol string `json:"protocol"` 24 | Range int `json:"range"` 25 | } 26 | 27 | // NetworksProperty describes the container networks. 28 | type NetworksProperty struct { 29 | Aliases []string `json:"aliases"` 30 | } 31 | 32 | // Mount describes how volumes should be mounted. 33 | type Mount struct { 34 | Source string `json:"source"` 35 | Destination string `json:"destination"` 36 | Type string `json:"type"` 37 | } 38 | -------------------------------------------------------------------------------- /internal/util/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | v1 "k8s.io/api/core/v1" 9 | "k8s.io/client-go/kubernetes" 10 | "k8s.io/client-go/kubernetes/scheme" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/remotecommand" 13 | "k8s.io/klog" 14 | ) 15 | 16 | // Request is the structure used as argument for RemoteCmd 17 | type Request struct { 18 | // Client is the kubernetes clientset 19 | Client kubernetes.Interface 20 | // RestConfig is the kubernetes config 21 | RestConfig *rest.Config 22 | // Pod is the selected pod for this port forwarding 23 | Pod v1.Pod 24 | // Cmd contains the command to be executed 25 | Cmd []string 26 | // Container contains the name of the container in which the cmd should be executed 27 | Container string 28 | // Stdin contains a Reader if stdin is required (nil if ignored) 29 | Stdin io.Reader 30 | // Stdout contains a Writer if stdout is required (nil if ignored) 31 | Stdout io.Writer 32 | // Stderr contains a Writer if stderr is required (nil if ignored) 33 | Stderr io.Writer 34 | // TTY will enable interactive tty mode (requires stdin) 35 | TTY bool 36 | } 37 | 38 | // RemoteCmd will execute given exec object in kubernetes. 39 | func RemoteCmd(req Request) error { 40 | r := req.Client.CoreV1().RESTClient().Post().Resource("pods"). 41 | Name(req.Pod.Name). 42 | Namespace(req.Pod.Namespace). 43 | SubResource("exec") 44 | 45 | r.VersionedParams(&corev1.PodExecOptions{ 46 | Container: req.Container, 47 | Command: req.Cmd, 48 | Stdin: req.Stdin != nil, 49 | Stdout: req.Stdout != nil, 50 | Stderr: req.Stderr != nil, 51 | TTY: req.Stdin != nil && req.TTY, 52 | }, scheme.ParameterCodec) 53 | 54 | ex, err := remotecommand.NewSPDYExecutor(req.RestConfig, "POST", r.URL()) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | klog.V(3).Infof("exec %s:%v", req.Pod.Name, req.Cmd) 60 | 61 | return ex.StreamWithContext(context.TODO(), remotecommand.StreamOptions{ 62 | Stdin: req.Stdin, 63 | Stdout: req.Stdout, 64 | Stderr: req.Stderr, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /internal/util/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/containers/image/v5/image" 8 | "github.com/containers/image/v5/transports/alltransports" 9 | "github.com/containers/image/v5/types" 10 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 11 | ) 12 | 13 | // InspectConfig will return an Image object with the configuration 14 | // of the specified image. (docker://docker.io/joyrex2001/kubedock:latest) 15 | func InspectConfig(name string) (*v1.Image, error) { 16 | sys := &types.SystemContext{ 17 | OSChoice: "linux", 18 | } 19 | 20 | ctx := context.Background() 21 | src, err := parseImageSource(ctx, sys, name) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer src.Close() 26 | 27 | img, err := image.FromUnparsedImage(ctx, sys, image.UnparsedInstance(src, nil)) 28 | if err != nil { 29 | return nil, fmt.Errorf("Error parsing manifest for image: %w", err) 30 | } 31 | 32 | config, err := img.OCIConfig(ctx) 33 | if err != nil { 34 | return nil, fmt.Errorf("Error reading OCI-formatted configuration data: %w", err) 35 | } 36 | return config, err 37 | } 38 | 39 | // parseImageSource converts image URL-like string to an ImageSource. 40 | // The caller must call .Close() on the returned ImageSource. 41 | func parseImageSource(ctx context.Context, sys *types.SystemContext, name string) (types.ImageSource, error) { 42 | ref, err := alltransports.ParseImageName(name) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return ref.NewImageSource(ctx, sys) 47 | } 48 | -------------------------------------------------------------------------------- /internal/util/ioproxy/ioproxy.go: -------------------------------------------------------------------------------- 1 | package ioproxy 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "k8s.io/klog" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // StdType is the type of standard stream 12 | // a writer can multiplex to. 13 | type StdType byte 14 | 15 | const ( 16 | // Stdin represents standard input stream type. 17 | Stdin StdType = iota 18 | // Stdout represents standard output stream type. 19 | Stdout 20 | // Stderr represents standard error steam type. 21 | Stderr 22 | ) 23 | 24 | // IoProxy is a proxy writer which adds the output prefix before writing data. 25 | type IoProxy struct { 26 | io.Writer 27 | out io.Writer 28 | prefix StdType 29 | buf []byte 30 | flusher bool 31 | lock *sync.Mutex 32 | } 33 | 34 | // New will return a new IoProxy instance. 35 | func New(w io.Writer, prefix StdType, lock *sync.Mutex) *IoProxy { 36 | return &IoProxy{ 37 | out: w, 38 | prefix: prefix, 39 | buf: []byte{}, 40 | lock: lock, 41 | } 42 | } 43 | 44 | // Write will write given data to the an internal buffer, which will be 45 | // flushed if a newline is encountered, of when the maximum size of the 46 | // buffer has been reached. 47 | func (w *IoProxy) Write(p []byte) (int, error) { 48 | w.lock.Lock() 49 | defer w.lock.Unlock() 50 | w.buf = append(w.buf, p...) 51 | for w.process() != 0 { 52 | } 53 | if len(w.buf) > 0 && !w.flusher { 54 | w.flusher = true 55 | go func() { 56 | time.Sleep(100 * time.Millisecond) 57 | w.Flush() 58 | }() 59 | } 60 | return len(p), nil 61 | } 62 | 63 | func (w *IoProxy) writeAll(writer io.Writer, buf []byte) error { 64 | for len(buf) > 0 { 65 | n, err := writer.Write(buf) 66 | if err != nil { 67 | return err 68 | } 69 | buf = buf[n:] 70 | } 71 | return nil 72 | } 73 | 74 | // process iterates over the buffer and writes chunks that end with 75 | // a newline character to the output writer. 76 | func (w *IoProxy) process() int { 77 | bufferLength := len(w.buf) 78 | // Iterate over the buffer to find newline characters 79 | for pos := 0; pos < bufferLength; pos++ { 80 | if w.buf[pos] == '\n' { 81 | w.write(w.buf[:pos+1]) 82 | w.buf = w.buf[pos+1:] 83 | return pos + 1 84 | } 85 | } 86 | return 0 87 | } 88 | 89 | // write will write data to the configured writer, using the correct header. 90 | func (w *IoProxy) write(p []byte) error { 91 | header := [8]byte{} 92 | header[0] = byte(w.prefix) 93 | binary.BigEndian.PutUint32(header[4:], uint32(len(p))) 94 | err := w.writeAll(w.out, header[:]) 95 | if err != nil { 96 | klog.Errorf("Error when writing docker log header: %v", err) 97 | return err 98 | } 99 | err = w.writeAll(w.out, p) 100 | if err != nil { 101 | klog.Errorf("Error ehen writing docker log content: %v", err) 102 | } 103 | return err 104 | } 105 | 106 | // Flush will write all buffer data still present. 107 | func (w *IoProxy) Flush() error { 108 | w.lock.Lock() 109 | defer w.lock.Unlock() 110 | if len(w.buf) == 0 { 111 | return nil 112 | } 113 | err := w.write(w.buf) 114 | w.buf = []byte{} 115 | w.flusher = false 116 | return err 117 | } 118 | -------------------------------------------------------------------------------- /internal/util/ioproxy/ioproxy_test.go: -------------------------------------------------------------------------------- 1 | package ioproxy 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | type ShortWriteBuffer struct { 10 | bytes.Buffer 11 | } 12 | 13 | func (buf *ShortWriteBuffer) Write(b []byte) (int, error) { 14 | if len(b) == 0 { 15 | return buf.Buffer.Write(b) 16 | } 17 | return buf.Buffer.Write(b[:1]) 18 | } 19 | 20 | func (buf *ShortWriteBuffer) Bytes() []byte { 21 | return buf.Buffer.Bytes() 22 | } 23 | 24 | type TestBuffer interface { 25 | Write(b []byte) (int, error) 26 | Bytes() []byte 27 | } 28 | 29 | func TestWrite(t *testing.T) { 30 | tests := []struct { 31 | write string 32 | read []byte 33 | flush []byte 34 | }{ 35 | { 36 | write: "hello\n\nto the bat-mobile\nlet's go", 37 | read: []byte{ 38 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xa, 39 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xa, 40 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x61, 0x74, 0x2d, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0xa, 41 | }, 42 | flush: []byte{ 43 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xa, 44 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xa, 45 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x61, 0x74, 0x2d, 0x6d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0xa, 46 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x6c, 0x65, 0x74, 0x27, 0x73, 0x20, 0x67, 0x6f, 47 | }, 48 | }, 49 | } 50 | for i, tst := range tests { 51 | // with manual flush 52 | writeToBufferfuncName(t, &bytes.Buffer{}, tst, i) 53 | // with manual flush and short writes 54 | writeToBufferfuncName(t, &ShortWriteBuffer{}, tst, i) 55 | 56 | // without manual flushing 57 | // There is no automatic flushing. Automatic caused an issue where data could be written to 58 | // the gin.Context after the request was finished and the gin.Context was returned to the pool. 59 | // This causes issues where sometime a length 0 byte array (with 8 byte stream header was written 60 | // to another connection that reused the gin.Context from the pool. 61 | } 62 | } 63 | 64 | func writeToBufferfuncName(t *testing.T, buf TestBuffer, tst struct { 65 | write string 66 | read []byte 67 | flush []byte 68 | }, i int) { 69 | iop := New(buf, Stdout, &sync.Mutex{}) 70 | iop.Write([]byte(tst.write)) 71 | if !bytes.Equal(buf.Bytes(), tst.read) { 72 | t.Errorf("failed read %d - expected %v, but got %v", i, tst.read, buf.Bytes()) 73 | } 74 | iop.Flush() 75 | if !bytes.Equal(buf.Bytes(), tst.flush) { 76 | t.Errorf("failed flush %d - expected %v, but got %v", i, tst.flush, buf.Bytes()) 77 | } 78 | if len(iop.buf) > 0 { 79 | t.Errorf("failed flush %d - buffer not empty...", i) 80 | } 81 | } 82 | 83 | func TestLargeLine(t *testing.T) { 84 | buf := &bytes.Buffer{} 85 | iop := New(buf, Stdout, &sync.Mutex{}) 86 | 87 | data := make([]byte, 1350) 88 | for i := 0; i < len(data); i++ { 89 | data[i] = 65 90 | } 91 | data[1349] = 10 92 | iop.Write(data) 93 | iop.Flush() 94 | 95 | if len(buf.Bytes()) != 1350+8 { 96 | t.Errorf("failed large line test - buffer size was not linesize + header (%d) but %d", 1350+8, len(buf.Bytes())) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /internal/util/md2text/md2text.go: -------------------------------------------------------------------------------- 1 | package md2text 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // ToText will convert given markdown to a ascii text and 11 | // wraps the text to fit within given width. 12 | func ToText(text string, cols int) string { 13 | res := "" 14 | scanner := bufio.NewScanner(strings.NewReader(text)) 15 | raw := false 16 | render := true 17 | table := []string{} 18 | for scanner.Scan() { 19 | line := scanner.Text() 20 | if strings.HasPrefix(line, "```") { 21 | raw = !raw 22 | continue 23 | } 24 | 25 | if strings.HasPrefix(line, "[skip_render_start]") { 26 | render = false 27 | continue 28 | } 29 | 30 | if strings.HasPrefix(line, "[skip_render_end]") { 31 | render = true 32 | continue 33 | } 34 | 35 | if !render { 36 | continue 37 | } 38 | 39 | if strings.HasPrefix(line, "|") { 40 | table = append(table, line) 41 | continue 42 | } else { 43 | if len(table) > 0 { 44 | res += renderTable(table) 45 | table = []string{} 46 | } 47 | } 48 | 49 | if !raw { 50 | line = convertHeader(line) 51 | res += wrapString(line, cols) 52 | } else { 53 | res += " " + line 54 | } 55 | res += "\n" 56 | } 57 | 58 | return res 59 | } 60 | 61 | // wrapString will wrap a string into multiple lines in order to make it 62 | // fit the given maximum column width. 63 | func wrapString(text string, cols int) string { 64 | res := "" 65 | line := "" 66 | for _, w := range strings.Split(text, " ") { 67 | if len(w)+len(line) < cols { 68 | if line != "" { 69 | line += " " 70 | } 71 | line += w 72 | } else { 73 | res += line 74 | line = "\n" + w 75 | } 76 | } 77 | res += line 78 | return res 79 | } 80 | 81 | // convertHeader will convert a markdown header to an 82 | // ascii alternative. 83 | func convertHeader(text string) string { 84 | re1 := regexp.MustCompile("(?m)^((#+) +(.*$))") 85 | sub := re1.FindAllStringSubmatch(text, -1) 86 | for i := range sub { 87 | ch := "" 88 | n := len(sub[i][3]) 89 | switch len(sub[i][2]) { 90 | case 1: 91 | ch = "\n" + strings.Repeat("=", n) 92 | case 2: 93 | ch = "\n" + strings.Repeat("-", n) 94 | } 95 | text = strings.ReplaceAll(text, sub[i][1], sub[i][3]+ch) 96 | } 97 | 98 | re2 := regexp.MustCompile(`(?m)\(http[^\)]*\)`) 99 | text = re2.ReplaceAllString(text, "") 100 | 101 | return text 102 | } 103 | 104 | // renderTable will render a markdown to text. 105 | func renderTable(rows []string) string { 106 | out := "" 107 | 108 | headers := strings.Split(strings.Trim(rows[0], "|"), "|") 109 | data := make([][]string, len(rows)-2) 110 | 111 | for i := 2; i < len(rows); i++ { 112 | data[i-2] = strings.Split(strings.Trim(rows[i], "|"), "|") 113 | } 114 | 115 | colWidths := make([]int, len(headers)) 116 | for i, header := range headers { 117 | colWidths[i] = len(header) 118 | for _, row := range data { 119 | if len(row[i]) > colWidths[i] { 120 | colWidths[i] = len(row[i]) 121 | } 122 | } 123 | } 124 | 125 | out += renderLine(colWidths) 126 | out += renderRow(headers, colWidths) 127 | out += renderLine(colWidths) 128 | for _, row := range data { 129 | out += renderRow(row, colWidths) 130 | } 131 | out += renderLine(colWidths) 132 | 133 | return out 134 | } 135 | 136 | // renderRow will render a row within a markdown table. 137 | func renderRow(row []string, colWidths []int) string { 138 | out := "" 139 | for i, cell := range row { 140 | out += fmt.Sprintf("| %-*s ", colWidths[i], cell) 141 | } 142 | out += "|\n" 143 | return out 144 | } 145 | 146 | // renderLine will render a divider line in a markdown table. 147 | func renderLine(colWidths []int) string { 148 | out := "" 149 | for _, width := range colWidths { 150 | out += "+" 151 | for i := 0; i < width+2; i++ { 152 | out += "-" 153 | } 154 | } 155 | out += "+\n" 156 | return out 157 | } 158 | -------------------------------------------------------------------------------- /internal/util/myip/myip.go: -------------------------------------------------------------------------------- 1 | package myip 2 | 3 | import ( 4 | "net" 5 | "os" 6 | 7 | "k8s.io/klog" 8 | ) 9 | 10 | // Get returns the IP address of the pod if running in Kubernetes, or 11 | // the IP address of the host's network interface. 12 | func Get() (string, error) { 13 | podIP := os.Getenv("POD_IP") 14 | if podIP != "" { 15 | return podIP, nil 16 | } 17 | 18 | interfaces, err := net.Interfaces() 19 | if err != nil { 20 | return "127.0.0.1", err 21 | } 22 | 23 | for _, iface := range interfaces { 24 | addrs, err := iface.Addrs() 25 | if err != nil { 26 | klog.V(2).Infof("error getting addresses for interface %s: %s", iface.Name, err) 27 | continue 28 | } 29 | 30 | for _, addr := range addrs { 31 | if ipNet, ok := addr.(*net.IPNet); ok { 32 | if !ipNet.IP.IsLoopback() && !ipNet.IP.IsLinkLocalUnicast() { 33 | return ipNet.IP.String(), nil 34 | } 35 | } 36 | } 37 | } 38 | 39 | return "127.0.0.1", nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/util/podtemplate/podtemplate.go: -------------------------------------------------------------------------------- 1 | package podtemplate 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/client-go/kubernetes/scheme" 9 | ) 10 | 11 | // PodFromFile will read a given file with pod definition and returns a corev1.Pod 12 | // accordingly. 13 | func PodFromFile(file string) (*corev1.Pod, error) { 14 | decode := scheme.Codecs.UniversalDeserializer().Decode 15 | stream, err := os.ReadFile(file) 16 | if err != nil { 17 | return nil, err 18 | } 19 | obj, gvk, err := decode(stream, nil, nil) 20 | if err != nil { 21 | return nil, err 22 | } 23 | if gvk.Kind == "Pod" { 24 | return obj.(*corev1.Pod), nil 25 | } 26 | return nil, fmt.Errorf("invalid podtemplate: %s", file) 27 | } 28 | 29 | // ContainerFromPod will return a corev1.Container that is based on the first 30 | // configured container in the given pod, which can be used as a template 31 | // for to be created containers. If no containers are present in the pod, 32 | // it will return an empty corev1.Container object instead. 33 | func ContainerFromPod(pod *corev1.Pod) corev1.Container { 34 | container := corev1.Container{} 35 | if len(pod.Spec.Containers) > 0 { 36 | container = pod.Spec.Containers[0] 37 | } 38 | return container 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/podtemplate/podtemplate_test.go: -------------------------------------------------------------------------------- 1 | package podtemplate 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPodFromFile(t *testing.T) { 8 | pod, err := PodFromFile("test/test_pod.yaml") 9 | if err != nil { 10 | t.Errorf("unexpected error: %s", err) 11 | } 12 | if pod == nil { 13 | t.Error("error unmarshalling pod") 14 | } 15 | if pod != nil && pod.Spec.ServiceAccountName != "kubedock" { 16 | t.Error("invalid serviceAccountName") 17 | } 18 | 19 | container := ContainerFromPod(pod) 20 | if container.Resources.Requests != nil { 21 | t.Error("unexpected resources in container template") 22 | } 23 | 24 | pod, err = PodFromFile("test/test_container.yaml") 25 | if err != nil { 26 | t.Errorf("unexpected error: %s", err) 27 | } 28 | container = ContainerFromPod(pod) 29 | if container.Resources.Requests == nil { 30 | t.Error("expected resources in container template") 31 | } else { 32 | reqmem := container.Resources.Requests.Memory().String() 33 | if reqmem != "64Mi" { 34 | t.Errorf("unexpected value for request.memory %s, expected 64Mi", reqmem) 35 | } 36 | } 37 | 38 | pod, err = PodFromFile("test/notfound.yaml") 39 | if pod != nil { 40 | t.Error("unexpected pod object") 41 | } 42 | if err == nil { 43 | t.Error("expected an error when file is not available") 44 | } 45 | 46 | pod, err = PodFromFile("test/test_invalid_kind.yaml") 47 | if pod != nil { 48 | t.Error("unexpected pod object") 49 | } 50 | if err == nil { 51 | t.Error("expected an error when kind is not a pod") 52 | } 53 | 54 | pod, err = PodFromFile("test/test_invalid.yaml") 55 | if pod != nil { 56 | t.Error("unexpected pod object") 57 | } 58 | if err == nil { 59 | t.Error("expected an error when file is invalid yaml") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/util/podtemplate/test/test_container.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | annotations: 5 | example: kubedock 6 | spec: 7 | containers: 8 | - resources: 9 | requests: 10 | memory: "64Mi" 11 | cpu: "10m" 12 | securityContext: 13 | allowPrivilegeEscalation: false -------------------------------------------------------------------------------- /internal/util/podtemplate/test/test_invalid.yaml: -------------------------------------------------------------------------------- 1 | Invalid yaml -------------------------------------------------------------------------------- /internal/util/podtemplate/test/test_invalid_kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | example: service 6 | -------------------------------------------------------------------------------- /internal/util/podtemplate/test/test_pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | annotations: 5 | example: kubedock 6 | spec: 7 | serviceAccountName: kubedock 8 | -------------------------------------------------------------------------------- /internal/util/portforward/logger.go: -------------------------------------------------------------------------------- 1 | package portforward 2 | 3 | import ( 4 | "io" 5 | 6 | "k8s.io/klog" 7 | ) 8 | 9 | type logger struct { 10 | io.Writer 11 | } 12 | 13 | // NewLogger will return a new logger instance. 14 | func NewLogger() io.Writer { 15 | return &logger{} 16 | } 17 | 18 | // Write will write the log using klog. 19 | func (w *logger) Write(p []byte) (int, error) { 20 | klog.V(3).Info(string(p)) 21 | return len(p), nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/util/portforward/portforward.go: -------------------------------------------------------------------------------- 1 | package portforward 2 | 3 | // Source: https://github.com/gianarb/kube-port-forward 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "path" 10 | 11 | v1 "k8s.io/api/core/v1" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/portforward" 14 | "k8s.io/client-go/transport/spdy" 15 | "k8s.io/klog" 16 | ) 17 | 18 | // Request is the structure used as argument for ToPod 19 | type Request struct { 20 | // RestConfig is the kubernetes config 21 | RestConfig *rest.Config 22 | // Pod is the selected pod for this port forwarding 23 | Pod v1.Pod 24 | // LocalPort is the local port that will be selected to expose the PodPort 25 | LocalPort int 26 | // PodPort is the target port for the pod 27 | PodPort int 28 | // StopCh is the channel used to manage the port forward lifecycle 29 | StopCh <-chan struct{} 30 | // ReadyCh communicates when the tunnel is ready to receive traffic 31 | ReadyCh chan struct{} 32 | } 33 | 34 | // ToPod will portforward to given pod. 35 | func ToPod(req Request) error { 36 | transport, upgrader, err := spdy.RoundTripperFor(req.RestConfig) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | logr := NewLogger() 42 | klog.Infof("start port-forward %d->%d", req.LocalPort, req.PodPort) 43 | 44 | url, err := getURLScheme(req) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, url) 50 | fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", req.LocalPort, req.PodPort)}, req.StopCh, req.ReadyCh, logr, logr) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return fw.ForwardPorts() 56 | } 57 | 58 | // getURLScheme will take given request and create a valid url scheme for use 59 | // by the portforward api. 60 | func getURLScheme(req Request) (*url.URL, error) { 61 | portfw := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", req.Pod.Namespace, req.Pod.Name) 62 | 63 | base, err := url.Parse(req.RestConfig.Host) 64 | if err != nil { 65 | return nil, fmt.Errorf("error parsing base URL: %w", err) 66 | } 67 | if base.Scheme == "" { 68 | base.Scheme = "https" 69 | } 70 | 71 | return &url.URL{Scheme: base.Scheme, Host: base.Host, Path: path.Join(base.Path, portfw)}, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/util/portforward/portforward_test.go: -------------------------------------------------------------------------------- 1 | package portforward 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/client-go/rest" 11 | ) 12 | 13 | func TestGetURLScheme(t *testing.T) { 14 | tests := []struct { 15 | in Request 16 | out *url.URL 17 | err bool 18 | }{ 19 | { 20 | in: Request{ 21 | Pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "abc", Name: "def"}}, 22 | RestConfig: &rest.Config{Host: "https://tst-cluster"}, 23 | }, 24 | out: &url.URL{ 25 | Host: "tst-cluster", 26 | Scheme: "https", 27 | Path: "/api/v1/namespaces/abc/pods/def/portforward", 28 | }, 29 | err: false, 30 | }, 31 | { 32 | in: Request{ 33 | Pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "ghi", Name: "jkl"}}, 34 | RestConfig: &rest.Config{Host: "https://tst-cluster/cluster-1"}, 35 | }, 36 | out: &url.URL{ 37 | Host: "tst-cluster", 38 | Scheme: "https", 39 | Path: "/cluster-1/api/v1/namespaces/ghi/pods/jkl/portforward", 40 | }, 41 | err: false, 42 | }, 43 | { 44 | in: Request{ 45 | Pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "abc", Name: "def"}}, 46 | RestConfig: &rest.Config{Host: ":tst-cluster"}, 47 | }, 48 | err: true, 49 | }, 50 | } 51 | 52 | for i, tst := range tests { 53 | out, err := getURLScheme(tst.in) 54 | if err != nil && !tst.err { 55 | t.Errorf("failed test %d - unexpected error %s", i, err) 56 | } 57 | if err == nil && tst.err { 58 | t.Errorf("failed test %d - expected error, but succeeded instead", i) 59 | } 60 | if !reflect.DeepEqual(out, tst.out) { 61 | t.Errorf("failed test %d - expected %v, but got %v", i, tst.out, out) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/util/reverseproxy/tcpproxy.go: -------------------------------------------------------------------------------- 1 | package reverseproxy 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "time" 8 | 9 | "k8s.io/klog" 10 | ) 11 | 12 | const retryRate = 5 // number of tries per second for retry scenarios 13 | const initialConnectTimeOut = 5 // number of seconds to try to wait before actually listening 14 | 15 | // Request is the structure used as argument for Proxy 16 | type Request struct { 17 | // LocalPort is the local port that will be selected for the reverse proxy 18 | LocalPort int 19 | // PodPort is the target port for the reverse proxy 20 | RemotePort int 21 | // RemoteIP is the target ip for the reverse proxy 22 | RemoteIP string 23 | // StopCh is the channel used to manage the reverse proxy lifecycle 24 | StopCh <-chan struct{} 25 | // MaxRetry is the maximum number of retries (equals to seconds) upon error 26 | // and initial connection. 27 | MaxRetry int 28 | } 29 | 30 | // Proxy will open a reverse tcp proxy, listening to the provided 31 | // local port and proxies this to the given remote ip and destination port. 32 | // based on: https://gist.github.com/vmihailenco/1380352 33 | func Proxy(req Request) error { 34 | local := fmt.Sprintf("0.0.0.0:%d", req.LocalPort) 35 | remote := fmt.Sprintf("%s:%d", req.RemoteIP, req.RemotePort) 36 | 37 | klog.Infof("start reverse-proxy %s->%s", local, remote) 38 | 39 | // this is a workaround to make sure that healthchecks based on log output, rather than 40 | // end-to-end connectivity, have a bit more slack setting up this connectivity; this 41 | // fixes liquibase read-timeouts whe using quarkus + postgres + liquibase. 42 | waitUntilRemoteAcceptsConnection(remote, initialConnectTimeOut) 43 | 44 | listener, err := net.Listen("tcp", local) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | done := false 50 | go func() { 51 | <-req.StopCh 52 | klog.Infof("stopped reverse-proxy %s->%s", local, remote) 53 | done = true 54 | listener.Close() 55 | }() 56 | 57 | go func() { 58 | for try := 0; try < req.MaxRetry*retryRate && !done; try++ { 59 | if remoteAcceptsConnection(remote) { 60 | klog.V(3).Infof("proxying from 127.0.0.1:%s -> %s", local, remote) 61 | break 62 | } else { 63 | time.Sleep(time.Second / retryRate) 64 | } 65 | } 66 | for !done { 67 | conn, err := listener.Accept() 68 | klog.V(3).Infof("accepted connection for %s to %s", local, remote) 69 | if err != nil { 70 | if !done { 71 | klog.Errorf("error accepting connection: %s", err) 72 | } 73 | continue 74 | } 75 | go handleConnection(conn, local, remote, req.MaxRetry) 76 | } 77 | return 78 | }() 79 | 80 | return nil 81 | } 82 | 83 | // handleConnection will proxy a single connection towards the given endpoint. If the initial 84 | // connection fails, it will retry with a maximum of 30 tries (equal to 30 seconds). It will 85 | // close the given connection when returned. 86 | func handleConnection(conn net.Conn, local, remote string, maxRetry int) { 87 | var err error 88 | var conn2 net.Conn 89 | for try := 0; try < maxRetry*retryRate; try++ { 90 | conn2, err = net.DialTimeout("tcp", remote, time.Second/retryRate) 91 | if err == nil { 92 | klog.V(3).Infof("handling connection for %s", local) 93 | go io.Copy(conn2, conn) 94 | io.Copy(conn, conn2) 95 | conn2.Close() 96 | conn.Close() 97 | return 98 | } 99 | klog.Warningf("error dialing %s: %s (attempt: %d)", remote, err, try) 100 | } 101 | klog.Errorf("error dialing %s: max retry attempts reached", remote) 102 | conn.Close() 103 | } 104 | 105 | // waitUntilAcceptConnection will wait until the given remote is accepting connections, 106 | // if given timeout seconds is passed, it will return a timeout error. 107 | func waitUntilRemoteAcceptsConnection(remote string, timeout int) error { 108 | for try := 0; try < timeout*retryRate; try++ { 109 | if !remoteAcceptsConnection(remote) { 110 | time.Sleep(time.Second / retryRate) 111 | continue 112 | } else { 113 | return nil 114 | } 115 | } 116 | return fmt.Errorf("timeout connecting to %s", remote) 117 | } 118 | 119 | // remoteAcceptsConnection will check if the given remote is accepting connections. 120 | func remoteAcceptsConnection(remote string) bool { 121 | conn, err := net.DialTimeout("tcp", remote, time.Second/retryRate) 122 | if err != nil { 123 | return false 124 | } 125 | conn.Close() 126 | return true 127 | } 128 | -------------------------------------------------------------------------------- /internal/util/reverseproxy/tcpproxy_test.go: -------------------------------------------------------------------------------- 1 | package reverseproxy 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func helloServer(host string, port int, stop chan struct{}) error { 13 | l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) 14 | if err != nil { 15 | return err 16 | } 17 | defer l.Close() 18 | 19 | done := false 20 | go func() { 21 | <-stop 22 | done = true 23 | l.Close() 24 | }() 25 | 26 | for { 27 | if done { 28 | return nil 29 | } 30 | conn, err := l.Accept() 31 | if err != nil { 32 | if done { 33 | return nil 34 | } 35 | return err 36 | } 37 | conn.Write([]byte("Hello!\n")) 38 | conn.Close() 39 | } 40 | } 41 | 42 | func callServer(host string, port int) (string, error) { 43 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) 44 | if err != nil { 45 | return "", err 46 | } 47 | defer conn.Close() 48 | buf := bufio.NewReader(conn) 49 | return buf.ReadString('\n') 50 | } 51 | 52 | func TestProxyNormal(t *testing.T) { 53 | stopP := make(chan struct{}, 1) 54 | req := Request{ 55 | LocalPort: 30390, 56 | RemoteIP: "127.0.0.1", 57 | RemotePort: 30391, 58 | StopCh: stopP, 59 | MaxRetry: 2, 60 | } 61 | 62 | if err := Proxy(req); err != nil { 63 | t.Errorf("unexpected error starting proxy: %s", err) 64 | } 65 | 66 | stopS := make(chan struct{}, 1) 67 | go func() { 68 | if err := helloServer("127.0.0.1", 30391, stopS); err != nil { 69 | t.Errorf("unexpected error running helloServer: %s", err) 70 | } 71 | }() 72 | 73 | <-time.After(time.Second) 74 | 75 | res, err := callServer("127.0.0.1", 30390) 76 | if err != nil { 77 | t.Errorf("unexpected error calling helloServer via proxy: %s", err) 78 | } 79 | 80 | if res != "Hello!\n" { 81 | t.Errorf("unexpected answer calling helloServer via proxy: %s", res) 82 | } 83 | 84 | stopP <- struct{}{} 85 | stopS <- struct{}{} 86 | } 87 | 88 | func TestProxyRefused(t *testing.T) { 89 | stopP := make(chan struct{}, 1) 90 | req := Request{ 91 | LocalPort: 30490, 92 | RemoteIP: "127.0.0.1", 93 | RemotePort: 30392, 94 | StopCh: stopP, 95 | MaxRetry: 1, 96 | } 97 | 98 | if err := Proxy(req); err != nil { 99 | t.Errorf("unexpected error starting proxy: %s", err) 100 | } 101 | 102 | _, err := callServer("127.0.0.1", 30490) 103 | if err == nil { 104 | t.Errorf("expected error calling helloServer via proxy but didn't get any") 105 | } 106 | 107 | stopP <- struct{}{} 108 | } 109 | 110 | func TestProxyNotReady(t *testing.T) { 111 | stopP := make(chan struct{}, 1) 112 | req := Request{ 113 | LocalPort: 30590, 114 | RemoteIP: "127.0.0.1", 115 | RemotePort: 30393, 116 | StopCh: stopP, 117 | MaxRetry: 2, 118 | } 119 | 120 | if err := Proxy(req); err != nil { 121 | t.Errorf("unexpected error starting proxy: %s", err) 122 | } 123 | 124 | <-time.After(time.Second) 125 | 126 | res, err := callServer("127.0.0.1", 30590) 127 | if err != io.EOF { 128 | t.Errorf("unexpected error calling helloServer via proxy: %s", err) 129 | } 130 | 131 | if res != "" { 132 | t.Errorf("unexpected answer calling helloServer via proxy: %s", res) 133 | } 134 | 135 | stopS := make(chan struct{}, 1) 136 | go func() { 137 | if err := helloServer("127.0.0.1", 30393, stopS); err != nil { 138 | t.Errorf("unexpected error running helloServer: %s", err) 139 | } 140 | }() 141 | 142 | <-time.After(time.Second) 143 | 144 | res, err = callServer("127.0.0.1", 30590) 145 | if err != nil { 146 | t.Errorf("unexpected error calling helloServer via proxy: %s", err) 147 | } 148 | 149 | if res != "Hello!\n" { 150 | t.Errorf("unexpected answer calling helloServer via proxy: %s", res) 151 | } 152 | 153 | stopP <- struct{}{} 154 | stopS <- struct{}{} 155 | } 156 | -------------------------------------------------------------------------------- /internal/util/stringid/stringid.go: -------------------------------------------------------------------------------- 1 | // Package stringid provides helper functions for dealing with string identifiers 2 | package stringid // import "github.com/moby/moby/pkg/stringid" 3 | 4 | import ( 5 | "crypto/rand" 6 | "encoding/hex" 7 | "fmt" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | const shortLen = 12 14 | 15 | var ( 16 | validShortID = regexp.MustCompile("^[a-f0-9]{12}$") 17 | validHex = regexp.MustCompile(`^[a-f0-9]{64}$`) 18 | ) 19 | 20 | // IsShortID determines if an arbitrary string *looks like* a short ID. 21 | func IsShortID(id string) bool { 22 | return validShortID.MatchString(id) 23 | } 24 | 25 | // TruncateID returns a shorthand version of a string identifier for convenience. 26 | // A collision with other shorthands is very unlikely, but possible. 27 | // In case of a collision a lookup with TruncIndex.Get() will fail, and the caller 28 | // will need to use a longer prefix, or the full-length Id. 29 | func TruncateID(id string) string { 30 | if i := strings.IndexRune(id, ':'); i >= 0 { 31 | id = id[i+1:] 32 | } 33 | if len(id) > shortLen { 34 | id = id[:shortLen] 35 | } 36 | return id 37 | } 38 | 39 | // GenerateRandomID returns a unique id. 40 | func GenerateRandomID() string { 41 | b := make([]byte, 32) 42 | for { 43 | if _, err := rand.Read(b); err != nil { 44 | panic(err) // This shouldn't happen 45 | } 46 | id := hex.EncodeToString(b) 47 | // if we try to parse the truncated for as an int and we don't have 48 | // an error then the value is all numeric and causes issues when 49 | // used as a hostname. ref #3869 50 | if _, err := strconv.ParseInt(TruncateID(id), 10, 64); err == nil { 51 | continue 52 | } 53 | return id 54 | } 55 | } 56 | 57 | // ValidateID checks whether an ID string is a valid image ID. 58 | func ValidateID(id string) error { 59 | if ok := validHex.MatchString(id); !ok { 60 | return fmt.Errorf("image ID %q is invalid", id) 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/util/stringid/stringid_test.go: -------------------------------------------------------------------------------- 1 | package stringid 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestGenerateRandomID(t *testing.T) { 9 | id := GenerateRandomID() 10 | 11 | if len(id) != 64 { 12 | t.Fatalf("Id returned is incorrect: %s", id) 13 | } 14 | } 15 | 16 | func TestShortenId(t *testing.T) { 17 | id := "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2" 18 | truncID := TruncateID(id) 19 | if truncID != "90435eec5c4e" { 20 | t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) 21 | } 22 | } 23 | 24 | func TestShortenSha256Id(t *testing.T) { 25 | id := "sha256:4e38e38c8ce0b8d9041a9c4fefe786631d1416225e13b0bfe8cfa2321aec4bba" 26 | truncID := TruncateID(id) 27 | if truncID != "4e38e38c8ce0" { 28 | t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) 29 | } 30 | } 31 | 32 | func TestShortenIdEmpty(t *testing.T) { 33 | id := "" 34 | truncID := TruncateID(id) 35 | if len(truncID) > len(id) { 36 | t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) 37 | } 38 | } 39 | 40 | func TestShortenIdInvalid(t *testing.T) { 41 | id := "1234" 42 | truncID := TruncateID(id) 43 | if len(truncID) != len(id) { 44 | t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) 45 | } 46 | } 47 | 48 | func TestIsShortIDNonHex(t *testing.T) { 49 | id := "some non-hex value" 50 | if IsShortID(id) { 51 | t.Fatalf("%s is not a short ID", id) 52 | } 53 | } 54 | 55 | func TestIsShortIDNotCorrectSize(t *testing.T) { 56 | id := strings.Repeat("a", shortLen+1) 57 | if IsShortID(id) { 58 | t.Fatalf("%s is not a short ID", id) 59 | } 60 | id = strings.Repeat("a", shortLen-1) 61 | if IsShortID(id) { 62 | t.Fatalf("%s is not a short ID", id) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/util/tar/concatreader.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import "io" 4 | 5 | // ConcatReader is an [io.Reader] that first returns data from a wrapped bytes slice and then continues to return data 6 | // from a wrapped [io.Reader] 7 | type ConcatReader struct { 8 | data []byte 9 | offset int 10 | reader io.Reader 11 | } 12 | 13 | // NewConcatReader creates a new ConcatReader instance that sequentially reads data from a provided byte slice 14 | // and then continues reading from an underlying io.Reader. 15 | func NewConcatReader(data []byte, reader io.Reader) *ConcatReader { 16 | return &ConcatReader{data: data, reader: reader} 17 | } 18 | 19 | func (r *ConcatReader) Read(p []byte) (int, error) { 20 | if r.offset >= len(r.data) { 21 | n, err := r.reader.Read(p) 22 | r.offset += n 23 | return n, err 24 | } 25 | n := copy(p, r.data[r.offset:]) 26 | r.offset += n 27 | return n, nil 28 | } 29 | 30 | // ReadBytes returns the number of read bytes 31 | func (r *ConcatReader) ReadBytes() int { 32 | return r.offset 33 | } 34 | -------------------------------------------------------------------------------- /internal/util/tar/concatreader_test.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestConcatReader(t *testing.T) { 11 | data := make([]byte, 10) 12 | _, err := rand.Read(data) 13 | if err != nil { 14 | t.Errorf("unexpected error: %s", err) 15 | } 16 | 17 | // when bytes slice is empty 18 | reader := NewConcatReader(nil, bytes.NewReader(data)) 19 | actual, err := io.ReadAll(reader) 20 | if err != nil { 21 | t.Errorf("unexpected error: %s", err) 22 | } 23 | if !bytes.Equal(data, actual) { 24 | t.Errorf("data mismatch: expected %s, got %s", string(data), string(actual)) 25 | } 26 | if reader.ReadBytes() != len(data) { 27 | t.Errorf("read bytes mismatch: expected %d, got %d", len(data), reader.ReadBytes()) 28 | } 29 | 30 | // when bytes slice is not empty 31 | reader = NewConcatReader(data[:4], bytes.NewReader(data[4:])) 32 | actual, err = io.ReadAll(reader) 33 | if err != nil { 34 | t.Errorf("unexpected error: %s", err) 35 | } 36 | if !bytes.Equal(data, actual) { 37 | t.Errorf("data mismatch: expected %s, got %s", string(data), string(actual)) 38 | } 39 | if reader.ReadBytes() != len(data) { 40 | t.Errorf("read bytes mismatch: expected %d, got %d", len(data), reader.ReadBytes()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/util/tar/reader.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/bzip2" 7 | "compress/gzip" 8 | "io" 9 | 10 | "github.com/ulikunitz/xz" 11 | ) 12 | 13 | // Reader is able to read tar archive uncompressed, compressed with gzip, xz, or bzip2 14 | type Reader struct { 15 | concatReader *ConcatReader 16 | tr *tar.Reader 17 | close func() error 18 | } 19 | 20 | // NewReader initializes a new Reader instance to process compressed or uncompressed archive data. 21 | // It takes an `io.Reader` as input, reads the first 5 bytes to detect the compression type, 22 | // and returns a `*Reader` configured to handle the detected compression format (e.g., gzip, 23 | // bzip2, xz, or none). 24 | func NewReader(reader io.Reader) (r *Reader, err error) { 25 | first5Bytes := make([]byte, 5) 26 | _, err = reader.Read(first5Bytes) 27 | if err != nil { 28 | if err != io.EOF { 29 | return 30 | } 31 | } 32 | r = &Reader{ 33 | concatReader: NewConcatReader(first5Bytes, reader), 34 | } 35 | switch detectCompressionType(first5Bytes) { 36 | case "gzip": 37 | zr, err := gzip.NewReader(r.concatReader) 38 | if err != nil { 39 | return nil, err 40 | } 41 | r.close = zr.Close 42 | r.tr = tar.NewReader(zr) 43 | case "bzip2": 44 | r.tr = tar.NewReader(bzip2.NewReader(r.concatReader)) 45 | case "xz": 46 | xzr, err := xz.NewReader(r.concatReader) 47 | if err != nil { 48 | return nil, err 49 | } 50 | r.tr = tar.NewReader(xzr) 51 | default: 52 | r.tr = tar.NewReader(r.concatReader) 53 | } 54 | return r, nil 55 | } 56 | 57 | // Next advances to the next entry in the tar archive. 58 | // The Header.Size determines how many bytes can be read for the next file. 59 | // Any remaining data in the current file is automatically discarded. 60 | // At the end of the archive, Next returns the error io.EOF. 61 | // 62 | // If Next encounters a non-local name (as defined by [filepath.IsLocal]) 63 | // and the GODEBUG environment variable contains `tarinsecurepath=0`, 64 | // Next returns the header with an [ErrInsecurePath] error. 65 | // A future version of Go may introduce this behavior by default. 66 | // Programs that want to accept non-local names can ignore 67 | // the [ErrInsecurePath] error and use the returned header. 68 | func (r *Reader) Next() (*tar.Header, error) { 69 | return r.tr.Next() 70 | } 71 | 72 | // Read reads from the current file in the tar archive. 73 | // It returns (0, io.EOF) when it reaches the end of that file, 74 | // until [Next] is called to advance to the next file. 75 | // 76 | // If the current file is sparse, then the regions marked as a hole 77 | // are read back as NUL-bytes. 78 | // 79 | // Calling Read on special types like [TypeLink], [TypeSymlink], [TypeChar], 80 | // [TypeBlock], [TypeDir], and [TypeFifo] returns (0, [io.EOF]) regardless of what 81 | // the [Header.Size] claims. 82 | func (r *Reader) Read(p []byte) (int, error) { 83 | return r.tr.Read(p) 84 | } 85 | 86 | // ReadBytes returns the number of read bytes 87 | func (r *Reader) ReadBytes() int { 88 | return r.concatReader.ReadBytes() 89 | } 90 | 91 | // Close closes the reader for further reading and returns an error on failure. 92 | func (r *Reader) Close() error { 93 | if r.close != nil { 94 | return r.close() 95 | } 96 | return nil 97 | } 98 | 99 | // detectCompressionType determines the compression type based on magic bytes. 100 | func detectCompressionType(data []byte) string { 101 | if len(data) < 3 { 102 | return "unknown" 103 | } 104 | switch { 105 | case bytes.HasPrefix(data, []byte{0x1f, 0x8b}): // Gzip 106 | return "gzip" 107 | case bytes.HasPrefix(data, []byte{0xfd, '7', 'z', 'X', 'Z'}): // XZ 108 | return "xz" 109 | case bytes.HasPrefix(data, []byte{'B', 'Z', 'h'}): // Bzip2 110 | return "bzip2" 111 | default: 112 | return "unknown" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/util/tar/reader_test.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "crypto/rand" 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/dsnet/compress/bzip2" 10 | "github.com/ulikunitz/xz" 11 | "io" 12 | "testing" 13 | ) 14 | 15 | func TestReader(t *testing.T) { 16 | // write tar archive 17 | buf := new(bytes.Buffer) 18 | tw := tar.NewWriter(buf) 19 | filename := "some.file" 20 | if err := tw.WriteHeader(&tar.Header{ 21 | Typeflag: tar.TypeReg, 22 | Name: filename, 23 | Size: 10, 24 | }); err != nil { 25 | t.Fatalf("unexpected error: %s", err) 26 | } 27 | data := make([]byte, 10) 28 | _, err := rand.Read(data) 29 | if err != nil { 30 | t.Fatalf("unexpected error: %s", err) 31 | } 32 | if _, err := tw.Write(data); err != nil { 33 | t.Fatalf("unexpected error: %s", err) 34 | } 35 | if err := tw.Close(); err != nil { 36 | t.Fatalf("unexpected error: %s", err) 37 | } 38 | archive := make([]byte, buf.Len()) 39 | copy(archive, buf.Bytes()) 40 | 41 | // read uncompressed archive 42 | tr, err := NewReader(bytes.NewReader(archive)) 43 | if err != nil { 44 | t.Fatalf("unexpected error: %s", err) 45 | } 46 | assertTarContent(t, tr, filename, data) 47 | 48 | // read gzip archive 49 | buf.Reset() 50 | gz := gzip.NewWriter(buf) 51 | if err != nil { 52 | t.Fatalf("unexpected error: %s", err) 53 | } 54 | if _, err = gz.Write(archive); err != nil { 55 | t.Fatalf("unexpected error: %s", err) 56 | } 57 | if err := gz.Close(); err != nil { 58 | t.Fatalf("unexpected error: %s", err) 59 | } 60 | tr, err = NewReader(buf) 61 | if err != nil { 62 | t.Fatalf("unexpected error: %s", err) 63 | } 64 | assertTarContent(t, tr, filename, data) 65 | 66 | // read bzip2 archive 67 | buf.Reset() 68 | bz, err := bzip2.NewWriter(buf, nil) 69 | if err != nil { 70 | t.Fatalf("unexpected error: %s", err) 71 | } 72 | if _, err = bz.Write(archive); err != nil { 73 | t.Fatalf("unexpected error: %s", err) 74 | } 75 | if err := bz.Close(); err != nil { 76 | t.Fatalf("unexpected error: %s", err) 77 | } 78 | tr, err = NewReader(buf) 79 | if err != nil { 80 | t.Fatalf("unexpected error: %s", err) 81 | } 82 | assertTarContent(t, tr, filename, data) 83 | 84 | // read xz archive 85 | buf.Reset() 86 | xzw, err := xz.NewWriter(buf) 87 | if err != nil { 88 | t.Fatalf("unexpected error: %s", err) 89 | } 90 | if _, err = xzw.Write(archive); err != nil { 91 | t.Fatalf("unexpected error: %s", err) 92 | } 93 | if err = xzw.Close(); err != nil { 94 | t.Fatalf("unexpected error: %s", err) 95 | } 96 | tr, err = NewReader(buf) 97 | if err != nil { 98 | t.Fatalf("unexpected error: %s", err) 99 | } 100 | assertTarContent(t, tr, filename, data) 101 | } 102 | 103 | func assertTarContent(t *testing.T, tr *Reader, filename string, fileContent []byte) { 104 | t.Helper() 105 | hdr, err := tr.Next() 106 | if err != nil { 107 | t.Fatalf("unexpected error: %s", err) 108 | } 109 | if hdr.Name != filename { 110 | t.Errorf("filename mismatch: expected %s, got %s", filename, hdr.Name) 111 | } 112 | data := make([]byte, hdr.Size) 113 | _, err = tr.Read(data) 114 | if err != nil && err != io.EOF { 115 | t.Fatalf("unexpected error: %s", err) 116 | } 117 | if !bytes.Equal(data, fileContent) { 118 | t.Errorf("fileContent mismatch: expected %s, got %s", spew.Sdump(fileContent), spew.Sdump(data)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/util/tar/tar.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "k8s.io/klog" 11 | ) 12 | 13 | // PackFolder will write the given folder as a tar to the given Writer. 14 | func PackFolder(src string, buf io.Writer) error { 15 | tw := tar.NewWriter(buf) 16 | 17 | // walk through every file in the folder 18 | filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { 19 | // generate tar header 20 | header, err := tar.FileInfoHeader(fi, file) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | rel, err := filepath.Rel(src, file) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | header.Name = filepath.ToSlash(rel) 31 | if err := tw.WriteHeader(header); err != nil { 32 | return err 33 | } 34 | 35 | klog.V(4).Infof("add to tar file: %s", header.Name) 36 | 37 | // if not a dir, write file content 38 | if !fi.IsDir() { 39 | data, err := os.Open(file) 40 | if err != nil { 41 | return err 42 | } 43 | if _, err := io.Copy(tw, data); err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | }) 49 | 50 | // produce tar 51 | if err := tw.Close(); err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // UnpackFile will extract the given file from the given archive to the 59 | // given dest writer. 60 | func UnpackFile(dst, fname string, archive io.Reader, dest io.Writer) error { 61 | tr, err := NewReader(archive) 62 | if err != nil { 63 | return err 64 | } 65 | for { 66 | header, err := tr.Next() 67 | if err != nil { 68 | return err 69 | } 70 | if header != nil && filepath.Join(dst, header.Name) == fname { 71 | _, err = io.Copy(dest, tr) 72 | return err 73 | } 74 | } 75 | } 76 | 77 | // GetTargetFolderNames will return all affected folders in the archive 78 | // provided. 79 | func GetTargetFolderNames(dst string, archive io.Reader) ([]string, error) { 80 | return getTargets(dst, archive, tar.TypeDir) 81 | } 82 | 83 | // GetTargetFileNames will return all file names in the archive 84 | // provided. 85 | func GetTargetFileNames(dst string, archive io.Reader) ([]string, error) { 86 | return getTargets(dst, archive, tar.TypeReg) 87 | } 88 | 89 | // GetFileMode will return the file mode permissions of the given file in 90 | // the archive. 91 | func GetFileMode(dst string, fname string, archive io.Reader) (os.FileMode, error) { 92 | tr, err := NewReader(archive) 93 | if err != nil { 94 | return 0, err 95 | } 96 | for { 97 | header, err := tr.Next() 98 | if err != nil { 99 | return 0, err 100 | } 101 | if header != nil && filepath.Join(dst, header.Name) == fname { 102 | return header.FileInfo().Mode(), nil 103 | } 104 | } 105 | } 106 | 107 | // getTargets will return all given asset names of type (dir/file). 108 | func getTargets(dst string, archive io.Reader, typ byte) ([]string, error) { 109 | res := []string{} 110 | tr, err := NewReader(archive) 111 | if err != nil { 112 | return nil, err 113 | } 114 | for { 115 | header, err := tr.Next() 116 | switch { 117 | case err == io.EOF: 118 | return res, nil 119 | case err != nil: 120 | return res, err 121 | case header == nil: 122 | continue 123 | } 124 | target := filepath.Join(dst, header.Name) 125 | if header.Typeflag == typ { 126 | res = append(res, target) 127 | } 128 | } 129 | } 130 | 131 | // IsSingleFileArchive will return true if there is only 1 file stored in the 132 | // given archive. 133 | func IsSingleFileArchive(archive []byte) bool { 134 | tr, err := NewReader(bytes.NewReader(archive)) 135 | if err != nil { 136 | klog.Errorf("error reading tar archive: %v", err) 137 | return false 138 | } 139 | count := 0 140 | for count < 2 { 141 | header, err := tr.Next() 142 | if err != nil { 143 | return count == 1 144 | } 145 | if header.Typeflag == tar.TypeReg { 146 | count++ 147 | } 148 | } 149 | return count == 1 150 | } 151 | 152 | // GetTarSize will return the actual size of the tar file for a byte array 153 | // containing padded tar data. 154 | func GetTarSize(dat []byte) (int, error) { 155 | var err error 156 | 157 | tr, err := NewReader(bytes.NewReader(dat)) 158 | if err != nil { 159 | return 0, err 160 | } 161 | 162 | for { 163 | if _, err = tr.Next(); err != nil { 164 | if err == io.EOF { 165 | err = nil 166 | } 167 | break 168 | } 169 | io.Copy(io.Discard, tr) 170 | } 171 | 172 | return tr.ReadBytes(), err 173 | } 174 | -------------------------------------------------------------------------------- /internal/util/tar/tar_test.go: -------------------------------------------------------------------------------- 1 | package tar 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "slices" 7 | "testing" 8 | ) 9 | 10 | func TestPackFolder(t *testing.T) { 11 | var b bytes.Buffer 12 | w := bufio.NewWriter(&b) 13 | 14 | err := PackFolder("./", w) 15 | if err != nil { 16 | t.Errorf("unexpected error: %s", err) 17 | } 18 | w.Flush() 19 | 20 | dat := b.Bytes() 21 | if IsSingleFileArchive(dat) { 22 | t.Error("archive contains more than 1 file, but IsSingleFileArchive says not") 23 | } 24 | 25 | files, err := GetTargetFileNames("", bytes.NewReader(dat)) 26 | if err != nil { 27 | t.Errorf("unexpected error: %s", err) 28 | } 29 | if !slices.Contains(files, "tar_test.go") { 30 | t.Error("expected archive to contain tar_test.go") 31 | } 32 | 33 | folders, err := GetTargetFolderNames("", bytes.NewReader(dat)) 34 | if err != nil { 35 | t.Errorf("unexpected error: %s", err) 36 | } 37 | if !slices.Contains(folders, ".") { 38 | t.Error("expected archive to contain .") 39 | } 40 | 41 | mode, err := GetFileMode("", "tar_test.go", bytes.NewReader(dat)) 42 | if err != nil { 43 | t.Errorf("unexpected error: %s", err) 44 | } 45 | if mode == 0 { 46 | t.Error("expected archive to contain tar_test.go with valid file mode") 47 | } 48 | 49 | rsz := len(dat) 50 | csz, err := GetTarSize(append(dat, []byte{0, 0, 0, 0}...)) 51 | if err != nil { 52 | t.Errorf("unexpected error: %s", err) 53 | } 54 | if csz != rsz { 55 | t.Errorf("GetTarSize returns %d instead of %d bytes", csz, rsz) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/joyrex2001/kubedock/cmd" 7 | ) 8 | 9 | //go:embed README.md 10 | var readme string 11 | 12 | //go:embed config.md 13 | var config string 14 | 15 | //go:embed LICENSE 16 | var license string 17 | 18 | func main() { 19 | cmd.README = readme 20 | cmd.LICENSE = license 21 | cmd.CONFIG = config 22 | cmd.Execute() 23 | } 24 | --------------------------------------------------------------------------------