├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION.txt ├── _config.yml ├── cmd ├── create.go ├── root.go └── version.go ├── go.mod ├── go.sum ├── img └── kapp-create.gif ├── internal ├── artifacts │ ├── delims.go │ ├── delims_test.go │ ├── dockermake │ │ ├── projectfiles.go │ │ └── templates │ │ │ └── templates.go │ ├── golang │ │ ├── projectfiles.go │ │ └── templates │ │ │ └── templates.go │ ├── helm │ │ ├── projectfiles.go │ │ └── templates │ │ │ └── templates.go │ ├── layoutcreator.go │ ├── projectfile.go │ ├── projectfile_test.go │ ├── projectinfo.go │ ├── projectinfo_test.go │ ├── projectlayout.go │ └── projectlayout_test.go └── utils │ ├── fileutils.go │ └── fileutils_test.go ├── main.go └── version └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | sudo: required 4 | services: 5 | - docker 6 | notifications: 7 | email: true 8 | go: 9 | - 1.x 10 | matrix: 11 | fast_finish: true 12 | install: 13 | - go get golang.org/x/lint 14 | script: 15 | - make 16 | - go vet $(go list ./... | grep -v vendor) 17 | - test -z "$(golint ./... | grep -v vendor | tee /dev/stderr)" 18 | - test -z "$(gofmt -s -l . | grep -v vendor | tee /dev/stderr)" 19 | - go test $(go list ./... | grep -v vendor) 20 | - make cover 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `kapp` 2 | 3 | Thanks for thinking about contributing to `kapp`! All types of contributions are 4 | welcome: 5 | 6 | * [Issues](#issues) 7 | * [Documentation](#documentation) 8 | * [Tests](#tests) 9 | * [Features](#features) 10 | 11 | Start with setting up your environment by following the instructions in 12 | [development setup](#development-setup). 13 | 14 | ## Issues 15 | 16 | Please use [GitHub issues](https://github.com/peterj/kapp/issues) to report bugs 17 | or submit new feature request. Before creating a new issue, do a quick search to 18 | see if same/similar issue is already logged and consider adding +1 to the 19 | existing issue. If no issue exists, feel free to open a new one and include as 20 | much information as possible (things like tools versions, steps to reproduce it, 21 | etc.) to be able to reproduce the behavior. 22 | 23 | ## Documentation 24 | 25 | People make mistekes, forget things, don't explain concepts well and so forth. 26 | Documentation contributions are extremely important as docs are the first thing 27 | users see. Feel free to expand on the existing documentation, change or update 28 | it to make it clearer or add more examples. 29 | 30 | ## Tests 31 | 32 | Check out the [current code coverage](https://codecov.io/gh/peterj/kapp) to see 33 | where test coverage is missing. Follow the 34 | [development setup](#development-setup) to get your machine ready for 35 | development. 36 | 37 | ## Features 38 | 39 | If you have an idea for a new feature, feel free to log an issue and describe 40 | what the feature would be and get feedback from others. Next, get your machine 41 | ready for development by following the instructions in [development 42 | setup)[#development-setup] and write some code (don't forget to add tests for 43 | any new code your write). 44 | 45 | ## Development setup 46 | 47 | 1. Fork and clone the repo. 48 | > Note: your `master` branch should be pointing at the original repo and you 49 | > will be making pull requests from branches on your fork. Do this to set it 50 | > up: 51 | > 52 | > ``` 53 | > git remote add upstream https://github.com/peterj/kapp.git 54 | > git fetch upstream 55 | > git branch --set-upstream-to=upstream/master master 56 | > ``` 57 | 58 | 2) Run `dep ensure` to get all dependencies. 59 | 3) Run `make all` to build the app, run tests and other tools. 60 | 4) Run the app `./kapp` to ensure it works. 61 | 62 | ## Make changes, commit and open a pull request 63 | 64 | After you've made your changes, make sure you run `make all` to ensure there are 65 | no issues (no failing tests, no vet/fmt issues). Finally, create a pull request 66 | from your fork against the original repo. 67 | 68 | ### Create new release 69 | 70 | To create a new release, follow the steps below: 71 | 72 | 1. `make bump-version` 73 | 2. `make all` 74 | 3. `make release` 75 | 4. `make tag` 76 | 5. Push the tag (e.g. `git push origin [TAG]`) 77 | 6. Create a new release on [GitHub](https://github.com/peterj/kapp/releases) 78 | and upload the binaries 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Jausovec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_DIR?=$(shell pwd) 2 | BUILDDIR:=$(CURRENT_DIR)/release 3 | 4 | NAME:=kapp 5 | PKG:=github.com/peterj/$(NAME) 6 | GOOSARCHES=darwin/amd64 7 | VERSION_FILE:=VERSION.txt 8 | 9 | VERSION=$(shell cat ./$(VERSION_FILE)) 10 | GITCOMMIT:=$(shell git rev-parse --short HEAD) 11 | GITUNTRACKEDCHANGES:=$(shell git status --porcelain --untracked-files=no) 12 | ifneq ($(GITUNTRACKEDCHANGES),) 13 | GITCOMMIT := $(GITCOMMIT)-dirty 14 | endif 15 | 16 | # Sets the actual GITCOMMIT and VERSION values 17 | VERSION_INFO=-X $(PKG)/version.GITCOMMIT=$(GITCOMMIT) -X $(PKG)/version.VERSION=$(VERSION) 18 | 19 | # Set the linker flags 20 | GO_LDFLAGS=-ldflags "-w $(VERSION_INFO)" 21 | 22 | all: build fmt lint test vet 23 | 24 | # Builds the binary 25 | .PHONY: build 26 | build: 27 | @echo "-> $@" 28 | CGO_ENABLED=0 go build -i -installsuffix cgo ${GO_LDFLAGS} -o $(NAME) . 29 | 30 | # Installs the binary 31 | .PHONY: install 32 | install: 33 | @echo "+ $@" 34 | go install -a -tags "$(BUILDTAGS)" ${GO_LDFLAGS} . 35 | 36 | # Gofmts all code (sans vendor folder) just in case not using automatic formatting 37 | .PHONY: fmt 38 | fmt: 39 | @echo "-> $@" 40 | @gofmt -s -l . | grep -v vendor | tee /dev/stderr 41 | 42 | # Runs golint 43 | .PHONY: lint 44 | lint: 45 | @echo "-> $@" 46 | @golint ./... | grep -v vendor | tee /dev/stderr 47 | 48 | # Runs all tests 49 | .PHONY: test 50 | test: 51 | @echo "-> $@" 52 | @go test -v $(shell go list ./... | grep -v vendor) 53 | 54 | # Runs tests with coverage 55 | .PHONY: cover 56 | cover: 57 | @echo "" > coverage.txt 58 | @for d in $(shell go list ./... | grep -v vendor); do \ 59 | go test -race -coverprofile=profile.out -covermode=atomic "$$d"; \ 60 | if [ -f profile.out ]; then \ 61 | cat profile.out >> coverage.txt; \ 62 | rm profile.out; \ 63 | fi; \ 64 | done; 65 | 66 | # Runs govet 67 | .PHONY: vet 68 | vet: 69 | @echo "-> $@" 70 | @go vet $(shell go list ./... | grep -v vendor) | tee /dev/stderr 71 | 72 | # Bumps the version of the service 73 | .PHONY: bump-version 74 | bump-version: 75 | $(eval NEW_VERSION = $(shell echo $(VERSION) | awk -F. '{$NF+=1; OFS=FS} {$1 = $1; printf "%s",$0}')) 76 | @echo "Bumping VERSION.txt from $(VERSION) to $(NEW_VERSION)" 77 | echo $(NEW_VERSION) > VERSION.txt 78 | git add VERSION.txt README.md 79 | git commit -vsam "Bump version to $(NEW_VERSION)" 80 | 81 | # Create a new git tag to prepare to build a release 82 | .PHONY: tag 83 | tag: 84 | git tag -sa $(VERSION) -m "$(VERSION)" 85 | @echo "Run git push origin $(VERSION) to push your new tag to GitHub and trigger build." 86 | 87 | define buildrelease 88 | GOOS=$(1) GOARCH=$(2) CGO_ENABLED=1 go build \ 89 | -o $(BUILDDIR)/$(NAME)-$(1)-$(2) \ 90 | -a -tags "$(BUILDTAGS) static_build netgo" \ 91 | -installsuffix netgo ${GO_LDFLAGS_STATIC} .; 92 | md5sum $(BUILDDIR)/$(NAME)-$(1)-$(2) > $(BUILDDIR)/$(NAME)-$(1)-$(2).md5; 93 | shasum -a 256 $(BUILDDIR)/$(NAME)-$(1)-$(2) > $(BUILDDIR)/$(NAME)-$(1)-$(2).sha256; 94 | endef 95 | 96 | # Builds the cross-compiled binaries, naming them in such a way for release (eg. binary-GOOS-GOARCH) 97 | .PHONY: release 98 | release: *.go VERSION.txt 99 | @echo "+ $@" 100 | $(foreach GOOSARCH,$(GOOSARCHES), $(call buildrelease,$(subst /,,$(dir $(GOOSARCH))),$(notdir $(GOOSARCH)))) 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Create *K*ubernetes _app_ (`kapp`)](https://peterj.github.io/kapp/) 2 | 3 | [![Build Status](https://travis-ci.org/peterj/kapp.svg?branch=master)](https://travis-ci.org/peterj/kapp) 4 | [![GitHub release](https://img.shields.io/github/release/peterj/kapp/all.svg)](https://github.com/peterj/kapp/releases) 5 | [![codecov](https://codecov.io/gh/peterj/kapp/branch/master/graph/badge.svg)](https://codecov.io/gh/peterj/kapp) 6 | 7 | Create Go apps/services that run on Kubernetes with minimal configuration. 8 | Inspiration for this project came from 9 | [create-react-app](https://github.com/facebook/create-react-app) project. 10 | 11 | * [Quick Overview](#quick-overview) 12 | * [Prerequisites](#prerequisites) 13 | * [Installation](#installation) 14 | * [Creating an App](#creating-an-app) - How to create a new Kubernetes app 15 | * [Development Workflow](#development-workflow) 16 | 17 | Tool was developed and tested on macOS. There's no guarantee that it works on 18 | other platforms. If you run into any issues, please 19 | [file them](https://github.com/peterj/kapp/issues/new). 20 | 21 | > **Note**: At the moment, project only supports Go. Please 22 | > [file an issue](https://github.com/peterj/kapp/issues/new) if you'd like to 23 | > see support for other languages and/or [send a PR](CONTRIBUTING). 24 | 25 | # Quick Overview 26 | 27 | ```bash 28 | # Creates an app called helloworld 29 | kapp create helloworld --package github.com/peterj/helloworld 30 | 31 | # Initialize the Git repo and make an inital commit 32 | cd helloworld 33 | git init && git add * && git commit -m 'inital commit' 34 | 35 | # Build the app 36 | make all 37 | ``` 38 | 39 | Run the app with `./helloworld` and access it at http://localhost:8080/. 40 | 41 | ![kapp demo GIT](img/kapp-create.gif) 42 | 43 | # Prerequisites 44 | 45 | * [Go](https://golang.org/dl/) 46 | * [Docker](https://www.docker.com/docker-mac) 47 | * [Helm](https://helm.sh/) 48 | * [Dep](https://github.com/golang/dep) 49 | * [Git](https://git-scm.com/) 50 | * Kubernetes cluster - you can also use Kubernetes support in 51 | [Docker for Mac](https://www.docker.com/docker-mac), 52 | [Minikube](https://github.com/kubernetes/minikube) or an actual cluster from 53 | one of the cloud providers 54 | 55 | # Installation 56 | 57 | You can download the latest binary from the 58 | [Releases page](https://github.com/peterj/kapp/releases). Alternatively, you can 59 | use `go get` and install `kapp` like that: 60 | 61 | ```bash 62 | go get github.com/peterj/kapp 63 | make install 64 | ``` 65 | 66 | Alternatively, you can install kapp using `homebrew` on a mac. 67 | ``` bash 68 | brew install peterj/kapp/kapp 69 | ``` 70 | 71 | # Creating an app 72 | 73 | To create a new Kubernetes app, run the following command: 74 | 75 | ``` 76 | kapp create helloworld --package github.com/[username]/helloworld 77 | ``` 78 | 79 | _Note: the package name is required in order to properly configure the generated 80 | files._ 81 | 82 | The command above will create a folder called `helloworld` in the current 83 | working folder. The structure of the created Go project looks like this: 84 | 85 | ``` 86 | helloworld 87 | ├── Dockerfile 88 | ├── Makefile 89 | ├── VERSION.txt 90 | ├── docker.mk 91 | ├── go.mod 92 | ├── go.sum 93 | ├── helm 94 | │   └── helloworld 95 | │   ├── Chart.yaml 96 | │   ├── templates 97 | │   │   ├── _helpers.tpl 98 | │   │   ├── deployment.yaml 99 | │   │   ├── service.yaml 100 | │   │   ├── serviceaccount.yaml 101 | │   │   └── tests 102 | │   │   └── test-connection.yaml 103 | │   └── values.yaml 104 | ├── main.go 105 | └── version 106 | └── version.go 107 | ``` 108 | 109 | # Development workflow 110 | 111 | The inital workflow for getting your app running in Kubernetes involves these 112 | steps: 113 | 114 | 1. [Build the app image](#build-the-image) 115 | 2. [Push the app image to the registry](#push-the-image) 116 | 3. [Create intial app release (first deployment)](#first-deployment) 117 | 4. [Interact with the app](#interact-with-the-app) 118 | 5. [Deploy app updates](#deploy-app-upgrades) 119 | 120 | After you have created the inital release (step #3) you can continue with this 121 | [workflow](#deploy-app-upgrades) 122 | 123 | ## Build the image 124 | 125 | Makefile task `build.image` can be used to build the Docker image that contains 126 | your app and tag that image. Note that before you run `make build.image`, you 127 | have to do these two things: 128 | 129 | 1. Login to the image registry you want to use 130 | 2. Set the `DOCKER_REGISTRY` environment variable to that registry 131 | 132 | Below is an example on how to set the image registry and run the `build.image` 133 | task: 134 | 135 | ```bash 136 | $ cd helloworld 137 | 138 | # Login to the hub.docker.com (or any other image registry) 139 | $ docker login 140 | 141 | # Replace 'kubeapp' with your hub.docker.com username 142 | $ export DOCKER_REGISTRY=kubeapp 143 | 144 | # Build the image in format: kubeapp/helloworld:0.1.0 145 | $ make build.image 146 | -> build.image 147 | docker build -f Dockerfile -t kubeapp/helloworld:0.1.0 . 148 | ... (Docker build output) ... 149 | Successfully tagged kubeapp/helloworld:0.1.0 150 | ``` 151 | 152 | ## Push the image 153 | 154 | With image built, you can use `make push.image` task to push the built image to 155 | the registry: 156 | 157 | ```bash 158 | $ make push.image 159 | -> push.image 160 | docker push kubeapp/helloworld:0.1.0 161 | The push refers to repository [docker.io/kubeapp/helloworld] 162 | ... (docker push output) 163 | 0.1.0: digest: sha256:b13772ff86c9f2691bfd56a6cbdc73d3456886f8b85385a43699706f0471c866 size: 1156 164 | ``` 165 | 166 | ## First deployment 167 | 168 | Task `install.app` is used to create an inital installation/deployment of your 169 | app to Kubernetes. Before running this task, you need to ensure you have Helm 170 | installed and initialized on the cluster and your current cluster context is set 171 | to the cluster you want to deploy the app to. 172 | 173 | ```bash 174 | $ make install.app 175 | -> install.app 176 | kubectl create ns helloworld 177 | namespace/helloworld created 178 | helm install helloworld helm/helloworld --namespace helloworld --set=image.repository=pj3677/helloworld 179 | NAME: helloworld 180 | LAST DEPLOYED: Wed Dec 11 16:52:01 2019 181 | NAMESPACE: helloworld 182 | STATUS: deployed 183 | REVISION: 1 184 | ``` 185 | 186 | The `install.app` task will install your application in `helloworld` namespace. 187 | The initial installation creates a Kubernetes deployment as well as a Kubernetes 188 | service you can use to access the application. 189 | 190 | To double check your app is deployed, run the following Helm command: 191 | 192 | ```bash 193 | $ helm list 194 | NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION 195 | helloblah default 1 2019-12-11 16:41:18.551571 -0800 PST deployed helloblah-0.1.0 0.1.0 196 | ``` 197 | 198 | Alternatively, you can use `kubectl` to check the created resources. With the 199 | command in the example below, we are getting all services and deployments from 200 | the `helloworld` namespace that have a label called `app` set to `helloworld`: 201 | 202 | ```bash 203 | $ kubectl get svc,deploy -l app=helloworld -n helloworld 204 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 205 | svc/helloworld ClusterIP 10.100.205.117 80/TCP 2m 206 | 207 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 208 | deploy/helloworld 1 1 1 1 2m 209 | ``` 210 | 211 | ## Interact with the app 212 | 213 | Now that your app is deployed and running in Kubernetes, you can interact with 214 | it. There are a couple of different ways you could interact with the app, the 215 | simplest being Kubernetes proxy to create a connection to the cluster: 216 | 217 | ```bash 218 | # Create a proxy to the cluster and run it in the background 219 | $ kubectl proxy & 220 | 221 | # Access the deployed service through the proxy 222 | $ curl http://127.0.0.1:8001/api/v1/namespaces/helloworld/services/helloworld:http/proxy/ 223 | Hello 224 | ``` 225 | 226 | Another way to access the service is to run a container inside the cluster and 227 | run `curl` from there. 228 | 229 | ```bash 230 | # This will give you a terminal inside the container running on the cluster 231 | $ kubectl run curl --image=radial/busyboxplus:curl -i --tty 232 | 233 | # Access the service 'helloworld' in namespace 'helloworld' 234 | $ curl helloworld.helloworld.svc.cluster.local 235 | Hello 236 | ``` 237 | 238 | ## Deploy app upgrades 239 | 240 | As part of your dev workflow, you will be making changes to your app and you 241 | would want to deploy those changes and test the app out. Let's say we updated 242 | the code in `main.go` to return `Hello World` instead of just `Hello`. After 243 | you've built your app (e.g. `make all`), the sequence of commands to deploy the 244 | updated app would be something like this: 245 | 246 | ```bash 247 | # (OPTIONAL) Bumps the version in VERSION.txt file 248 | $ make bump-version 249 | 250 | # Builds the new version of the image, pushes it to the registry and upgrades the app 251 | $ make upgrade 252 | ``` 253 | 254 | Now if you try to access the app you should get back `Hello World`. 255 | 256 | # Contributing 257 | 258 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how to get 259 | started contributing to `kapp`. 260 | 261 | # License 262 | 263 | `kapp` is open source software [licensed as MIT](LICENSE). 264 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/peterj/kapp/internal/artifacts/helm" 11 | "github.com/pkg/errors" 12 | 13 | "github.com/peterj/kapp/internal/artifacts" 14 | 15 | "github.com/fatih/color" 16 | "github.com/peterj/kapp/internal/artifacts/dockermake" 17 | "github.com/peterj/kapp/internal/artifacts/golang" 18 | 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // ApplicationName variable 23 | var ApplicationName string 24 | 25 | // PackageName of the project (e.g. github.com/[username]/[projectname]) 26 | var PackageName string 27 | 28 | // Language represents the project language 29 | var Language string 30 | 31 | func init() { 32 | createCmd.Flags().StringVarP(&ApplicationName, "name", "n", "", "application name") 33 | createCmd.Flags().StringVarP(&PackageName, "package", "p", "", "package name") 34 | createCmd.Flags().StringVarP(&Language, "language", "l", "golang", "project language") 35 | 36 | createCmd.MarkFlagRequired("package") 37 | rootCmd.AddCommand(createCmd) 38 | } 39 | 40 | // askYesNoWarning prints a warning yes/no question and waits for user input 41 | func askYesNoWarning(message string) bool { 42 | red := color.New(color.FgRed).SprintfFunc() 43 | fmt.Println(red("WARNING!")) 44 | 45 | reader := bufio.NewReader(os.Stdin) 46 | fmt.Print(message) 47 | answer, _ := reader.ReadString('\n') 48 | return strings.ToLower(string(answer[0])) == "y" 49 | } 50 | 51 | var createCmd = &cobra.Command{ 52 | Use: "create", 53 | Short: "Creates a new Kubernetes app", 54 | Args: func(cmd *cobra.Command, args []string) error { 55 | if len(args) != 1 { 56 | return errors.New("missing folder name") 57 | } 58 | return nil 59 | }, 60 | RunE: func(cmd *cobra.Command, args []string) error { 61 | blue := color.New(color.FgBlue).SprintfFunc() 62 | green := color.New(color.FgGreen).SprintfFunc() 63 | 64 | outputFolder := args[0] 65 | wd, _ := os.Getwd() 66 | appFolder := path.Join(wd, outputFolder) 67 | 68 | if _, err := os.Stat(appFolder); err == nil { 69 | msg := fmt.Sprintf("Output folder (%s) already exists.\nAre you sure you want to continue (y/n)? ", blue(appFolder)) 70 | if !askYesNoWarning(msg) { 71 | return fmt.Errorf("Aborted. Output folder already exists") 72 | } 73 | } 74 | 75 | // If project name is not provided, we just the output folder name 76 | if ApplicationName == "" { 77 | ApplicationName = outputFolder 78 | } 79 | 80 | if !strings.HasSuffix(PackageName, ApplicationName) { 81 | msg := fmt.Sprintf("Package name (%s) should contain the application name (%s).\nAre you sure you want to continue (y/n)? ", blue(PackageName), blue(ApplicationName)) 82 | if !askYesNoWarning(msg) { 83 | return fmt.Errorf("Aborted. Package name did not contain the application name") 84 | } 85 | } 86 | 87 | fmt.Printf(`Creating Kubernetes App: 88 | Application name: %s 89 | Language........: %s 90 | Package name....: %s 91 | `, blue(ApplicationName), blue(Language), blue(PackageName)) 92 | 93 | switch strings.ToLower(Language) { 94 | case "golang": 95 | { 96 | projectInfo := &artifacts.ProjectInfo{ 97 | ApplicationName: ApplicationName, 98 | PackageName: PackageName, 99 | VersionFileName: "VERSION.txt", 100 | } 101 | 102 | allFiles := []*artifacts.ProjectFile{} 103 | for _, e := range golang.ProjectFiles() { 104 | allFiles = append(allFiles, e) 105 | } 106 | 107 | for _, e := range helm.ProjectFiles(ApplicationName) { 108 | allFiles = append(allFiles, e) 109 | } 110 | 111 | for _, e := range dockermake.ProjectFiles() { 112 | allFiles = append(allFiles, e) 113 | } 114 | 115 | layout, err := artifacts.NewProjectLayout(projectInfo, allFiles) 116 | if err != nil { 117 | return errors.Wrap(err, "RunE: new project layout") 118 | } 119 | 120 | if err := layout.Create(outputFolder); err != nil { 121 | return errors.Wrap(err, fmt.Sprintf("RunE: create layout: output folder '%s'", outputFolder)) 122 | } 123 | } 124 | default: 125 | { 126 | return errors.Wrap(fmt.Errorf("RunE: project language %s it not supported", Language), "") 127 | } 128 | } 129 | fmt.Printf("Done.") 130 | fmt.Printf("\n\nGet started:\n %s\n %s\n %s\n", green(fmt.Sprintf("cd %s", appFolder)), green("git init && git add * && git commit -m 'inital commit'"), green("make all")) 131 | fmt.Printf("\nNote: Refer to README.md (%s) for explanation of how to build and push Docker image and deploy to Kubernetes\n\n", green("https://github.com/peterj/kapp/blob/master/README.md")) 132 | 133 | return nil 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // Verbose indicates if app output should be verbose or not 11 | var Verbose bool 12 | 13 | func init() { 14 | rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "show verbose output") 15 | flag.Parse() 16 | } 17 | 18 | var rootCmd = &cobra.Command{ 19 | Use: "kapp", 20 | Short: "Create Kubernetes App is a tool for creating simple services that run on Kubernetes", 21 | Long: `Create Kubernetes App helps you jump start your Kubernetes services by creating all 22 | necessary files that are need for getting your service up and running in Kubernetes.`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | cmd.HelpFunc()(cmd, args) 25 | }, 26 | } 27 | 28 | // Execute runs the command 29 | func Execute() { 30 | if err := rootCmd.Execute(); err != nil { 31 | os.Exit(1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/peterj/kapp/version" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | rootCmd.AddCommand(versionCmd) 13 | } 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Prints the version number", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | fmt.Printf(`version : %s 20 | git hash : %s 21 | go version : %s 22 | go compiler : %s 23 | platform : %s/%s`, version.VERSION, version.GITCOMMIT, 24 | runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterj/kapp 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fatih/color v1.7.0 7 | github.com/mattn/go-colorable v0.1.4 // indirect 8 | github.com/mattn/go-isatty v0.0.11 // indirect 9 | github.com/pkg/errors v0.8.1 10 | github.com/spf13/cobra v0.0.5 11 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 9 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 12 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 13 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 14 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 15 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 16 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 17 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 18 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 19 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 20 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 21 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 22 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 23 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 24 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 27 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 28 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 29 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 30 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 31 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 32 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 33 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 34 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 35 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 36 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 37 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 38 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 39 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 40 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= 41 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 48 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 50 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms= 51 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 52 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | -------------------------------------------------------------------------------- /img/kapp-create.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterj/kapp/bf7950c4fd59d0770c2b6dffe67c847e9d859487/img/kapp-create.gif -------------------------------------------------------------------------------- /internal/artifacts/delims.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | // Delims holds the template delimiters for the project file (e.g. {{ }} or [[ ]]) 4 | type Delims struct { 5 | Left string 6 | Right string 7 | } 8 | 9 | // NewDelims creates new delimiters instance 10 | func NewDelims(left, right string) *Delims { 11 | return &Delims{ 12 | Left: left, 13 | Right: right, 14 | } 15 | } 16 | 17 | // NewDefaultDelims creates new default delimiters ("{{ }}") 18 | func NewDefaultDelims() *Delims { 19 | return &Delims{ 20 | Left: "{{", 21 | Right: "}}", 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/artifacts/delims_test.go: -------------------------------------------------------------------------------- 1 | package artifacts_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/peterj/kapp/internal/artifacts" 7 | ) 8 | 9 | func TestDelims(t *testing.T) { 10 | tt := []struct { 11 | testName string 12 | delimLeft string 13 | delimRight string 14 | }{ 15 | { 16 | testName: "custom delims", 17 | delimLeft: "[[", 18 | delimRight: "]]", 19 | }, 20 | } 21 | 22 | t.Log("Give the need to test default delims creation") 23 | { 24 | defaultDelims := NewDefaultDelims() 25 | if defaultDelims.Left != "{{" { 26 | t.Fatalf("\t%s\tShould have the correct left delimiters : exp[%s] got[%s]\n", failed, "{{", defaultDelims.Left) 27 | } 28 | t.Logf("\t%s\tShould have the correct left delimiters\n", succeeded) 29 | 30 | if defaultDelims.Right != "}}" { 31 | t.Fatalf("\t%s\tShould have the correct right delimiters : exp[%s] got[%s]\n", failed, "}}", defaultDelims.Right) 32 | } 33 | t.Logf("\t%s\tShould have the correct right delimiters\n", succeeded) 34 | } 35 | 36 | t.Log("Give the need to test custom delims creation") 37 | { 38 | for i, tst := range tt { 39 | t.Logf("\tTest %d: \t%s", i, tst.testName) 40 | { 41 | delims := NewDelims(tst.delimLeft, tst.delimRight) 42 | if delims.Left != tst.delimLeft { 43 | t.Fatalf("\t%s\tShould have the correct left delimiters : exp[%s] got[%s]\n", failed, tst.delimLeft, delims.Left) 44 | } 45 | t.Logf("\t%s\tShould have the correct left delimiters\n", succeeded) 46 | 47 | if delims.Right != tst.delimRight { 48 | t.Fatalf("\t%s\tShould have the correct right delimiters : exp[%s] got[%s]\n", failed, tst.delimRight, delims.Right) 49 | } 50 | t.Logf("\t%s\tShould have the correct right delimiters\n", succeeded) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/artifacts/dockermake/projectfiles.go: -------------------------------------------------------------------------------- 1 | package dockermake 2 | 3 | import ( 4 | "github.com/peterj/kapp/internal/artifacts" 5 | dockertemplates "github.com/peterj/kapp/internal/artifacts/dockermake/templates" 6 | ) 7 | 8 | // ProjectFiles returns docker.mk project file 9 | func ProjectFiles() []*artifacts.ProjectFile { 10 | return []*artifacts.ProjectFile{ 11 | artifacts.NewProjectFile(dockertemplates.DockerMkFile, "docker.mk", artifacts.NewDefaultDelims()), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/artifacts/dockermake/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // DockerMkFile holds the contents of the docker.mk file 4 | const DockerMkFile = `VERSION=$(shell cat ./{{ .VersionFileName }}) 5 | 6 | # Docker settings (make sure DOCKER_REGISTRY environment variable is set) 7 | DOCKERFILE:=Dockerfile 8 | IMAGE_NAME={{ .ApplicationName }} 9 | REGISTRY_NAME:=$(DOCKER_REGISTRY) 10 | FULL_IMAGE_NAME=$(REGISTRY_NAME)/$(IMAGE_NAME):$(VERSION) 11 | 12 | # Kubernetes/Helm settings 13 | KUBE_NAMESPACE:={{ .ApplicationName }} 14 | RELEASE_NAME:={{ .ApplicationName }} 15 | HELM_CHART_NAME:=helm/{{ .ApplicationName }} 16 | 17 | # Builds a docker image 18 | define build_image 19 | docker build -f $(1) -t $(2) . 20 | endef 21 | 22 | # Pushes a docker image to registry 23 | define push_image 24 | docker push $(1) 25 | endef 26 | 27 | # Installs a new Helm chart 28 | define helm_install 29 | kubectl create ns $(3) 30 | helm install $(1) $(2) --namespace $(3) --set=image.repository=$(4) 31 | endef 32 | 33 | # Upgrades an existing Helm chart 34 | define helm_upgrade 35 | helm upgrade $(1) $(2) --namespace $(3) --set=image.repository=$(4) 36 | endef 37 | 38 | .PHONY: build.image 39 | build.image: 40 | @echo "-> $@" 41 | $(call build_image, $(DOCKERFILE), $(FULL_IMAGE_NAME)) 42 | 43 | .PHONY: push.image 44 | push.image: 45 | @echo "-> $@" 46 | $(call push_image, $(FULL_IMAGE_NAME)) 47 | 48 | .PHONE: install.app 49 | install.app: 50 | @echo "-> $@" 51 | $(call helm_install,$(RELEASE_NAME),$(HELM_CHART_NAME),$(KUBE_NAMESPACE),$(REGISTRY_NAME)/$(IMAGE_NAME)) 52 | 53 | .PHONY: upgrade.app 54 | upgrade.app: 55 | @echo "-> $@" 56 | $(call helm_upgrade,$(RELEASE_NAME),$(HELM_CHART_NAME),$(KUBE_NAMESPACE),$(REGISTRY_NAME)/$(IMAGE_NAME)) 57 | 58 | .PHONY: upgrade 59 | upgrade: build.image push.image upgrade.app 60 | ` 61 | -------------------------------------------------------------------------------- /internal/artifacts/golang/projectfiles.go: -------------------------------------------------------------------------------- 1 | package golang 2 | 3 | import ( 4 | "github.com/peterj/kapp/internal/artifacts" 5 | gotemplates "github.com/peterj/kapp/internal/artifacts/golang/templates" 6 | ) 7 | 8 | // ProjectFiles returns a slice of files needed for a Go project 9 | func ProjectFiles() []*artifacts.ProjectFile { 10 | goTemplateDelimiters := artifacts.NewDefaultDelims() 11 | 12 | return []*artifacts.ProjectFile{ 13 | artifacts.NewProjectFile(gotemplates.Makefile, "Makefile", goTemplateDelimiters), 14 | artifacts.NewProjectFile(gotemplates.VersionTxtFile, "VERSION.txt", goTemplateDelimiters), 15 | artifacts.NewProjectFile(gotemplates.Dockerfile, "Dockerfile", goTemplateDelimiters), 16 | artifacts.NewProjectFile(gotemplates.VersionGo, "version/version.go", goTemplateDelimiters), 17 | artifacts.NewProjectFile(gotemplates.MainGo, "main.go", goTemplateDelimiters), 18 | artifacts.NewProjectFile(gotemplates.GitIgnore, ".gitignore", goTemplateDelimiters), 19 | artifacts.NewProjectFile(gotemplates.GoModFile, "go.mod", goTemplateDelimiters), 20 | artifacts.NewProjectFile(gotemplates.GoSumFile, "go.sum", goTemplateDelimiters), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/artifacts/golang/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // VersionTxtFile holds the contents of the VERSION.txt 4 | const VersionTxtFile = `0.1.0` 5 | 6 | // Makefile holds the contents of the Makefile 7 | const Makefile = `include docker.mk 8 | NAME:={{ .ApplicationName }} 9 | PKG:={{ .PackageName }} 10 | GOOSARCHES=darwin/amd64 11 | VERSION_FILE:={{ .VersionFileName }} 12 | 13 | VERSION=$(shell cat ./$(VERSION_FILE)) 14 | GITCOMMIT:=$(shell git rev-parse --short HEAD) 15 | GITUNTRACKEDCHANGES:=$(shell git status --porcelain --untracked-files=no) 16 | ifneq ($(GITUNTRACKEDCHANGES),) 17 | GITCOMMIT := $(GITCOMMIT)-dirty 18 | endif 19 | 20 | # Sets the actual GITCOMMIT and VERSION values 21 | VERSION_INFO=-X $(PKG)/version.GITCOMMIT=$(GITCOMMIT) -X $(PKG)/version.VERSION=$(VERSION) 22 | 23 | # Set the linker flags 24 | GO_LDFLAGS=-ldflags "-w $(VERSION_INFO)" 25 | 26 | all: build fmt lint test vet 27 | 28 | # Builds the binary 29 | .PHONY: build 30 | build: 31 | @echo "-> $@" 32 | CGO_ENABLED=0 go build -i -installsuffix cgo ${GO_LDFLAGS} -o $(NAME) . 33 | 34 | # Gofmts all code (sans vendor folder) just in case not using automatic formatting 35 | .PHONY: fmt 36 | fmt: 37 | @echo "-> $@" 38 | @gofmt -s -l . | grep -v vendor | tee /dev/stderr 39 | 40 | # Runs golint 41 | .PHONY: lint 42 | lint: 43 | @echo "-> $@" 44 | @golint ./... | grep -v vendor | tee /dev/stderr 45 | 46 | # Runs all tests 47 | .PHONY: test 48 | test: 49 | @echo "-> $@" 50 | @go test -v $(shell go list ./... | grep -v vendor) 51 | 52 | # Runs govet 53 | .PHONY: vet 54 | vet: 55 | @echo "-> $@" 56 | @go vet $(shell go list ./... | grep -v vendor) | tee /dev/stderr 57 | 58 | # Bumps the version of the service 59 | .PHONY: bump-version 60 | bump-version: 61 | $(eval NEW_VERSION = $(shell echo $(VERSION) | awk -F. '{$NF+=1; OFS=FS} {$1 = $1; printf "%s",$0}')) 62 | @echo "Bumping VERSION.txt from $(VERSION) to $(NEW_VERSION)" 63 | echo $(NEW_VERSION) > VERSION.txt 64 | git commit -vsam "Bump version to $(NEW_VERSION)"` 65 | 66 | // Dockerfile holds the contents of the Dockerfile 67 | const Dockerfile = `FROM golang:1.13.5-alpine as builder 68 | 69 | RUN apk add --no-cache ca-certificates git make 70 | WORKDIR /src 71 | 72 | COPY go.mod . 73 | COPY go.sum . 74 | RUN go mod download 75 | 76 | COPY . . 77 | RUN make build 78 | 79 | FROM alpine:latest 80 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs 81 | COPY --from=builder /src/{{ .ApplicationName }} {{ .ApplicationName }} 82 | ENTRYPOINT [ "./{{ .ApplicationName }}" ]` 83 | 84 | // VersionGo holds the contents of the version.go file 85 | const VersionGo = `package version 86 | 87 | // VERSION indicates the binary version 88 | var VERSION string 89 | 90 | // GITCOMMIT indicates the git hash binary was built off of 91 | var GITCOMMIT string` 92 | 93 | // GoModFile holds the contents of the go.mod file. 94 | const GoModFile = `module {{ .PackageName }} 95 | 96 | go 1.13 97 | ` 98 | 99 | // GoSumFile holds the contents of the go.sum file. 100 | const GoSumFile = `` 101 | 102 | // MainGo holds the contents of the main.go file 103 | const MainGo = `package main 104 | 105 | import ( 106 | "fmt" 107 | "log" 108 | "net/http" 109 | "runtime" 110 | 111 | "{{ .PackageName }}/version" 112 | ) 113 | 114 | func indexHandler(w http.ResponseWriter, r *http.Request) { 115 | w.Header().Set("Content-Type", "application/json") 116 | w.Write([]byte("Hello\n")) 117 | } 118 | 119 | func healthHandler(w http.ResponseWriter, r *http.Request) { 120 | w.WriteHeader(200) 121 | } 122 | 123 | func main() { 124 | port := ":8080" 125 | fmt.Printf("running on : %s\n", port) 126 | fmt.Printf("version : %s\n", version.VERSION) 127 | fmt.Printf("git hash : %s\n", version.GITCOMMIT) 128 | fmt.Printf("go version : %s\n", runtime.Version()) 129 | fmt.Printf("go compiler: %s\n", runtime.Compiler) 130 | fmt.Printf("platform : %s/%s\n", runtime.GOOS, runtime.GOARCH) 131 | 132 | http.HandleFunc("/", indexHandler) 133 | http.HandleFunc("/health", healthHandler) 134 | log.Fatal(http.ListenAndServe(port, nil)) 135 | }` 136 | 137 | // GitIgnore holds the contents of the .gitignore file 138 | const GitIgnore = `# Binaries for programs and plugins 139 | *.exe 140 | *.exe~ 141 | *.dll 142 | *.so 143 | *.dylib 144 | 145 | # Test binary, build with "go test -c" 146 | *.test 147 | 148 | # Output of the go coverage tool, specifically when used with LiteIDE 149 | *.out` 150 | -------------------------------------------------------------------------------- /internal/artifacts/helm/projectfiles.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/peterj/kapp/internal/artifacts" 7 | helmtemplates "github.com/peterj/kapp/internal/artifacts/helm/templates" 8 | ) 9 | 10 | // ProjectFiles returns a slice of files need for a Helm chart 11 | func ProjectFiles(applicationName string) []*artifacts.ProjectFile { 12 | // Since Helm uses "{{" and "}}" in its templates, we have to use [[, ]] 13 | helmDelimiters := artifacts.NewDelims("[[", "]]") 14 | rootFolder := path.Join("helm", applicationName) 15 | return []*artifacts.ProjectFile{ 16 | artifacts.NewProjectFile(helmtemplates.HelpersTpl, path.Join(rootFolder, "templates", "_helpers.tpl"), helmDelimiters), 17 | artifacts.NewProjectFile(helmtemplates.DeploymentYaml, path.Join(rootFolder, "templates", "deployment.yaml"), helmDelimiters), 18 | artifacts.NewProjectFile(helmtemplates.ServiceYaml, path.Join(rootFolder, "templates", "service.yaml"), helmDelimiters), 19 | artifacts.NewProjectFile(helmtemplates.ServiceAccountYaml, path.Join(rootFolder, "templates", "serviceaccount.yaml"), helmDelimiters), 20 | artifacts.NewProjectFile(helmtemplates.TestConnectionYaml, path.Join(rootFolder, "templates/tests", "test-connection.yaml"), helmDelimiters), 21 | artifacts.NewProjectFile(helmtemplates.ChartYaml, path.Join(rootFolder, "Chart.yaml"), helmDelimiters), 22 | artifacts.NewProjectFile(helmtemplates.ValuesYaml, path.Join(rootFolder, "values.yaml"), helmDelimiters), 23 | artifacts.NewProjectFile(helmtemplates.HelmIgnore, path.Join(rootFolder, ".helmignore"), helmDelimiters), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/artifacts/helm/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // HelpersTpl holds contents of the _helpers.tpl file 4 | const HelpersTpl = ` 5 | {{/* vim: set filetype=mustache: */}} 6 | {{/* 7 | Expand the name of the chart. 8 | */}} 9 | {{- define "[[ .ApplicationName ]].name" -}} 10 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 11 | {{- end -}} 12 | 13 | {{/* 14 | Create a default fully qualified app name. 15 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 16 | If release name contains chart name it will be used as a full name. 17 | */}} 18 | {{- define "[[ .ApplicationName ]].fullname" -}} 19 | {{- if .Values.fullnameOverride -}} 20 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- $name := default .Chart.Name .Values.nameOverride -}} 23 | {{- if contains $name .Release.Name -}} 24 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 25 | {{- else -}} 26 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 27 | {{- end -}} 28 | {{- end -}} 29 | {{- end -}} 30 | 31 | {{/* 32 | Create chart name and version as used by the chart label. 33 | */}} 34 | {{- define "[[ .ApplicationName ]].chart" -}} 35 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 36 | {{- end -}} 37 | 38 | {{/* 39 | Common labels 40 | */}} 41 | {{- define "[[ .ApplicationName ]].labels" -}} 42 | helm.sh/chart: {{ include "[[ .ApplicationName ]].chart" . }} 43 | {{ include "[[ .ApplicationName ]].selectorLabels" . }} 44 | {{- if .Chart.AppVersion }} 45 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 46 | {{- end }} 47 | app.kubernetes.io/managed-by: {{ .Release.Service }} 48 | {{- end -}} 49 | 50 | {{/* 51 | Selector labels 52 | */}} 53 | {{- define "[[ .ApplicationName ]].selectorLabels" -}} 54 | app.kubernetes.io/name: {{ include "[[ .ApplicationName ]].name" . }} 55 | app.kubernetes.io/instance: {{ .Release.Name }} 56 | {{- end -}} 57 | 58 | {{/* 59 | Create the name of the service account to use 60 | */}} 61 | {{- define "[[ .ApplicationName ]].serviceAccountName" -}} 62 | {{- if .Values.serviceAccount.create -}} 63 | {{ default (include "[[ .ApplicationName ]].fullname" .) .Values.serviceAccount.name }} 64 | {{- else -}} 65 | {{ default "default" .Values.serviceAccount.name }} 66 | {{- end -}} 67 | {{- end -}} 68 | ` 69 | 70 | // DeploymentYaml holds the contents of the deployment.yaml file 71 | const DeploymentYaml = `apiVersion: apps/v1 72 | kind: Deployment 73 | metadata: 74 | name: {{ template "[[ .ApplicationName ]].fullname" . }} 75 | labels: 76 | {{- include "[[ .ApplicationName ]].labels" . | nindent 4 }} 77 | spec: 78 | replicas: {{ .Values.replicaCount }} 79 | selector: 80 | matchLabels: 81 | {{- include "[[ .ApplicationName ]].selectorLabels" . | nindent 6 }} 82 | template: 83 | metadata: 84 | labels: 85 | {{- include "[[ .ApplicationName ]].selectorLabels" . | nindent 8 }} 86 | spec: 87 | {{- with .Values.imagePullSecrets }} 88 | imagePullSecrets: 89 | {{- toYaml . | nindent 8 }} 90 | {{- end }} 91 | serviceAccountName: {{ include "[[ .ApplicationName ]].serviceAccountName" . }} 92 | securityContext: 93 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 94 | containers: 95 | - name: {{ .Chart.Name }} 96 | securityContext: 97 | {{- toYaml .Values.securityContext | nindent 12 }} 98 | image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" 99 | imagePullPolicy: {{ .Values.image.pullPolicy }} 100 | ports: 101 | - name: http 102 | containerPort: 80 103 | protocol: TCP 104 | resources: 105 | {{- toYaml .Values.resources | nindent 12 }} 106 | {{- with .Values.nodeSelector }} 107 | nodeSelector: 108 | {{- toYaml . | nindent 8 }} 109 | {{- end }} 110 | {{- with .Values.affinity }} 111 | affinity: 112 | {{- toYaml . | nindent 8 }} 113 | {{- end }} 114 | {{- with .Values.tolerations }} 115 | tolerations: 116 | {{- toYaml . | nindent 8 }} 117 | {{- end }} 118 | ` 119 | 120 | // ServiceYaml holds the contents of the service.yaml file 121 | const ServiceYaml = `apiVersion: v1 122 | kind: Service 123 | metadata: 124 | name: {{ include "[[ .ApplicationName ]].fullname" . }} 125 | labels: 126 | {{- include "[[ .ApplicationName ]].labels" . | nindent 4 }} 127 | spec: 128 | type: {{ .Values.service.type }} 129 | ports: 130 | - port: {{ .Values.service.port }} 131 | targetPort: http 132 | protocol: TCP 133 | name: http 134 | selector: 135 | {{- include "[[ .ApplicationName ]].selectorLabels" . | nindent 4 }} 136 | ` 137 | 138 | // ServiceAccountYaml holds the contents of the serviceaccount.yaml file 139 | const ServiceAccountYaml = `{{- if .Values.serviceAccount.create -}} 140 | apiVersion: v1 141 | kind: ServiceAccount 142 | metadata: 143 | name: {{ include "[[ .ApplicationName ]].serviceAccountName" . }} 144 | labels: 145 | {{ include "[[ .ApplicationName ]].labels" . | nindent 4 }} 146 | {{- end -}} 147 | ` 148 | 149 | // TestConnectionYaml holds the contents of the test-connection.yaml file 150 | const TestConnectionYaml = `apiVersion: v1 151 | kind: Pod 152 | metadata: 153 | name: "{{ include "[[ .ApplicationName ]].fullname" . }}-test-connection" 154 | labels: 155 | {{ include "[[ .ApplicationName ]].labels" . | nindent 4 }} 156 | annotations: 157 | "helm.sh/hook": test-success 158 | spec: 159 | containers: 160 | - name: wget 161 | image: busybox 162 | command: ['wget'] 163 | args: ['{{ include "[[ .ApplicationName ]].fullname" . }}:{{ .Values.service.port }}'] 164 | restartPolicy: Never 165 | ` 166 | 167 | // HelmIgnore holds the contents of the .helmignore file 168 | const HelmIgnore = `# Patterns to ignore when building packages. 169 | # This supports shell glob matching, relative path matching, and 170 | # negation (prefixed with !). Only one pattern per line. 171 | .DS_Store 172 | # Common VCS dirs 173 | .git/ 174 | .gitignore 175 | .bzr/ 176 | .bzrignore 177 | .hg/ 178 | .hgignore 179 | .svn/ 180 | # Common backup files 181 | *.swp 182 | *.bak 183 | *.tmp 184 | *~ 185 | # Various IDEs 186 | .project 187 | .idea/ 188 | *.tmproj 189 | .vscode/ 190 | ` 191 | 192 | // ChartYaml holds the contents of the Chart.yaml file 193 | const ChartYaml = `apiVersion: v2 194 | name: [[ .ApplicationName ]] 195 | description: A Helm chart for [[ .ApplicationName ]] service 196 | type: application 197 | appVersion: 0.1.0 198 | version: 0.1.0` 199 | 200 | // ValuesYaml holds the contents of the values.yaml file 201 | const ValuesYaml = `replicaCount: 1 202 | image: 203 | repository: [[ .DockerRepository ]] 204 | pullPolicy: Always 205 | 206 | imagePullSecrets: [] 207 | 208 | serviceAccount: 209 | create: true 210 | name: 211 | 212 | podSecurityContext: {} 213 | securityContext: {} 214 | 215 | service: 216 | type: ClusterIP 217 | port: 80 218 | 219 | resources: {} 220 | nodeSelector: {} 221 | tolerations: [] 222 | affinity: {} 223 | ` 224 | -------------------------------------------------------------------------------- /internal/artifacts/layoutcreator.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | // LayoutCreator should be implemented by project layouts to create project files 4 | type LayoutCreator interface { 5 | Create(outputFolder string) error 6 | } 7 | -------------------------------------------------------------------------------- /internal/artifacts/projectfile.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | 8 | "github.com/peterj/kapp/internal/utils" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ProjectFile represents a file that's part of a project 13 | type ProjectFile struct { 14 | Template string 15 | TargetFileNamePath string 16 | Delimiters *Delims 17 | } 18 | 19 | // NewProjectFile creates a new project file 20 | func NewProjectFile(template, targetFileName string, delims *Delims) *ProjectFile { 21 | return &ProjectFile{ 22 | Template: template, 23 | TargetFileNamePath: targetFileName, 24 | Delimiters: delims, 25 | } 26 | } 27 | 28 | // Write writes contents to the project file 29 | func (p *ProjectFile) Write(rootPath string, contents []byte) error { 30 | fullPath := path.Join(rootPath, p.TargetFileNamePath) 31 | if err := utils.CreateFolder(fullPath); err != nil { 32 | return errors.Wrap(err, fmt.Sprintf("Write: create folder: '%s'", fullPath)) 33 | } 34 | if err := ioutil.WriteFile(fullPath, contents, 0644); err != nil { 35 | return errors.Wrap(err, fmt.Sprintf("Write: file: '%s'", fullPath)) 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/artifacts/projectfile_test.go: -------------------------------------------------------------------------------- 1 | package artifacts_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/peterj/kapp/internal/artifacts" 7 | ) 8 | 9 | const succeeded = "\u2713" 10 | const failed = "\u2717" 11 | 12 | func TestProjectFile(t *testing.T) { 13 | tt := []struct { 14 | testName string 15 | template string 16 | targetFileNamePath string 17 | delimLeft string 18 | delimRight string 19 | }{ 20 | { 21 | testName: "create ProjectFile instance", 22 | template: "Contents of the template are here", 23 | targetFileNamePath: "somefile.txt", 24 | delimLeft: "{{", 25 | delimRight: "}}", 26 | }, 27 | } 28 | 29 | t.Log("Give the need to test project file creation") 30 | { 31 | defaultDelims := NewDefaultDelims() 32 | for i, tst := range tt { 33 | t.Logf("\tTest %d: \t%s", i, tst.testName) 34 | { 35 | projectFile := NewProjectFile(tst.template, tst.targetFileNamePath, defaultDelims) 36 | if projectFile.Template != tst.template { 37 | t.Fatalf("\t%s\tShould have the correct template contents : exp[%s] got[%s]\n", failed, tst.template, projectFile.Template) 38 | } 39 | t.Logf("\t%s\tShould have the correct template contents\n", succeeded) 40 | 41 | if projectFile.TargetFileNamePath != tst.targetFileNamePath { 42 | t.Fatalf("\t%s\tShould have the correct target filename path : exp[%s] got[%s]\n", failed, tst.targetFileNamePath, projectFile.TargetFileNamePath) 43 | } 44 | t.Logf("\t%s\tShould have the correct target filename path\n", succeeded) 45 | 46 | if projectFile.Delimiters.Left != tst.delimLeft { 47 | t.Fatalf("\t%s\tShould have the correct left delimiters : exp[%s] got[%s]\n", failed, tst.delimLeft, projectFile.Delimiters.Left) 48 | } 49 | t.Logf("\t%s\tShould have the correct left delimiters\n", succeeded) 50 | 51 | if projectFile.Delimiters.Right != tst.delimRight { 52 | t.Fatalf("\t%s\tShould have the correct right delimiters : exp[%s] got[%s]\n", failed, tst.delimRight, projectFile.Delimiters.Right) 53 | } 54 | t.Logf("\t%s\tShould have the correct right delimiters\n", succeeded) 55 | 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/artifacts/projectinfo.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | // ProjectInfo holds info we need to create a project 4 | type ProjectInfo struct { 5 | ApplicationName string 6 | PackageName string 7 | VersionFileName string 8 | DockerRepository string 9 | } 10 | 11 | // NewProjectInfo creates a new ProjectInfo 12 | func NewProjectInfo(applicationName, packageName, dockerRepository string) *ProjectInfo { 13 | return &ProjectInfo{ 14 | ApplicationName: applicationName, 15 | PackageName: packageName, 16 | DockerRepository: dockerRepository, 17 | VersionFileName: "VERSION.txt", 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/artifacts/projectinfo_test.go: -------------------------------------------------------------------------------- 1 | package artifacts_test 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | . "github.com/peterj/kapp/internal/artifacts" 9 | ) 10 | 11 | func TestProjectInfo(t *testing.T) { 12 | tt := []struct { 13 | testName string 14 | appName string 15 | versionFileName string 16 | packageName string 17 | dockerRepositoryName string 18 | }{ 19 | { 20 | testName: "create ProjectInfo instance", 21 | appName: "myapp", 22 | versionFileName: "VERSION.txt", 23 | packageName: "github.com/test/myapp", 24 | dockerRepositoryName: "myrepo.registry.io/something", 25 | }, 26 | } 27 | 28 | t.Log("Given the need to test project info creation") 29 | { 30 | for i, tst := range tt { 31 | t.Logf("\tTest %d: \t%s", i, tst.testName) 32 | { 33 | projectInfo := NewProjectInfo(tst.appName, tst.packageName, tst.dockerRepositoryName) 34 | 35 | if projectInfo.ApplicationName != tst.appName { 36 | t.Fatalf("\t%s\tShould have the correct app name : exp[%s] got[%s]\n", failed, tst.appName, projectInfo.ApplicationName) 37 | } 38 | t.Logf("\t%s\tShould have the correct app name\n", succeeded) 39 | 40 | if projectInfo.PackageName != tst.packageName { 41 | t.Fatalf("\t%s\tShould have the correct package name : exp[%s] got[%s]\n", failed, tst.packageName, projectInfo.PackageName) 42 | } 43 | t.Logf("\t%s\tShould have the correct package name\n", succeeded) 44 | 45 | if projectInfo.DockerRepository != tst.dockerRepositoryName { 46 | t.Fatalf("\t%s\tShould have the correct Docker repository name : exp[%s] got[%s]\n", failed, tst.dockerRepositoryName, projectInfo.DockerRepository) 47 | } 48 | t.Logf("\t%s\tShould have the correct Docker repository name\n", succeeded) 49 | 50 | if projectInfo.VersionFileName != tst.versionFileName { 51 | t.Fatalf("\t%s\tShould have the correct version file name : exp[%s] got[%s]\n", failed, tst.versionFileName, projectInfo.VersionFileName) 52 | } 53 | t.Logf("\t%s\tShould have the correct version file name\n", succeeded) 54 | } 55 | } 56 | } 57 | 58 | t.Log("Given the need to test writing to a file") 59 | { 60 | fileName := "targetfile.txt" 61 | fileContents := []byte("file contents") 62 | 63 | p := NewProjectFile("template", fileName, NewDefaultDelims()) 64 | tempFolder := os.TempDir() 65 | 66 | // Write to the file 67 | p.Write(tempFolder, fileContents) 68 | 69 | targetFilePath := path.Join(tempFolder, fileName) 70 | _, err := os.Stat(targetFilePath) 71 | if err != nil { 72 | t.Fatalf("\t%s\tFile should exist : exp[%s] got[%s]\n", failed, targetFilePath, err.Error()) 73 | } 74 | t.Logf("\t%s\tCorrect file should exist\n", succeeded) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/artifacts/projectlayout.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | 8 | "github.com/peterj/kapp/internal/utils" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ProjectLayout holds the information about the project and files 13 | // in that project 14 | type ProjectLayout struct { 15 | ProjectInfo *ProjectInfo 16 | Files []*ProjectFile 17 | } 18 | 19 | // NewProjectLayout creates a new project layout using project and project files 20 | func NewProjectLayout(project *ProjectInfo, projectFiles []*ProjectFile) (LayoutCreator, error) { 21 | return &ProjectLayout{ 22 | Files: projectFiles, 23 | ProjectInfo: project, 24 | }, nil 25 | } 26 | 27 | // Create will write files in project layout to the output folder 28 | func (p *ProjectLayout) Create(outputFolder string) error { 29 | // Get the output path by appending the output folder to 30 | // the current working folder 31 | outputPath, err := utils.FullPath(outputFolder) 32 | if err != nil { 33 | return errors.Wrap(err, fmt.Sprintf("Create: output folder: '%s'", outputFolder)) 34 | } 35 | 36 | for _, f := range p.Files { 37 | tmpl := template.New("tmpl").Delims(f.Delimiters.Left, f.Delimiters.Right) 38 | tmpl, err := tmpl.Parse(f.Template) 39 | if err != nil { 40 | return errors.Wrap(err, fmt.Sprintf("Create: parse template: '%s'", f.TargetFileNamePath)) 41 | } 42 | 43 | var result bytes.Buffer 44 | if err := tmpl.ExecuteTemplate(&result, "tmpl", p.ProjectInfo); err != nil { 45 | return errors.Wrap(err, fmt.Sprintf("Create: execute template: '%s'", result.String())) 46 | } 47 | if err := f.Write(outputPath, result.Bytes()); err != nil { 48 | return errors.Wrap(err, "Create: write") 49 | } 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/artifacts/projectlayout_test.go: -------------------------------------------------------------------------------- 1 | package artifacts_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | . "github.com/peterj/kapp/internal/artifacts" 10 | ) 11 | 12 | func TestProjectLayout(t *testing.T) { 13 | tt := []struct { 14 | testName string 15 | projectInfo *ProjectInfo 16 | projectFiles []*ProjectFile 17 | }{ 18 | { 19 | testName: "create ProjectLayout instance", 20 | projectInfo: &ProjectInfo{ 21 | ApplicationName: "app", 22 | PackageName: "package", 23 | VersionFileName: "VERSION.txt", 24 | DockerRepository: "dockerrepo", 25 | }, 26 | projectFiles: []*ProjectFile{ 27 | { 28 | Delimiters: &Delims{ 29 | Left: "{{", 30 | Right: "}}", 31 | }, 32 | TargetFileNamePath: "firstfile", 33 | Template: "template", 34 | }, 35 | { 36 | Delimiters: &Delims{ 37 | Left: "{{", 38 | Right: "}}", 39 | }, 40 | TargetFileNamePath: "secondfile", 41 | Template: "template", 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | t.Log("Given the need to test project layout creation") 48 | { 49 | for i, tst := range tt { 50 | t.Logf("\tTest %d: \t%s", i, tst.testName) 51 | { 52 | creator, _ := NewProjectLayout(tst.projectInfo, tst.projectFiles) 53 | layout, _ := creator.(*ProjectLayout) 54 | 55 | if len(layout.Files) != len(tst.projectFiles) { 56 | t.Fatalf("\t%s\tShould have the correct number of files : exp[%d] got[%d]\n", failed, len(tst.projectFiles), len(layout.Files)) 57 | } 58 | t.Logf("\t%s\tShould have the correct number of files\n", succeeded) 59 | 60 | if layout.ProjectInfo.ApplicationName != tst.projectInfo.ApplicationName { 61 | t.Fatalf("\t%s\tShould have the correct app name : exp[%s] got[%s]\n", failed, tst.projectInfo.ApplicationName, layout.ProjectInfo.ApplicationName) 62 | } 63 | t.Logf("\t%s\tShould have the correct app name\n", succeeded) 64 | 65 | if layout.ProjectInfo.DockerRepository != tst.projectInfo.DockerRepository { 66 | t.Fatalf("\t%s\tShould have the correct docker repo name : exp[%s] got[%s]\n", failed, tst.projectInfo.DockerRepository, layout.ProjectInfo.DockerRepository) 67 | } 68 | t.Logf("\t%s\tShould have the correct docker repo name\n", succeeded) 69 | 70 | if layout.ProjectInfo.PackageName != tst.projectInfo.PackageName { 71 | t.Fatalf("\t%s\tShould have the correct package name : exp[%s] got[%s]\n", failed, tst.projectInfo.PackageName, layout.ProjectInfo.PackageName) 72 | } 73 | t.Logf("\t%s\tShould have the correct package name\n", succeeded) 74 | 75 | if layout.ProjectInfo.VersionFileName != tst.projectInfo.VersionFileName { 76 | t.Fatalf("\t%s\tShould have the correct version file name : exp[%s] got[%s]\n", failed, tst.projectInfo.VersionFileName, layout.ProjectInfo.VersionFileName) 77 | } 78 | t.Logf("\t%s\tShould have the correct version file name\n", succeeded) 79 | } 80 | } 81 | } 82 | 83 | createTests := []struct { 84 | testName string 85 | projectInfo *ProjectInfo 86 | projectFiles []*ProjectFile 87 | expectedContents string 88 | }{ 89 | { 90 | testName: "one project file", 91 | projectInfo: &ProjectInfo{ 92 | ApplicationName: "app", 93 | PackageName: "package", 94 | VersionFileName: "VERSION.txt", 95 | DockerRepository: "dockerrepo", 96 | }, 97 | projectFiles: []*ProjectFile{ 98 | { 99 | Delimiters: &Delims{ 100 | Left: "{{", 101 | Right: "}}", 102 | }, 103 | TargetFileNamePath: "firstfile", 104 | Template: "{{ .ApplicationName }} {{ .PackageName }} {{ .VersionFileName }} {{ .DockerRepository }}", 105 | }, 106 | }, 107 | expectedContents: "app package VERSION.txt dockerrepo", 108 | }, 109 | { 110 | testName: "multiple project files", 111 | projectInfo: &ProjectInfo{ 112 | ApplicationName: "app", 113 | PackageName: "package", 114 | VersionFileName: "VERSION.txt", 115 | DockerRepository: "dockerrepo", 116 | }, 117 | projectFiles: []*ProjectFile{ 118 | { 119 | Delimiters: &Delims{ 120 | Left: "{{", 121 | Right: "}}", 122 | }, 123 | TargetFileNamePath: "firstfile", 124 | Template: "{{ .ApplicationName }} {{ .PackageName }} {{ .VersionFileName }} {{ .DockerRepository }}", 125 | }, 126 | { 127 | Delimiters: &Delims{ 128 | Left: "{{", 129 | Right: "}}", 130 | }, 131 | TargetFileNamePath: "secondfile", 132 | Template: "{{ .ApplicationName }} {{ .PackageName }} {{ .VersionFileName }} {{ .DockerRepository }}", 133 | }, 134 | }, 135 | expectedContents: "app package VERSION.txt dockerrepo", 136 | }, 137 | { 138 | testName: "non-default delimiters, multiple project files", 139 | projectInfo: &ProjectInfo{ 140 | ApplicationName: "app", 141 | PackageName: "package", 142 | VersionFileName: "VERSION.txt", 143 | DockerRepository: "dockerrepo", 144 | }, 145 | projectFiles: []*ProjectFile{ 146 | { 147 | Delimiters: &Delims{ 148 | Left: "[[", 149 | Right: "]]", 150 | }, 151 | TargetFileNamePath: "firstfile", 152 | Template: "[[ .ApplicationName ]] [[ .PackageName ]] [[ .VersionFileName ]] [[ .DockerRepository ]]", 153 | }, 154 | { 155 | Delimiters: &Delims{ 156 | Left: "{{", 157 | Right: "}}", 158 | }, 159 | TargetFileNamePath: "secondfile", 160 | Template: "{{ .ApplicationName }} {{ .PackageName }} {{ .VersionFileName }} {{ .DockerRepository }}", 161 | }, 162 | }, 163 | expectedContents: "app package VERSION.txt dockerrepo", 164 | }, 165 | } 166 | 167 | t.Log("Given the need to test project layout Create func") 168 | { 169 | for i, tst := range createTests { 170 | t.Logf("\tTest %d: \t%s", i, tst.testName) 171 | { 172 | // Change the working folder to a temporary folder 173 | workingFolder := os.TempDir() 174 | os.Chdir(workingFolder) 175 | 176 | outputFolder := "testfolder" 177 | layout, _ := NewProjectLayout(tst.projectInfo, tst.projectFiles) 178 | layout.Create(outputFolder) 179 | 180 | // Check that the workingFolder + testFolder + targetfilenamepath was created 181 | for _, f := range tst.projectFiles { 182 | rootOutputFolder := path.Join(workingFolder, outputFolder) 183 | targetFilePath := path.Join(rootOutputFolder, f.TargetFileNamePath) 184 | if _, err := os.Stat(targetFilePath); err != nil { 185 | t.Fatalf("\t%s\tFile should exist : exp[%s] got[%s]\n", failed, targetFilePath, err.Error()) 186 | } 187 | t.Logf("\t%s\tCorrect file should exist %s\n", succeeded, f.TargetFileNamePath) 188 | 189 | // Check the file contents 190 | actualContentsBytes, _ := ioutil.ReadFile(targetFilePath) 191 | if tst.expectedContents != string(actualContentsBytes) { 192 | t.Fatalf("\t%s\tFile contents are correct : exp[%s] got[%s]\n", failed, tst.expectedContents, string(actualContentsBytes)) 193 | } 194 | t.Logf("\t%s\tCorrect file contents\n", succeeded) 195 | } 196 | } 197 | } 198 | 199 | } 200 | 201 | createErrorTests := []struct { 202 | testName string 203 | projectInfo *ProjectInfo 204 | projectFiles []*ProjectFile 205 | }{ 206 | { 207 | testName: "invalid field in the template (execute template error)", 208 | projectInfo: &ProjectInfo{ 209 | ApplicationName: "app", 210 | PackageName: "package", 211 | VersionFileName: "VERSION.txt", 212 | DockerRepository: "dockerrepo", 213 | }, 214 | projectFiles: []*ProjectFile{ 215 | { 216 | Delimiters: &Delims{ 217 | Left: "{{", 218 | Right: "}}", 219 | }, 220 | TargetFileNamePath: "firstfile", 221 | Template: "{{ .Missing }}", 222 | }, 223 | }, 224 | }, 225 | } 226 | t.Log("Given the need to test Create func errors") 227 | { 228 | for i, tst := range createErrorTests { 229 | t.Logf("\tTest %d: \t%s", i, tst.testName) 230 | { 231 | // Change the working folder to a temporary folder 232 | workingFolder := os.TempDir() 233 | os.Chdir(workingFolder) 234 | 235 | outputFolder := "testfolder" 236 | layout, _ := NewProjectLayout(tst.projectInfo, tst.projectFiles) 237 | err := layout.Create(outputFolder) 238 | if err == nil { 239 | t.Fatalf("\t%s\tError should be returned\n", failed) 240 | } 241 | t.Logf("\t%s\tError should be returned\n", succeeded) 242 | } 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /internal/utils/fileutils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // CreateFolder creates a folder from the path. 14 | func CreateFolder(path string) error { 15 | actualPath := ExtractFolder(path) 16 | if err := os.MkdirAll(actualPath, os.ModePerm); err != nil { 17 | return errors.Wrap(err, fmt.Sprintf("createFolder: %s", actualPath)) 18 | } 19 | return nil 20 | } 21 | 22 | // ExtractFolder removes the filename from the path and 23 | // returns the folder portion only 24 | func ExtractFolder(path string) string { 25 | actualPath := path 26 | 27 | // Check if there multiple parts in the path 28 | if strings.Contains(path, "/") { 29 | filename := filepath.Base(path) 30 | actualPath = strings.TrimSuffix(path, filename) 31 | actualPath = strings.TrimSuffix(actualPath, "/") 32 | } 33 | return actualPath 34 | } 35 | 36 | // FullPath returns the full path to the template 37 | func FullPath(parts ...string) (string, error) { 38 | wd, err := os.Getwd() 39 | if err != nil { 40 | return "", errors.Wrap(err, "fullPath") 41 | } 42 | // prepend wd to the parts that were passed in 43 | return path.Join(append([]string{wd}, parts...)...), nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/utils/fileutils_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | . "github.com/peterj/kapp/internal/utils" 9 | ) 10 | 11 | const succeeded = "\u2713" 12 | const failed = "\u2717" 13 | 14 | func TestFileUtils(t *testing.T) { 15 | tt := []struct { 16 | testName string 17 | path string 18 | expectedFolder string 19 | }{ 20 | { 21 | testName: "single folder", 22 | path: "foldername", 23 | expectedFolder: "foldername", 24 | }, 25 | { 26 | testName: "single folder with '/'", 27 | path: "foldername/", 28 | expectedFolder: "foldername", 29 | }, 30 | { 31 | testName: "single folder with a file", 32 | path: "foldername/myfile.txt", 33 | expectedFolder: "foldername", 34 | }, 35 | { 36 | testName: "multiple folders with a file", 37 | path: "foldername/subfolder/myfile.txt", 38 | expectedFolder: "foldername/subfolder", 39 | }, 40 | { 41 | testName: "no folder (empty path)", 42 | path: "", 43 | expectedFolder: "", 44 | }, 45 | } 46 | 47 | t.Log("Give the need to test extract folder functionality") 48 | { 49 | for i, tst := range tt { 50 | t.Logf("\tTest %d: \t%s", i, tst.testName) 51 | { 52 | actualFolder := ExtractFolder(tst.path) 53 | if actualFolder != tst.expectedFolder { 54 | t.Fatalf("\t%s\tShould have the correct folder name : exp[%s] got[%s]\n", failed, tst.expectedFolder, actualFolder) 55 | } 56 | t.Logf("\t%s\tShould have the correct folder name\n", succeeded) 57 | } 58 | } 59 | } 60 | 61 | wd, _ := os.Getwd() 62 | fullPathTests := []struct { 63 | testName string 64 | parts []string 65 | expectedPath string 66 | }{ 67 | { 68 | testName: "multiple folders", 69 | parts: []string{"one", "two", "three"}, 70 | expectedPath: path.Join(wd, "one", "two", "three"), 71 | }, 72 | { 73 | testName: "single folder", 74 | parts: []string{"one"}, 75 | expectedPath: path.Join(wd, "one"), 76 | }, 77 | { 78 | testName: "empty (no folder)", 79 | parts: []string{""}, 80 | expectedPath: path.Join(wd), 81 | }, 82 | } 83 | 84 | t.Log("Give the need to test full path functionality") 85 | { 86 | for i, tst := range fullPathTests { 87 | t.Logf("\tTest %d: \t%s", i, tst.testName) 88 | { 89 | fullPath, _ := FullPath(tst.parts...) 90 | 91 | if fullPath != tst.expectedPath { 92 | t.Fatalf("\t%s\tShould have the correct path : exp[%s] got[%s]\n", failed, tst.expectedPath, fullPath) 93 | } 94 | t.Logf("\t%s\tShould have the correct path\n", succeeded) 95 | } 96 | } 97 | } 98 | 99 | createFolderTests := []struct { 100 | testName string 101 | inputPath string 102 | expectedFolder string 103 | }{ 104 | { 105 | testName: "single folder, single file", 106 | inputPath: "somefolder/file.txt", 107 | expectedFolder: "somefolder", 108 | }, 109 | { 110 | testName: "single folder", 111 | inputPath: "myfolder", 112 | expectedFolder: "myfolder", 113 | }, 114 | { 115 | testName: "multiple folders", 116 | inputPath: "subfold1/subfold2/", 117 | expectedFolder: "subfold1/subfold2", 118 | }, 119 | { 120 | testName: "multiple folders with a file", 121 | inputPath: "subfold1/subfold2/myfile.txt", 122 | expectedFolder: "subfold1/subfold2", 123 | }, 124 | } 125 | t.Log("Given the need to test create folder functionality") 126 | { 127 | for i, tst := range createFolderTests { 128 | t.Logf("\tTest %d: \t%s", i, tst.testName) 129 | { 130 | tempFolder := os.TempDir() 131 | os.Chdir(tempFolder) 132 | CreateFolder(tst.inputPath) 133 | 134 | pathToTest := path.Join(tempFolder, tst.expectedFolder) 135 | _, err := os.Stat(pathToTest) 136 | if err != nil { 137 | t.Fatalf("\t%s\tFolder should exist : exp[%s] got[%s]\n", failed, tst.expectedFolder, pathToTest) 138 | } 139 | t.Logf("\t%s\tCorrect folder should exist\n", succeeded) 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/peterj/kapp/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // VERSION indicates the binary version 4 | var VERSION string 5 | 6 | // GITCOMMIT indicates the git hash binary was built off of 7 | var GITCOMMIT string 8 | --------------------------------------------------------------------------------