├── .DEREK.yml ├── .dockerignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── USER_GUIDE.md ├── cmd ├── apply.go ├── apply_test.go ├── create_github.go ├── registry_login.go ├── registry_login_test.go └── root.go ├── docs └── ofc-bootstrap.png ├── example.init.yaml ├── get.sh ├── go.mod ├── go.sum ├── hack ├── hashgen.sh ├── install-ci.sh └── integration-test.sh ├── main.go ├── pkg ├── github │ └── handler.go ├── ingress │ ├── ingress.go │ └── ingress_test.go ├── stack │ ├── stack.go │ └── stack_test.go ├── tls │ ├── .gitignore │ ├── issuer_test.go │ └── tls.go ├── types │ ├── deployment_test.go │ ├── merge.go │ ├── merge_test.go │ ├── secrets.go │ ├── types.go │ └── types_test.go └── validators │ ├── validators.go │ └── validators_test.go ├── scripts ├── clone-cloud-components.sh ├── create-functions-auth.sh ├── deploy-cloud-components.sh ├── export-sealed-secret-pubcert.sh ├── get-cert-manager.sh ├── get-sealedsecretscontroller.sh └── patch-fn-serviceaccount.sh ├── templates ├── aws.yml ├── dashboard_config.yml ├── edge-auth-dep.yml ├── gateway_config.yml ├── github.yml ├── github │ └── index.html ├── gitlab.yml ├── issue-prod.yml ├── k8s │ ├── ingress-auth.yml │ ├── ingress-wildcard.yml │ └── tls │ │ ├── auth-domain-cert.yml │ │ ├── issuer-prod.yml │ │ ├── issuer-staging.yml │ │ └── wildcard-domain-cert.yml ├── of-builder-dep.yml ├── slack.yml └── stack.yml └── version └── version.go /.DEREK.yml: -------------------------------------------------------------------------------- 1 | redirect: https://raw.githubusercontent.com/openfaas/faas/master/.DEREK.yml 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ofc-bootstrap 2 | tmp 3 | bootstrap 4 | pub-cert.pem 5 | .idea 6 | .vscode 7 | kubeseal 8 | key 9 | key.pub 10 | ofc-bootstrap-darwin 11 | ofc-bootstrap.exe 12 | 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | alexellis 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behaviour 4 | 5 | 6 | 7 | ## Current Behaviour 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Operating System and version (e.g. Linux, Windows, MacOS): 30 | 31 | * `faas-cli version` ( full output ): 32 | 33 | * `ofc-bootstrap version` ( full output ): 34 | 35 | * Kubernetes version `kubectl version`: 36 | 37 | * What kind of Kubernetes service or distribution are you using? 38 | 39 | * Link to your project or a code example to reproduce issue: 40 | 41 | * Please also follow the [OpenFaaS Cloud self-hosted troubleshooting guide](https://docs.openfaas.com/openfaas-cloud/self-hosted/troubleshoot/) and paste in any other diagnostic information you have: 42 | 43 | 44 | * init.yaml (obscure your secrets, but leave domains in place): 45 | 46 | ```yaml 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## How Has This Been Tested? 5 | 6 | 7 | 8 | 9 | 10 | ## Checklist: 11 | 12 | I have: 13 | 14 | - [ ] checked my changes follow the style of the existing code / OpenFaaS repos 15 | - [ ] updated the documentation and/or roadmap in README.md 16 | - [ ] read the [CONTRIBUTION](https://github.com/openfaas/faas/blob/master/CONTRIBUTING.md) guide 17 | - [ ] signed-off my commits with `git commit -s` 18 | - [ ] added unit tests 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@master 19 | with: 20 | fetch-depth: 1 21 | - name: Make all 22 | run: make ci 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | jobs: 8 | publish: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@master 15 | with: 16 | fetch-depth: 1 17 | - name: Make all 18 | run: make ci 19 | - name: Upload release binaries 20 | uses: alexellis/upload-assets@0.2.2 21 | env: 22 | GITHUB_TOKEN: ${{ github.token }} 23 | with: 24 | asset_paths: '["./bin/ofc*"]' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ofc-bootstrap 2 | tmp 3 | bootstrap 4 | pub-cert.pem 5 | .idea 6 | .vscode 7 | kubeseal 8 | key 9 | key.pub 10 | ofc-bootstrap-darwin 11 | ofc-bootstrap.exe 12 | init.yaml 13 | main 14 | bin/ 15 | config.json 16 | /credentials 17 | /github.yml 18 | /github.yaml 19 | /private-key 20 | CUSTOMERS 21 | /install.sh 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2020 Alex Ellis 4 | Copyright (c) 2018-2020 OpenFaaS Author(s) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | Version := $(shell git describe --tags --dirty) 2 | GitCommit := $(shell git rev-parse HEAD) 3 | LDFLAGS := "-s -w -X github.com/openfaas/ofc-bootstrap/cmd.Version=$(Version) -X github.com/openfaas/ofc-bootstrap/cmd.GitCommit=$(GitCommit)" 4 | SOURCE_DIRS = cmd pkg main.go 5 | export GO111MODULE=on 6 | 7 | .PHONY: all 8 | all: gofmt test dist hash 9 | 10 | .PHONY: ci 11 | ci: all install-ci ci 12 | 13 | .PHONY: build 14 | build: 15 | CGO_ENABLED=0 GOOS=linux go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o ofc-bootstrap 16 | 17 | .PHONY: gofmt 18 | gofmt: 19 | @test -z $(shell gofmt -l -s $(SOURCE_DIRS) ./ | tee /dev/stderr) || (echo "[WARN] Fix formatting issues with 'make fmt'" && exit 1) 20 | 21 | .PHONY: test 22 | test: 23 | CGO_ENABLED=0 go test $(shell go list ./... | grep -v /vendor/|xargs echo) -cover 24 | 25 | .PHONY: dist 26 | dist: 27 | mkdir -p bin 28 | CGO_ENABLED=0 GOOS=linux go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/ofc-bootstrap 29 | CGO_ENABLED=0 GOOS=darwin go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/ofc-bootstrap-darwin 30 | CGO_ENABLED=0 GOOS=windows go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/ofc-bootstrap.exe 31 | 32 | .PHONY: hash 33 | hash: 34 | rm -rf bin/*.sha256 && ./hack/hashgen.sh 35 | 36 | .PHONY: install-ci 37 | install-ci: 38 | ./hack/install-ci.sh 39 | 40 | .PHONY: ci 41 | ci: 42 | ./hack/integration-test.sh 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ofc-bootstrap 2 | 3 | > Provide a managed OpenFaaS experience for your team 4 | 5 | How? By automating the whole installation of OpenFaaS Cloud on Kubernetes into a single command and CLI. 6 | 7 | [![Build Status](https://github.com/openfaas/ofc-bootstrap/workflows/build/badge.svg?branch=master)](https://github.com/openfaas/ofc-bootstrap/actions) 8 | ### What is this and who is it for? 9 | 10 | You can use this tool to configure a Kubernetes cluster with [OpenFaaS Cloud](https://github.com/openfaas/openfaas-cloud). You just need to complete all the pre-requisites and fill out your `init.yaml` file then run the tool. It automates several pages of manual steps using Golang templates and bash scripts so that you can get your own [OpenFaaS Cloud](https://github.com/openfaas/openfaas-cloud) in around 1.5 minutes. 11 | 12 | Experience level: intermediate Kubernetes & cloud. 13 | 14 | The `ofc-bootstrap` will install the following components: 15 | 16 | * [OpenFaaS](https://github.com/openfaas/faas) installed with helm 17 | * [Nginx as your IngressController](https://github.com/kubernetes/ingress-nginx) - with rate-limits configured 18 | * [SealedSecrets](https://github.com/bitnami-labs/sealed-secrets) from Bitnami - store secrets for functions in git 19 | * [cert-manager](https://github.com/jetstack/cert-manager) - provision HTTPS certificates with LetsEncrypt 20 | * [buildkit from Docker](https://github.com/moby/buildkit) - to building immutable Docker images for each function 21 | * Authentication/authorization - through OAuth2 delegating to GitHub/GitLab 22 | * Deep integration into GitHub/GitLab - for updates and commit statuses 23 | * A personalized dashboard for each user 24 | 25 | ### Conceptual design 26 | 27 | The ofc-bootstrap tool is used to install OpenFaaS Cloud in a single click. You will need to configure it with all the necessary secrets and configuration beforehand using a YAML file. 28 | 29 | ![](./docs/ofc-bootstrap.png) 30 | 31 | > ofc-bootstrap packages a number of primitives such as an IngressController, a way to obtain certificates from LetsEncrypt, the OpenFaaS Cloud components, OpenFaaS itself and Minio for build log storage. Each component is interchangeable. 32 | 33 | ### Video demo 34 | 35 | View a video demo by Alex Ellis running `ofc-bootstrap` in around 100 seconds on DigitalOcean. 36 | 37 | [![View demo](https://img.youtube.com/vi/Sa1VBSfVpK0/0.jpg)](https://www.youtube.com/watch?v=Sa1VBSfVpK0) 38 | 39 | ## Roadmap 40 | 41 | See the [ROADMAP.md](./ROADMAP.md) for features, development status and backlogs. 42 | 43 | ## Get started 44 | 45 | Follow the [user guide](USER_GUIDE.md). 46 | 47 | ### Join us on Slack 48 | 49 | Got questions, comments or suggestions? 50 | 51 | Join the team and community over on [Slack](https://docs.openfaas.com/community) 52 | 53 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ## Roadmap 2 | 3 | ### Goals for 1.0 4 | 5 | * Install OpenFaaS and Install OpenFaaS Cloud with a single command 6 | * Mirror features and config of OpenFaaS Cloud Community Cluster 7 | * Use Kubernetes as the underlying provider/platform 8 | * Use GitHub as the SCM (the source for git) 9 | * Build via Travis 10 | * Offer a flag for sourcing configuration from a YAML file 11 | * Offer a dry-run flag or configuration in the YAML file 12 | * Build a config file for the current OpenFaaS Cloud Community Cluster 13 | * Light-touch unit-testing 14 | * Publish a static binary on GitHub Releases for `ofc-bootstrap` tool 15 | * Use GitLab for as SCM (the source for git) 16 | * Allow namespaces to be overridden from `openfaas`/`openfaas-fn` to something else 17 | 18 | ### Goals for 2.0 19 | 20 | * Add version number to YAML file i.e `1.0` to enable versioning/migration of configs 21 | * Build a suitable dev environment for local work (without Ingress, TLS) 22 | * Use the Cobra CLI package and separate CLI commands 23 | * Add a registry login command 24 | * Add a GitHub integration command 25 | * Accept several YAML override files 26 | * go modules instead of `dep` 27 | 28 | ### Non-goals 29 | 30 | * Automatic configuration of DNS Zones in GKE / AWS Route 53 31 | * Deep / extensive / complicated unit-tests 32 | * Create a Docker image / run in Docker 33 | * Installing, configuring or provisioning Kubernetes clusters or nodes 34 | * Running on a system without bash 35 | * Terraform/Ansible/Puppet style of experience 36 | * Re-run without clean-up (i.e. no updates or upgrades) 37 | * Docker Swarm support 38 | * Move code into official CLI via `faas-cli system install openfaas-cloud` 39 | * Separate out the OpenFaaS installation for the official CLI `faas-cli system install --kubernetes` 40 | 41 | ## Status 42 | 43 | Help is wanted - the code is in a private repo for OpenFaaS maintainers to contribute to. Sign-off/DCO is required and standard OpenFaaS contributing procedures apply. 44 | 45 | Status: 46 | * [x] Move to Helm3 from using Helm2/tiller. 47 | * [x] Step: generate `payload_secret` for trust 48 | * [x] Refactor: default to init.yaml if present 49 | * [x] Step: Clone OpenFaaS Cloud repo https://github.com/openfaas/openfaas-cloud 50 | * [x] Step: deploy container builder (buildkit) 51 | * [x] Step: Add Ingress controller 52 | * [x] Step: Install OpenFaaS via helm 53 | * [x] Step: Install OpenFaaS namespaces 54 | * [x] Wildcard ingress 55 | * [x] Auth ingress 56 | * [x] init.yml - define GitHub App and load via struct 57 | * [x] Step: deploy OpenFaaS Cloud primary functions 58 | * [x] Step: deploy OpenFaaS Cloud dashboard 59 | * [x] Template: dashboard stack.yml if required 60 | * [x] Template: `gateway_config.yml` 61 | * [x] Step: install SealedSecrets 62 | * [x] Step: export SealedSecrets pub-cert 63 | * [ ] Step: export all passwords required for user such as GW via `kubectl` 64 | * [x] Step: setup issuer and certificate entries for cert-manager (probably with staging cert?) - make this optional to prevent rate-limiting. 65 | * [x] Make TLS optional in the Ingress config (not to get rate-limited by LetsEncrypt) 66 | * [x] init.yml - add `github_app_id` and `WEBHOOK_SECRET` 67 | * [x] Create basic-auth secrets for the functions in `openfaas-fn` 68 | * [x] Step: Install Minio and generate keys 69 | * [x] init.yml - define and OAuth App and load via struct 70 | * [x] Step: generate secrets and keys for the auth service (see auth/README.md) 71 | * [x] Template: auth service deployment YAML file 72 | * [x] Refactor: Generate passwords via Golang code or library 73 | 74 | Add all remaining steps from [installation guide](https://github.com/openfaas/openfaas-cloud/tree/master/docs). 75 | 76 | -------------------------------------------------------------------------------- /USER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # ofc-bootstrap user-guide 2 | 3 | You will need admin access to a Kubernetes cluster, some CLI tooling and a GitHub.com account or admin access to a self-hosted GitLab instance. 4 | 5 | ## Pre-reqs 6 | 7 | This tool automates the installation of OpenFaaS Cloud on Kubernetes. Before starting you will need to install some tools and then create either a local or remote cluster. 8 | 9 | For your cluster the following specifications are recommended: 10 | 11 | * 3-4 nodes with 2 vCPU each and 4GB RAM 12 | 13 | These are guidelines and not a hard requirement, you may well be able to run with fewer resources, but please do not ask for support if you use less and run into problems. 14 | 15 | > Note: You must use Intel hardware, ARM such as arm64 and armhf (Raspberry Pi) is not supported and not on the roadmap either. This could change if a company was willing to sponsor and pay for the features and ongoing maintenance. 16 | 17 | ### Note for k3s users 18 | 19 | If you are using k3s, then you will need to disable Traefik. ofc-bootstrap uses nginx-ingress for its IngressController, but k3s ships with Traefik and this will configuration is incompatible. When you set up k3s, make sure you pass the `--no-deploy traefik` flag. 20 | 21 | Example with [k3sup](https://k3sup.dev): 22 | 23 | ```sh 24 | k3sup install --ip $IP --user $USER --k3s-extra-args "--no-deploy traefik" 25 | ``` 26 | 27 | Example with [k3d](https://github.com/rancher/k3d): 28 | 29 | ```sh 30 | k3d cluster create --k3s-server-arg "--no-deploy=traefik" 31 | ``` 32 | 33 | Newer k3d versions will require an alternative: 34 | 35 | ```bash 36 | k3d create --k3s-server-arg "--no-deploy=traefik" 37 | ``` 38 | 39 | > A note on DigitalOcean: if you're planning on using k3s with DigitalOcean, please stop and think why you are doing this instead of using the managed service called DOKS. DOKS is a free, managed control-plane and much less work for you, k3s on Droplets will be more expensive given that you have to run your own "master". 40 | 41 | ### Credentials and dependent systems 42 | 43 | OpenFaaS Cloud installs, manages, and bundles software which spans source-control, TLS, DNS, and Docker image registries. You must have the following prepared before you start your installation. 44 | 45 | * You'll need to register a domain-name and set it up for management in Google Cloud DNS, DigitalOcean, Cloudflare DNS or AWS Route 53. 46 | * Set up a registry - the simplest option is to use your [Docker Hub](https://hub.docker.com) account. You can also use your own private registry or a cloud-hosted registry. You will need the credentials. If you need to, [set up your own private registry](https://github.com/alexellis/k8s-tls-registry). 47 | * Admin-level access to a GitHub.com account or a self-hosted GitLab installation. 48 | * A valid email address for use with [LetsEncrypt](https://letsencrypt.org), beware of [rate limits](https://letsencrypt.org/docs/rate-limits/). 49 | * Admin access to a Kubernetes cluster. 50 | * The ability to create one or more git repositories - one will be for your `CUSTOMERS` Access-Control List ACL and the other will be your test repository to check that everything worked. 51 | 52 | ### Tools 53 | 54 | * Kubernetes - [development options](https://blog.alexellis.io/be-kind-to-yourself/) 55 | * OpenSSL - the `openssl` binary must be available in `PATH` 56 | * Linux or Mac. Windows if `bash` is available 57 | 58 | The following are automatically installed for you: 59 | * [helm](https://docs.helm.sh/using_helm/#installing-helm) 60 | * [faas-cli](https://github.com/openfaas/faas-cli) `curl -sL https://cli.openfaas.com | sudo sh` 61 | * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-curl) 62 | 63 | If you are using a cluster with GKE then you must run the following command: 64 | 65 | ```bash 66 | kubectl create clusterrolebinding "cluster-admin-$(whoami)" \ 67 | --clusterrole=cluster-admin \ 68 | --user="$(gcloud config get-value core/account)" 69 | ``` 70 | 71 | ## Start by creating a Kubernetes cluster 72 | 73 | You may already have a Kubernetes cluster, if not, then follow the instructions below. 74 | 75 | Pick either A or B. 76 | 77 | ### A) Create a production cluster 78 | 79 | You can create a managed or self-hosted Kubernetes cluster using a Kubernetes engine from a cloud provider, or by running either `kubeadm` or `k3s`. 80 | 81 | Cloud-services: 82 | 83 | * [DigitalOcean Kubernetes](https://www.digitalocean.com/products/kubernetes/) (recommended) 84 | * [AKS](https://docs.microsoft.com/en-us/azure/aks/) 85 | * [EKS](https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html) ([Guide](https://www.openfaas.com/blog/eks-openfaas-cloud-build-guide/)) 86 | * [GKE](https://cloud.google.com/kubernetes-engine/) 87 | 88 | Local / on-premises: 89 | 90 | * [k3s](https://k3s.io) (recommended) 91 | * [kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/) 92 | 93 | Once set up make sure you have set your `KUBECONFIG` and / or `kubectl` tool to point at a the new cluster. 94 | 95 | Check this with: 96 | 97 | ```sh 98 | arkade get kubectx 99 | kubectx 100 | ``` 101 | 102 | Do not follow the instructions for B). 103 | 104 | ### B) Create a local cluster for development / testing 105 | 106 | For testing you can create a local cluster using `kind`, `minikube` or Docker Desktop. This is how you can install `kind` to setup a local cluster in a Docker container. 107 | 108 | Create a cluster with KinD 109 | 110 | ```bash 111 | arkade get kind 112 | kind create cluster 113 | ``` 114 | 115 | KinD will automatically switch you into the new context, but feel free to check with `kubectx`. 116 | 117 | ## Get `ofc-bootstrap` 118 | 119 | Now clone the GitHub repository, download the binary release and start customising your own `init.yaml` file. 120 | 121 | * Clone the `ofc-bootstrap` repository 122 | 123 | ```bash 124 | mkdir -p $GOPATH/src/github.com/openfaas-incubator 125 | cd $GOPATH/src/github.com/openfaas-incubator/ 126 | git clone https://github.com/openfaas/ofc-bootstrap 127 | ``` 128 | 129 | * Download the latest `ofc-bootstrap` binary release from GitHub 130 | 131 | Either run the following script, or follow the manual steps below. 132 | 133 | ```sh 134 | # Download and move to /usr/local/bin 135 | curl -sLSf https://raw.githubusercontent.com/openfaas/ofc-bootstrap/master/get.sh | \ 136 | sudo sh 137 | 138 | # Or, download and move manually 139 | curl -sLSf https://raw.githubusercontent.com/openfaas/ofc-bootstrap/master/get.sh | \ 140 | sh 141 | ``` 142 | 143 | Manual steps: 144 | 145 | Download [ofc-boostrap](https://github.com/openfaas/ofc-bootstrap/releases) from the GitHub releases page and move it to `/usr/local/bin/`. 146 | 147 | You may also need to run `chmod +x /usr/local/bin/ofc-bootstrap`. 148 | 149 | For Linux use the binary with no suffix, for MacOS, use the binary with the `-darwin` suffix. 150 | 151 | ## Create your own `init.yaml` 152 | 153 | Create your own `init.yaml` file from the example: 154 | 155 | ```sh 156 | cp example.init.yaml init.yaml 157 | ``` 158 | 159 | In the following steps you will make a series of edits to the `init.yaml` file to customize it for your OpenFaaS Cloud installation. 160 | 161 | Each setting is described with a comment to help you decide what value to set. 162 | 163 | ## Set the `root_domain` 164 | 165 | Edit `root_domain` and add your own domain i.e. `example.com` or `ofc.example.com` 166 | 167 | If you picked a root domain of `example.com`, then your URLs would correspond to the following: 168 | 169 | * `system.example.com` 170 | * `auth.system.example.com` 171 | * `*.example.com` 172 | 173 | After the installation has completed in a later step, you will need to create DNS A records with your DNS provider. You don't need to create these records now. 174 | 175 | ## Prepare your Docker registry (if not using AWS ECR) 176 | 177 | > Note: If using ECR, please go to the next step. 178 | 179 | ofc-bootstrap has a command to generate the registry auth file in the correct format. 180 | 181 | If you are using Dockerhub you only need to supply your `--username` and `--password-stdin` (or `--password`, but this leaves the password in history). 182 | ```sh 183 | 184 | ofc-bootstrap registry-login --username --password-stdin 185 | (then enter your password and use ctrl+d to finish input) 186 | ``` 187 | 188 | You could also have you password in a file, or environment variable and echo/cat this instead of entering interactively 189 | 190 | If you are using a different registry (that is not ECR) then also provide a `--server` as well. 191 | 192 | 193 | Find the section of the YAML `registry: docker.io/ofctest/` 194 | 195 | You need to replace the value for your registry, note the final `/` which is required. 196 | 197 | * Valid: `registry: docker.io/my-org/` 198 | * Invalid: `registry: docker.io/my-org` 199 | 200 | * Valid: `registry: my-corp.jfrog.io/ofc-prod/` 201 | * Invalid: `registry: my-corp.jfrog.io/ofc-prod` 202 | * Invalid: `registry: my-corp.jfrog.io/` 203 | 204 | ## Prepare your Docker registry (if using AWS ECR) 205 | 206 | OpenFaaS Cloud also supports Amazon's managed container registry called ECR. 207 | 208 | * Set `enable_ecr: true` in `init.yaml` 209 | 210 | * Set your AWS region `ecr_region: "your-aws-region"` in `init.yaml` 211 | 212 | * Define a `./credentials/config.json` by running the following command 213 | 214 | ```sh 215 | ofc-bootstrap registry-login --ecr --region --account-id 216 | ``` 217 | 218 | At runtime it will use your mounted AWS credentials file from a separate secret to gain an access token for ECR. ECR access tokens need to be refreshed around every 12 hours and this is handled by the `ecr-login` binary built-into the OFC builder container image. 219 | 220 | * Set the `registry` 221 | 222 | Find the section of the YAML `registry:` set the value accordingly, replacing `ACCOUNT_ID` and `REGION` as per previous step: 223 | 224 | `$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/` 225 | 226 | The final `/` is required 227 | 228 | When using ECR a user can namespace their registries per cluster by adding a suffix to the ecr registry: 229 | 230 | `$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/your-cluster-suffix` 231 | 232 | This would create registries prefixed with `your-cluster-prefix` for the user's docker images. 233 | 234 | * Create a new user with the role `AmazonEC2ContainerRegistryFullAccess` - see also [AWS permissions for ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/ecr_managed_policies.html) 235 | 236 | * The file will be read from `~/.aws/credentials` by default, but you can change this via editing the path in `value_from` under the `ecr-credentials` secret 237 | 238 | * Get the credentials from the AWS console for your new user, and save the following file: `~/.aws/credentials` 239 | 240 | ```ini 241 | [default] 242 | aws_access_key_id = ACCESS_KEY_ID 243 | aws_secret_access_key = SECRET_ACCESS_KEY 244 | ``` 245 | 246 | ## Pick your Source Control Management (SCM) 247 | 248 | Choose SCM between GitHub.com or GitLab self-hosted. 249 | 250 | For GitHub set: 251 | 252 | ```yaml 253 | scm: github 254 | ``` 255 | 256 | For GitLab set: 257 | 258 | ```yaml 259 | scm: gitlab 260 | ``` 261 | 262 | ### Setup your GitHub or GitLab integration 263 | 264 | Setup the GitHub / GitLab App and OAuth App 265 | 266 | Your SCM will need to send webhooks to OpenFaaS Cloud's github-event or gitlab-event function for CI/CD. This is protected by a confidential secret called a *Webhook secret*. You can leave the field blank to have one generated for you, or you can set your own in `init.yaml`. 267 | 268 | * For GitHub create a GitHub App and download the private key file 269 | * Read the docs for how to [configure your GitHub App](https://docs.openfaas.com/openfaas-cloud/self-hosted/github/) 270 | * Leave the `value:` for `github-webhook-secret` blank for an auto-generated password, or set your own password in the GitHub App UI and in this section of the YAML. 271 | * Update `init.yaml` where you see the `## User-input` section 272 | * Set `app_id:` under the section named `github` with GitHub App's ID 273 | * If not using a generated value, set the `github-webhook-secret` literal value with your *Webhook secret* for the GitHub App's 274 | * Click *Generate a private key*, this will be downloaded to your local computer (if you ever need a new one, generate a new one and delete the old key) 275 | * Update the `private-key` `value_from` to the path of the GitHub App's private key 276 | * Make sure the app is "activated" using the checkbox at the bottom of the github page. 277 | 278 | 279 | * For GitLab create a System Hook 280 | * Leave the `value:` for `gitlab-webhook-secret` blank, or set your own password 281 | * Update the `## User-input` section including your System Hook's API Token and *Webhook secret* 282 | * Create your GitHub / GitLab OAuth App which is used for logging in to the dashboard 283 | * For GitLab update `init.yaml` with your `gitlab_instance` 284 | 285 | Alternatively, there are two automated ways you can create a GitHub App, but the GitHub OAuth configuration cannot be automated at this time. 286 | 287 | 1) Fully-automatic `ofc-bootstrap create-github-app` command - this is in Alpha status, but will generate a YAML file you can use with `--file` / `-f` as an override 288 | 2) Semi-automatic [GitHub App generator](http://alexellis.o6s.io/github-app) 289 | 290 | ### Setup your access control 291 | 292 | Access control to your OFC is controlled by a text file containing a list of valid usernames or organisations. This list only needs to contain organisation names, or the names of the users who are hosting repositories that OFC will manage. 293 | 294 | Create a new GitHub repository with a CUSTOMERS Access Control List (ACL) file. 295 | 296 | > Note: This repository should not contain any code or functions. 297 | 298 | * Create a new public GitHub repo 299 | * Add a file named `CUSTOMERS` and place each username or GitHub org you will use on a separate line 300 | * Find the GitHub "raw" URL (CDN) 301 | * Copy and paste the raw URL into th `init.yaml` file in `customers_url: ` 302 | 303 | ### Decide if you're using a LoadBalancer 304 | 305 | If you are using a public cloud offering and you know that they can offer a `LoadBalancer`, then the `ingress:` field will be set to `loadbalancer` which is the default. 306 | 307 | If you are deploying to a cloud or Kubernetes cluster where the type `LoadBalancer` is unavailable then you will need to change `ingress: loadbalancer` to `ingress: host` in `init.yaml`. Nginx will be configured as a `DaemonSet` exposed on port `80` and `443` on each node in your cluster. It is recommended that you create a DNS mapping between a chosen name and the IP of each node. 308 | 309 | > Note: it is a common error for new users to try to access the dashboard using the IP address of the load-balancer. 310 | > You must use the DNS name for the dashboard: i.e. `system.example.com/dashboard/username` 311 | 312 | ### Use authz (recommended) 313 | 314 | If you'd like to restrict who can log in to just those who use a GitHub account then create a GitHub OAuth App or the equivalent in GitLab. 315 | 316 | * Set `enable_oauth: ` to `true` 317 | 318 | > This feature is optional, but highly recommended 319 | 320 | Enable `auth` and fill out the OAuth App `client_id`. Configure `of-client-secret` with the OAuth App Client Secret. 321 | For GitLab set your `oauth_provider_base_url`. 322 | 323 | * Set `client_id: ` in the `oauth: ` section with the value of your OAuth `client_id` 324 | * Set `of-client-secret` in the secrets section at the top of the file using the value from your OAuth `client_secret` 325 | 326 | ### Customise s3 (not recommended) 327 | 328 | By default OpenFaaS Cloud will deploy Minio to keep track of your build logs. This can be customised to point at any compatible object storage service such as AWS S3 or DigitalOcean Spaces. 329 | 330 | Set `s3_url`, `s3_region` `s3_tls` and `s3_bucket` with appropriate values. 331 | 332 | ### Use TLS (recommended) 333 | 334 | OpenFaaS Cloud can use cert-manager to automatically provision TLS certificates for your OpenFaaS Cloud cluster using the DNS01 challenge. 335 | 336 | > This feature is optional, but highly recommended 337 | 338 | Pick between the following providers for the [DNS01 challenge](https://cert-manager.io/docs/configuration/acme/dns01/): 339 | 340 | * DigitalOcean DNS (free at time of writing) 341 | * Google Cloud DNS 342 | * AWS Route53 343 | * Cloudflare DNS 344 | 345 | > See also: [cert-manager docs for ACME/DNS01](https://cert-manager.io/docs/configuration/acme/dns01/) 346 | 347 | > Note: Comment out the relevant sections and configure as necessary 348 | 349 | You will set up the corresponding DNS A records in your DNS management dashboard after `ofc-bootstrap` has completed in the final step of the guide. 350 | 351 | In order to enable TLS, edit the following configuration: 352 | 353 | * Set `tls: true` 354 | * Choose between `issuer_type: "prod"` or `issuer_type: "staging"` 355 | * Choose between DNS Service `route53`, `clouddns`, `cloudflare` or `digitalocean` and then update `init.yaml` 356 | * If you are using an API credential for DigitalOcean, AWS or GCP, then download that file from your cloud provider and set the appropriate path. 357 | * Go to `# DNS Service Account secret` in `init.yaml` and choose and uncomment the section you need. 358 | 359 | You can start out by using the Staging issuer, then switch to the production issuer. 360 | 361 | * Set `issuer_type: "prod"` (recommended) or `issuer_type: "staging"` (for testing) 362 | 363 | 364 | > Hint: For aws route53 DNS, create your secret key file `~/Downloads/route53-secret-access-key` (the default location) with only the secret access key, no newline and no other characters. 365 | 366 | > Note if you want to switch from the staging TLS certificates to production certificates, see the appendix. 367 | 368 | ### Use a Kubernetes secret instead of a customers URL (optional) 369 | 370 | If you want to keep your list of users private, you can use a Kubernetes secret instead. 371 | 372 | Set `customers_secret:` to `true` and then edit the two secrets `customers` and `of-customers`. 373 | 374 | ### Enable dockerfile language support (optional) 375 | 376 | If you are planning on building functions using the `dockerfile` template you need to set `enable_dockerfile_lang: true`. 377 | 378 | When this value is set to `false`, your users can only use your recommended set of templates. 379 | 380 | ### Enable scaling to zero 381 | 382 | If you want your functions to scale to zero then you need to set `scale_to_zero: true`. 383 | 384 | ### Set the branch that will be built and deployed (optional) 385 | 386 | If you wish to deploy a branch other than master you can edit `build_branch` and set it to your desired branch. 387 | 388 | The default branch is `master` 389 | ## Set the OpenFaaS Cloud version (optional) 390 | 391 | This value should normally be left as per the number in the master branch, however you can edit `openfaas_cloud_version` if required. 392 | 393 | ## Toggle network policies (recommended) 394 | 395 | Network policies restriction for the `openfaas` and `openfaas-fn` namespaces are applied by default. 396 | 397 | When deployed, network policies restrict communication so that functions cannot talk to the core OpenFaaS components in the `openfaas` namespace. They also prevent functions from invoking each other directly. It is recommended to enable this feature. 398 | 399 | The default behaviour is to enable policies. If you would like to remove the restrictions, then set `network_policies: false`. 400 | 401 | ## Run `ofc-bootstrap` 402 | 403 | If you are now ready, you can run the `ofc-bootstrap` tool: 404 | 405 | ```bash 406 | cd $GOPATH/src/github.com/openfaas/ofc-bootstrap 407 | 408 | ofc-bootstrap apply --file init.yaml 409 | ``` 410 | 411 | Pay attention to the output from the tool and watch out for any errors that may come up. You will need to store the logs and share them with the maintainers if you run into any issues. 412 | 413 | ## Finish the configuration 414 | 415 | If you get anything wrong, there are some instructions in the appendix on how to make edits. It is usually easier to edit `init.yaml` and re-run the tool, or to delete your cluster and run the tool again. 416 | 417 | ## Configure DNS 418 | 419 | If you are running against a remote Kubernetes cluster you can now update your DNS entries so that they point at the IP address of your LoadBalancer found via `kubectl get svc`. 420 | 421 | When ofc-bootstrap has completed and you know the IP of your LoadBalancer: 422 | 423 | * `system.example.com` 424 | * `auth.system.example.com` 425 | * `*.example.com` 426 | 427 | ## Configure the GitHub / GitLab App Webhook 428 | 429 | Now over on GitHub / GitLab enter the URL for webhooks: 430 | 431 | GitHub.com: 432 | 433 | ``` 434 | http://system.example.com/github-event 435 | ``` 436 | 437 | GitLab self-hosted: 438 | 439 | ``` 440 | http://system.example.com/gitlab-event 441 | ``` 442 | 443 | For more details see the [GitLab instructions](https://github.com/openfaas/openfaas-cloud/blob/master/docs/GITLAB.md) in OpenFaaS Cloud. 444 | 445 | Then you need to enter the Webhook secret that was generated during the bootstrap process. Run the following commands to extract and decode it: 446 | 447 | ```sh 448 | export SECRET=$(kubectl get secret -n openfaas-fn github-webhook-secret -o jsonpath="{.data.github-webhook-secret}" | base64 --decode; echo) 449 | 450 | echo "Your webhook secret is: $SECRET" 451 | ``` 452 | 453 | Open the Github App UI and paste in the value into the "Webhook Secret" field. 454 | 455 | ## Smoke-test 456 | 457 | Now check the following and run a smoke test: 458 | 459 | * DNS is configured to the correct IP 460 | * Check TLS certificates are issued as expected 461 | * Check that you can trigger a build 462 | * Check that your build is pushing images to your registry or the Docker Hub 463 | * Check that your endpoint can be accessed 464 | 465 | ## View your dashboard 466 | 467 | Now view your dashboard over at: 468 | 469 | ``` 470 | http://system.example.com/dashboard/ 471 | ``` 472 | 473 | Just replace `` with your GitHub account. 474 | > If you have enabled OAuth you only need to navigate to system.example.com 475 | 476 | ## Trigger a build 477 | 478 | Now you can install your GitHub app on a repo, run `faas-cli new` and then rename the YAML file to `stack.yml` and do a `git push`. Your OpenFaaS Cloud cluster will build and deploy the functions found in that GitHub repo. 479 | 480 | If you're unsure how to do this, then you could use the [QuickStart for the Community Cluster](https://github.com/openfaas/community-cluster/tree/master/docs), just remember to change the URLs to your own cluster. 481 | 482 | ## Something went wrong? 483 | 484 | If you think that everything is set up correctly but want to troubleshoot then head over to the GitHub App webpage and click "Advanced" - here you can find each request/response from the GitHub push events. You can resend them or view any errors. 485 | 486 | ## Still not working? 487 | 488 | Follow the detailed [Troubleshooting Guide](https://docs.openfaas.com/openfaas-cloud/self-hosted/troubleshoot/#still-not-working) in the OpenFaaS docs. If you are still stuck after that please chat with us in #openfas-cloud on Slack. 489 | 490 | ## Access your OpenFaaS UI or API 491 | 492 | OpenFaaS Cloud abstracts away the core OpenFaaS UI and API. Your new API is driven by pushing changes into a Git repository, rather than running commands, or browsing a UI. 493 | 494 | You may still want access to your OpenFaaS cluster, in which case run the following: 495 | 496 | ```sh 497 | # Fetch your generated admin password: 498 | export PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo) 499 | 500 | # Open a tunnel to the gateway using `kubectl`: 501 | kubectl port-forward -n openfaas deploy/gateway 31112:8080 & 502 | 503 | # Point the CLI to the tunnel: 504 | export OPENFAAS_URL=http://127.0.0.1:31112 505 | 506 | # Log in: 507 | echo -n $PASSWORD | faas-cli login --username admin --password-stdin 508 | ``` 509 | 510 | At this point you can also view your UI dashboard at: http://127.0.0.1:31112 511 | 512 | ## Re-deploy the OpenFaaS Cloud functions (advanced) 513 | 514 | If you run the step above `Access your OpenFaaS UI or API`, then you can edit settings for OpenFaaS Cloud and redeploy your functions. This is an advanced step. 515 | 516 | ``` 517 | cd tmp/openfaas-cloud/ 518 | 519 | # Edit stack.yml 520 | # Edit github.yml or gitlab.yml 521 | # Edit gateway_config.yml 522 | # Edit buildshiprun_limits.yml 523 | 524 | # Edit aws.yml if you want to change AWS ECR settings such as the region 525 | 526 | # Update all functions 527 | faas-cli deploy -f stack.yml 528 | 529 | 530 | # Update AWS ECR functions if needed 531 | faas-cli deploy -f aws.yml 532 | 533 | # Update a single function, such as "buildshiprun" 534 | faas-cli deploy -f stack.yml --filter=buildshiprun 535 | ``` 536 | 537 | ## Invite your team 538 | 539 | For each user or org you want to enroll into your OpenFaaS Cloud edit the `CUSTOMERS` ACL file and add their username on a new line. 540 | 541 | ``` 542 | openfaas 543 | alexellis 544 | ``` 545 | 546 | ## Switch from staging to production TLS 547 | 548 | When you want to switch to the Production issuer from staging do the following: 549 | 550 | Flush out the staging certificates and orders 551 | 552 | ```sh 553 | kubectl delete certificates --all -n openfaas 554 | kubectl delete secret -n openfaas -l="cert-manager.io/certificate-name" 555 | kubectl delete order -n openfaas --all 556 | ``` 557 | 558 | Now update the staging references to "prod": 559 | 560 | ```sh 561 | sed -i '' s/letsencrypt-staging/letsencrypt-prod/g ./tmp/generated-ingress-ingress-wildcard.yaml 562 | sed -i '' s/letsencrypt-staging/letsencrypt-prod/g ./tmp/generated-ingress-ingress-auth.yaml 563 | sed -i '' s/letsencrypt-staging/letsencrypt-prod/g ./tmp/generated-tls-auth-domain-cert.yml 564 | sed -i '' s/letsencrypt-staging/letsencrypt-prod/g ./tmp/generated-tls-wildcard-domain-cert.yml 565 | ``` 566 | 567 | Now create the new ingress and certificates: 568 | 569 | ```sh 570 | kubectl apply -f ./tmp/generated-ingress-ingress-wildcard.yaml 571 | kubectl apply -f ./tmp/generated-ingress-ingress-auth.yaml 572 | kubectl apply -f ./tmp/generated-tls-auth-domain-cert.yml 573 | kubectl apply -f ./tmp/generated-tls-wildcard-domain-cert.yml 574 | ``` 575 | 576 | -------------------------------------------------------------------------------- /cmd/apply.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) OpenFaaS Author(s) 2020. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | package cmd 5 | 6 | import ( 7 | "bytes" 8 | b64 "encoding/base64" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "path" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/pkg/errors" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/alexellis/arkade/pkg/config" 22 | "github.com/alexellis/arkade/pkg/env" 23 | "github.com/alexellis/arkade/pkg/get" 24 | "github.com/alexellis/arkade/pkg/k8s" 25 | execute "github.com/alexellis/go-execute/pkg/v1" 26 | "github.com/openfaas/ofc-bootstrap/pkg/ingress" 27 | "github.com/openfaas/ofc-bootstrap/pkg/stack" 28 | "github.com/openfaas/ofc-bootstrap/pkg/tls" 29 | "github.com/openfaas/ofc-bootstrap/pkg/validators" 30 | 31 | "github.com/openfaas/ofc-bootstrap/pkg/types" 32 | yaml "gopkg.in/yaml.v2" 33 | ) 34 | 35 | func init() { 36 | rootCommand.AddCommand(applyCmd) 37 | 38 | applyCmd.Flags().StringArrayP("file", "f", []string{""}, "A number of init.yaml plan files") 39 | applyCmd.Flags().Bool("skip-sealedsecrets", false, "Skip SealedSecrets installation") 40 | applyCmd.Flags().Bool("skip-minio", false, "Skip Minio installation") 41 | applyCmd.Flags().Bool("skip-create-secrets", false, "Skip creating secrets") 42 | applyCmd.Flags().Bool("print-plan", false, "Print merged plan and exit") 43 | } 44 | 45 | var applyCmd = &cobra.Command{ 46 | Use: "apply", 47 | Short: "Apply configuration for OFC", 48 | RunE: runApplyCommandE, 49 | SilenceUsage: true, 50 | } 51 | 52 | type InstallPreferences struct { 53 | SkipMinio bool 54 | SkipSealedSecrets bool 55 | SkipCreateSecrets bool 56 | } 57 | 58 | func runApplyCommandE(command *cobra.Command, _ []string) error { 59 | prefs := InstallPreferences{} 60 | 61 | if os.Getuid() == 0 { 62 | return fmt.Errorf("do not run this tool as root, or on your server. Run it from your own client remotely") 63 | } 64 | 65 | files, err := command.Flags().GetStringArray("file") 66 | if err != nil { 67 | return err 68 | } 69 | printPlan, err := command.Flags().GetBool("print-plan") 70 | if err != nil { 71 | return err 72 | } 73 | 74 | prefs.SkipMinio, err = command.Flags().GetBool("skip-minio") 75 | if err != nil { 76 | return err 77 | } 78 | prefs.SkipSealedSecrets, err = command.Flags().GetBool("skip-sealedsecrets") 79 | if err != nil { 80 | return err 81 | } 82 | prefs.SkipCreateSecrets, err = command.Flags().GetBool("skip-create-secrets") 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if len(files) == 0 { 88 | return fmt.Errorf("provide one or more --file arguments") 89 | } 90 | 91 | plans := []types.Plan{} 92 | for _, yamlFile := range files { 93 | 94 | yamlBytes, err := ioutil.ReadFile(yamlFile) 95 | if err != nil { 96 | return fmt.Errorf("loading --file %s gave error: %s", yamlFile, err.Error()) 97 | } 98 | 99 | plan := types.Plan{} 100 | if err := yaml.Unmarshal(yamlBytes, &plan); err != nil { 101 | return fmt.Errorf("unmarshal of --file %s gave error: %s", yamlFile, err.Error()) 102 | } 103 | 104 | log.Printf("%s loaded\n", yamlFile) 105 | plans = append(plans, plan) 106 | } 107 | 108 | log.Printf("Loaded %d plan(s)\n", len(files)) 109 | planMerged, err := types.MergePlans(plans) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | if printPlan { 115 | out, _ := yaml.Marshal(planMerged) 116 | fmt.Println(string(out)) 117 | os.Exit(0) 118 | } 119 | 120 | plan := *planMerged 121 | 122 | plan, err = filterFeatures(plan) 123 | if err != nil { 124 | return fmt.Errorf("error while retreiving features: %s", err.Error()) 125 | } 126 | 127 | clientArch, clientOS := env.GetClientArch() 128 | userDir, err := config.InitUserDir() 129 | if err != nil { 130 | return err 131 | } 132 | fmt.Printf("User dir: %s\n", userDir) 133 | 134 | install := []string{"kubectl", "helm", "faas-cli", "arkade", "kubeseal"} 135 | if err := getTools(clientArch, clientOS, userDir, install); err != nil { 136 | return err 137 | } 138 | 139 | // pathCurrent := os.Getenv("PATH") 140 | // newPath := strings.Join(additionalPaths, ":") + ":" + pathCurrent 141 | // os.Setenv("PATH", newPath) 142 | // fmt.Printf("Path: %s\n", newPath) 143 | 144 | // To avoid cached versions of tools in /usr/local/bin/ 145 | newPath := "/bin/:/usr/bin/:/usr/sbin/:/sbin/:" + path.Join(userDir, "bin") 146 | os.Setenv("PATH", newPath) 147 | 148 | log.Printf("Validating tools available in PATH: %q\n", newPath) 149 | 150 | tools := []string{ 151 | "openssl version", 152 | "kubectl version --client", 153 | "helm version", 154 | "faas-cli version", 155 | "kubeseal --version", 156 | } 157 | 158 | if err := validateTools(tools); err != nil { 159 | return errors.Wrap(err, "validateTools") 160 | } 161 | 162 | if arch := k8s.GetNodeArchitecture(); len(arch) == 0 { 163 | return fmt.Errorf("unable to detect node architecture. Do not run as root, or directly on a Kubernetes master node") 164 | } 165 | 166 | if prefs.SkipCreateSecrets == false { 167 | if err := validatePlan(plan); err != nil { 168 | return errors.Wrap(err, "validatePlan") 169 | } 170 | } 171 | 172 | if err = createNamespaces(); err != nil { 173 | return errors.Wrap(err, "createNamespaces") 174 | } 175 | 176 | fmt.Printf("Plan loaded from: %s\n", files) 177 | 178 | os.MkdirAll("tmp", 0700) 179 | ioutil.WriteFile("tmp/go.mod", []byte("\n"), 0700) 180 | 181 | fmt.Println("Validating registry credentials file") 182 | if err := validateRegistryAuth(plan.Registry, plan.Secrets, plan.EnableECR); err != nil { 183 | return errors.Wrap(err, "error with registry credentials file") 184 | } 185 | 186 | start := time.Now() 187 | err = process(plan, prefs) 188 | done := time.Since(start) 189 | 190 | if err != nil { 191 | return fmt.Errorf("plan failed after %fs, error: %s", done.Seconds(), err.Error()) 192 | } 193 | 194 | fmt.Printf("Plan completed in %fs.\n", done.Seconds()) 195 | return nil 196 | } 197 | 198 | // Vars are variables parsed from flags 199 | type Vars struct { 200 | YamlFile string 201 | } 202 | 203 | func validateTools(tools []string) error { 204 | for _, tool := range tools { 205 | err := taskGivesStdout(tool) 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func taskGivesStdout(tool string) error { 215 | parts := strings.Split(tool, " ") 216 | args := []string{} 217 | 218 | if len(parts) > 0 { 219 | args = parts[1:] 220 | } 221 | 222 | task := execute.ExecTask{ 223 | Command: parts[0], 224 | Args: args, 225 | StreamStdio: false, 226 | } 227 | 228 | res, err := task.Execute() 229 | if err != nil { 230 | return fmt.Errorf("could not run: '%s', error: %s", tool, err) 231 | } 232 | if len(res.Stdout) == 0 { 233 | return fmt.Errorf("error executing '%s', no output was given - tool is available in PATH", task.Command) 234 | } 235 | return nil 236 | } 237 | 238 | func validateRegistryAuth(regEndpoint string, planSecrets []types.KeyValueNamespaceTuple, enableECR bool) error { 239 | if enableECR { 240 | return nil 241 | } 242 | for _, planSecret := range planSecrets { 243 | if planSecret.Name == "registry-secret" { 244 | confFileLocation := planSecret.Files[0].ExpandValueFrom() 245 | fileBytes, err := ioutil.ReadFile(confFileLocation) 246 | if err != nil { 247 | return err 248 | } 249 | return validators.ValidateRegistryAuth(regEndpoint, fileBytes) 250 | } 251 | } 252 | return nil 253 | } 254 | 255 | func validatePlan(plan types.Plan) error { 256 | for _, secret := range plan.Secrets { 257 | if featureEnabled(plan.Features, secret.Filters) { 258 | err := filesExists(secret.Files) 259 | if err != nil { 260 | return err 261 | } 262 | } 263 | } 264 | return nil 265 | } 266 | 267 | func filesExists(files []types.FileSecret) error { 268 | if len(files) > 0 { 269 | for _, file := range files { 270 | if len(file.ValueCommand) == 0 { 271 | if _, err := os.Stat(file.ExpandValueFrom()); err != nil { 272 | return err 273 | } 274 | } 275 | } 276 | } 277 | return nil 278 | } 279 | 280 | func process(plan types.Plan, prefs InstallPreferences) error { 281 | 282 | if plan.OpenFaaSCloudVersion == "" { 283 | plan.OpenFaaSCloudVersion = "master" 284 | fmt.Println("No openfaas_cloud_version set in init.yaml, using: master.") 285 | } 286 | 287 | if err := installIngressController(plan.Ingress); err != nil { 288 | return errors.Wrap(err, "installIngressController") 289 | } 290 | 291 | if !prefs.SkipCreateSecrets { 292 | createSecrets(plan) 293 | } 294 | 295 | saErr := patchFnServiceaccount() 296 | if saErr != nil { 297 | log.Println(saErr) 298 | } 299 | 300 | if !prefs.SkipMinio { 301 | accessKey, secretKey, err := getS3Credentials() 302 | if err != nil { 303 | return errors.Wrap(err, "getS3Credentials") 304 | } 305 | 306 | if len(accessKey) == 0 || len(secretKey) == 0 { 307 | return fmt.Errorf("S3 secrets returned from getS3Credentials were empty, but should have been generated") 308 | } 309 | if err := installMinio(accessKey, secretKey); err != nil { 310 | return errors.Wrap(err, "installMinio") 311 | } 312 | } 313 | 314 | if plan.TLS { 315 | if err := installCertmanager(); err != nil { 316 | return errors.Wrap(err, "installCertmanager") 317 | } 318 | } 319 | 320 | functionAuthErr := createFunctionsAuth() 321 | if functionAuthErr != nil { 322 | log.Println(functionAuthErr.Error()) 323 | } 324 | 325 | if err := installOpenfaas(plan.ScaleToZero, plan.IngressOperator, plan.OpenFaaSOperator); err != nil { 326 | return errors.Wrap(err, "unable to install openfaas") 327 | } 328 | 329 | retries := 260 330 | if plan.TLS { 331 | for i := 0; i < retries; i++ { 332 | log.Printf("Is cert-manager ready? %d/%d\n", i+1, retries) 333 | ready := certManagerReady() 334 | if ready { 335 | break 336 | } 337 | time.Sleep(time.Second * 2) 338 | } 339 | } 340 | 341 | ingressErr := ingress.Apply(plan) 342 | if ingressErr != nil { 343 | log.Println(ingressErr) 344 | } 345 | 346 | if plan.TLS { 347 | tlsErr := tls.Apply(plan) 348 | if tlsErr != nil { 349 | log.Println(tlsErr) 350 | } 351 | } 352 | 353 | fmt.Println("Creating stack.yml") 354 | 355 | if err := stack.Apply(plan); err != nil { 356 | return errors.Wrap(err, "stack.Apply(") 357 | } 358 | 359 | if !prefs.SkipSealedSecrets { 360 | if err := installSealedSecrets(); err != nil { 361 | return errors.Wrap(err, "unable to install sealed-secrets") 362 | } 363 | 364 | pubCert := exportSealedSecretPubCert() 365 | writeErr := ioutil.WriteFile("tmp/pubcert.pem", []byte(pubCert), 0700) 366 | if writeErr != nil { 367 | log.Println(writeErr) 368 | return writeErr 369 | } 370 | } 371 | 372 | if err := cloneCloudComponents(plan.OpenFaaSCloudVersion); err != nil { 373 | return errors.Wrap(err, "cloneCloudComponents") 374 | } 375 | 376 | if err := deployCloudComponents(plan); err != nil { 377 | return errors.Wrap(err, "deployCloudComponents") 378 | } 379 | 380 | return nil 381 | } 382 | 383 | func helmRepoAdd(name, repo string) error { 384 | log.Printf("Adding %s helm repo\n", name) 385 | 386 | task := execute.ExecTask{ 387 | Command: "helm", 388 | Args: []string{"repo", "add", name, repo}, 389 | StreamStdio: false, 390 | } 391 | 392 | taskRes, taskErr := task.Execute() 393 | 394 | if taskErr != nil { 395 | return taskErr 396 | } 397 | 398 | if len(taskRes.Stderr) > 0 { 399 | log.Println(taskRes.Stderr) 400 | } 401 | 402 | return nil 403 | } 404 | 405 | func helmRepoAddStable() error { 406 | log.Println("Adding stable helm repo") 407 | 408 | task := execute.ExecTask{ 409 | Command: "helm", 410 | StreamStdio: false, 411 | } 412 | 413 | taskRes, taskErr := task.Execute() 414 | 415 | if taskErr != nil { 416 | return taskErr 417 | } 418 | 419 | if len(taskRes.Stderr) > 0 { 420 | log.Println(taskRes.Stderr) 421 | } 422 | 423 | return nil 424 | } 425 | 426 | func helmRepoUpdate() error { 427 | log.Println("Updating helm repos") 428 | 429 | task := execute.ExecTask{ 430 | Command: "helm", 431 | Args: []string{"repo", "update"}, 432 | StreamStdio: false, 433 | } 434 | 435 | taskRes, taskErr := task.Execute() 436 | 437 | if taskErr != nil { 438 | return taskErr 439 | } 440 | 441 | if len(taskRes.Stderr) > 0 { 442 | log.Println(taskRes.Stderr) 443 | } 444 | 445 | return nil 446 | } 447 | 448 | func createFunctionsAuth() error { 449 | log.Println("Creating secrets for functions to consume") 450 | 451 | task := execute.ExecTask{ 452 | Command: "scripts/create-functions-auth.sh", 453 | Shell: true, 454 | StreamStdio: false, 455 | } 456 | 457 | taskRes, err := task.Execute() 458 | 459 | if err != nil { 460 | return err 461 | } 462 | 463 | if len(taskRes.Stderr) > 0 { 464 | log.Println(taskRes.Stderr) 465 | } 466 | 467 | return nil 468 | } 469 | 470 | func installIngressController(ingress string) error { 471 | log.Println("Installing ingress-nginx") 472 | 473 | env := []string{"PATH=" + os.Getenv("PATH")} 474 | 475 | // Adding wait took quite a long time, so disabling that. 476 | args := []string{"install", "ingress-nginx"} 477 | if ingress == "host" { 478 | args = append(args, "--host-mode") 479 | } 480 | 481 | task := execute.ExecTask{ 482 | Command: "arkade", 483 | Args: args, 484 | Shell: true, 485 | Env: env, 486 | StreamStdio: false, 487 | } 488 | 489 | res, err := task.Execute() 490 | if err != nil { 491 | return errors.Wrap(err, "error installing ingress-nginx") 492 | } 493 | 494 | if res.ExitCode != 0 { 495 | return fmt.Errorf("non-zero exit-code: %s %s", res.Stdout, res.Stderr) 496 | } 497 | 498 | if len(res.Stderr) > 0 { 499 | log.Printf("stderr: %s\n", res.Stderr) 500 | } 501 | return nil 502 | } 503 | 504 | func installSealedSecrets() error { 505 | log.Println("Installing sealed-secrets") 506 | 507 | var env []string 508 | args := []string{"install", "sealed-secrets", "--namespace=kube-system", "--wait"} 509 | 510 | task := execute.ExecTask{ 511 | Command: "arkade", 512 | Args: args, 513 | Shell: true, 514 | Env: env, 515 | StreamStdio: false, 516 | } 517 | 518 | res, err := task.Execute() 519 | if err != nil { 520 | return err 521 | } 522 | 523 | if res.ExitCode != 0 { 524 | return fmt.Errorf("non-zero exit-code: %s %s", res.Stdout, res.Stderr) 525 | } 526 | 527 | if len(res.Stderr) > 0 { 528 | log.Printf("stderr: %s\n", res.Stderr) 529 | } 530 | return nil 531 | } 532 | 533 | func installOpenfaas(scaleToZero, ingressOperator, openfaasOperator bool) error { 534 | log.Println("Installing openfaas") 535 | 536 | args := []string{"install", "openfaas", 537 | "--set basic_auth=true", 538 | "--set functionNamespace=openfaas-fn", 539 | "--set ingress.enabled=false", 540 | "--set gateway.scaleFromZero=true", 541 | "--set gateway.readTimeout=15m", 542 | "--set gateway.writeTimeout=15m", 543 | "--set gateway.upstreamTimeout=14m55s", 544 | "--set queueWorker.ackWait=15m", 545 | "--set faasnetes.readTimeout=5m", 546 | "--set faasnetes.writeTimeout=5m", 547 | "--set gateway.replicas=2", 548 | "--set queueWorker.replicas=2", 549 | "--set faasIdler.dryRun=" + strconv.FormatBool(!scaleToZero), 550 | "--set faasnetes.httpProbe=true", 551 | "--set faasnetes.imagePullPolicy=IfNotPresent", 552 | "--set ingressOperator.create=" + strconv.FormatBool(ingressOperator), 553 | "--set operator.create=" + strconv.FormatBool(openfaasOperator), 554 | "--wait", 555 | } 556 | 557 | task := execute.ExecTask{ 558 | Command: "arkade", 559 | Args: args, 560 | Shell: true, 561 | StreamStdio: false, 562 | } 563 | 564 | res, err := task.Execute() 565 | if err != nil { 566 | return err 567 | } 568 | 569 | if res.ExitCode != 0 { 570 | return fmt.Errorf("non-zero exit-code: %s %s", res.Stdout, res.Stderr) 571 | } 572 | 573 | if len(res.Stderr) > 0 { 574 | log.Printf("stderr: %s\n", res.Stderr) 575 | } 576 | 577 | return nil 578 | } 579 | 580 | func getS3Credentials() (string, string, error) { 581 | args := []string{"get", "secret", "-n", "openfaas-fn", "s3-access-key", "-o", "jsonpath={.data.s3-access-key}"} 582 | res, err := k8s.KubectlTask(args...) 583 | if err != nil { 584 | return "", "", err 585 | } 586 | if res.ExitCode != 0 { 587 | return "", "", fmt.Errorf("error getting s3 secret %s / %s", res.Stderr, res.Stdout) 588 | } 589 | 590 | decoded, _ := b64.StdEncoding.DecodeString(res.Stdout) 591 | accessKey := decoded 592 | 593 | args = []string{"get", "secret", "-n", "openfaas-fn", "s3-secret-key", "-o", "jsonpath={.data.s3-secret-key}"} 594 | res, err = k8s.KubectlTask(args...) 595 | if err != nil { 596 | return "", "", err 597 | } 598 | if res.ExitCode != 0 { 599 | return "", "", fmt.Errorf("error getting s3 secret %s / %s", res.Stderr, res.Stdout) 600 | } 601 | 602 | decoded, _ = b64.StdEncoding.DecodeString(res.Stdout) 603 | secretKey := decoded 604 | 605 | return string(accessKey), string(secretKey), nil 606 | } 607 | 608 | func installMinio(accessKey, secretKey string) error { 609 | log.Println("Installing minio") 610 | 611 | // # Minio has a default requests value of 4Gi RAM 612 | // # https://github.com/minio/charts/blob/master/minio/values.yaml 613 | 614 | args := []string{"install", "minio", 615 | "--namespace=openfaas", 616 | "--set persistence.enabled=false", 617 | "--set service.port=9000", 618 | "--set service.type=ClusterIP", 619 | "--set resources.requests.memory=512Mi", 620 | "--access-key=" + accessKey, 621 | "--secret-key=" + secretKey, 622 | "--wait", 623 | } 624 | 625 | task := execute.ExecTask{ 626 | Command: "arkade", 627 | Args: args, 628 | Shell: true, 629 | StreamStdio: false, 630 | } 631 | 632 | res, err := task.Execute() 633 | if err != nil { 634 | return err 635 | } 636 | 637 | if res.ExitCode != 0 { 638 | return fmt.Errorf("non-zero exit-code: %s %s", res.Stdout, res.Stderr) 639 | } 640 | 641 | if len(res.Stderr) > 0 { 642 | log.Printf("stderr: %s\n", res.Stderr) 643 | } 644 | return nil 645 | } 646 | 647 | func patchFnServiceaccount() error { 648 | log.Println("Patching openfaas-fn serviceaccount for pull secrets") 649 | 650 | task := execute.ExecTask{ 651 | Command: "scripts/patch-fn-serviceaccount.sh", 652 | Shell: true, 653 | StreamStdio: false, 654 | } 655 | 656 | taskRes, err := task.Execute() 657 | 658 | if err != nil { 659 | return err 660 | } 661 | 662 | if len(taskRes.Stderr) > 0 { 663 | log.Println(taskRes.Stderr) 664 | } 665 | return nil 666 | } 667 | 668 | func installCertmanager() error { 669 | log.Println("Installing cert-manager") 670 | 671 | args := []string{"install", "cert-manager", "--wait"} 672 | task := execute.ExecTask{ 673 | Command: "arkade", 674 | Args: args, 675 | Shell: true, 676 | StreamStdio: false, 677 | } 678 | 679 | res, err := task.Execute() 680 | if err != nil { 681 | return err 682 | } 683 | 684 | if res.ExitCode != 0 { 685 | return fmt.Errorf("non-zero exit-code: %s %s", res.Stdout, res.Stderr) 686 | } 687 | 688 | if len(res.Stderr) > 0 { 689 | log.Printf("stderr: %s\n", res.Stderr) 690 | } 691 | return nil 692 | } 693 | 694 | func createSecrets(plan types.Plan) error { 695 | for _, secret := range plan.Secrets { 696 | if featureEnabled(plan.Features, secret.Filters) { 697 | fmt.Printf("Creating secret: %s\n", secret.Name) 698 | 699 | command := types.BuildSecretTask(secret) 700 | fmt.Printf("Secret - %s %s\n", command.Command, strings.Join(command.Args, " ")) 701 | res, err := command.Execute() 702 | if err != nil { 703 | log.Println(err) 704 | } 705 | 706 | out := res.Stdout 707 | if len(res.Stderr) > 0 { 708 | out = out + " / " + res.Stderr 709 | } 710 | fmt.Printf("%s\n", out) 711 | } 712 | } 713 | 714 | return nil 715 | } 716 | 717 | func sealedSecretsReady() bool { 718 | 719 | task := execute.ExecTask{ 720 | Command: "./scripts/get-sealedsecretscontroller.sh", 721 | Shell: true, 722 | StreamStdio: false, 723 | } 724 | 725 | res, err := task.Execute() 726 | fmt.Println("sealedsecretscontroller", res.ExitCode, res.Stdout, res.Stderr, err) 727 | return res.Stdout == "1" 728 | } 729 | 730 | func exportSealedSecretPubCert() string { 731 | 732 | task := execute.ExecTask{ 733 | Command: "./scripts/export-sealed-secret-pubcert.sh", 734 | Shell: true, 735 | StreamStdio: false, 736 | Env: []string{"PATH=" + os.Getenv("PATH")}, 737 | } 738 | 739 | res, err := task.Execute() 740 | fmt.Println("secrets cert", res.ExitCode, res.Stdout, res.Stderr, err) 741 | return res.Stdout 742 | } 743 | 744 | func certManagerReady() bool { 745 | task := execute.ExecTask{ 746 | Command: "./scripts/get-cert-manager.sh", 747 | Shell: true, 748 | StreamStdio: false, 749 | } 750 | 751 | res, err := task.Execute() 752 | fmt.Println("cert-manager", res.ExitCode, res.Stdout, res.Stderr, err) 753 | return res.Stdout == "True" 754 | } 755 | 756 | func cloneCloudComponents(tag string) error { 757 | task := execute.ExecTask{ 758 | Command: "./scripts/clone-cloud-components.sh", 759 | Shell: true, 760 | Env: []string{ 761 | fmt.Sprintf("TAG=%v", tag), 762 | }, 763 | StreamStdio: false, 764 | } 765 | 766 | res, err := task.Execute() 767 | if err != nil { 768 | return err 769 | } 770 | 771 | fmt.Println(res) 772 | 773 | return nil 774 | } 775 | 776 | func deployCloudComponents(plan types.Plan) error { 777 | 778 | authEnv := "" 779 | if plan.EnableOAuth { 780 | authEnv = "ENABLE_OAUTH=true" 781 | } 782 | 783 | gitlabEnv := "" 784 | if plan.SCM == "gitlab" { 785 | gitlabEnv = "GITLAB=true" 786 | } 787 | 788 | networkPoliciesEnv := "" 789 | if plan.NetworkPolicies { 790 | networkPoliciesEnv = "ENABLE_NETWORK_POLICIES=true" 791 | } 792 | 793 | enableECREnv := "" 794 | if plan.EnableECR { 795 | enableECREnv = "ENABLE_AWS_ECR=true" 796 | } 797 | 798 | task := execute.ExecTask{ 799 | Command: "./scripts/deploy-cloud-components.sh", 800 | Shell: true, 801 | Env: []string{authEnv, 802 | gitlabEnv, 803 | networkPoliciesEnv, 804 | enableECREnv, 805 | }, 806 | StreamStdio: false, 807 | } 808 | 809 | res, err := task.Execute() 810 | if err != nil { 811 | return err 812 | } 813 | 814 | fmt.Println(res) 815 | 816 | return nil 817 | } 818 | 819 | func featureEnabled(features []string, secretFeatures []string) bool { 820 | for _, feature := range features { 821 | for _, secretFeature := range secretFeatures { 822 | if feature == secretFeature { 823 | return true 824 | } 825 | } 826 | } 827 | return false 828 | } 829 | 830 | func filterFeatures(plan types.Plan) (types.Plan, error) { 831 | var err error 832 | 833 | plan.Features = append(plan.Features, types.DefaultFeature) 834 | 835 | if plan.EnableECR == true { 836 | plan.Features = append(plan.Features, types.ECRFeature) 837 | } 838 | 839 | plan, err = filterGitRepositoryManager(plan) 840 | if err != nil { 841 | return plan, fmt.Errorf("Error while filtering features: %s", err.Error()) 842 | } 843 | 844 | if plan.TLS == true { 845 | plan, err = filterDNSFeature(plan) 846 | if err != nil { 847 | return plan, fmt.Errorf("Error while filtering features: %s", err.Error()) 848 | } 849 | } 850 | 851 | if plan.EnableOAuth == true { 852 | plan.Features = append(plan.Features, types.Auth) 853 | } 854 | 855 | return plan, err 856 | } 857 | 858 | func filterDNSFeature(plan types.Plan) (types.Plan, error) { 859 | if plan.TLSConfig.DNSService == types.DigitalOcean { 860 | plan.Features = append(plan.Features, types.DODNS) 861 | } else if plan.TLSConfig.DNSService == types.CloudDNS { 862 | plan.Features = append(plan.Features, types.GCPDNS) 863 | } else if plan.TLSConfig.DNSService == types.Route53 { 864 | plan.Features = append(plan.Features, types.Route53DNS) 865 | } else if plan.TLSConfig.DNSService == types.Cloudflare { 866 | plan.Features = append(plan.Features, types.CloudflareDNS) 867 | } else { 868 | return plan, fmt.Errorf("Error unavailable DNS service provider: %s", plan.TLSConfig.DNSService) 869 | } 870 | return plan, nil 871 | } 872 | 873 | func filterGitRepositoryManager(plan types.Plan) (types.Plan, error) { 874 | if plan.SCM == types.GitLabSCM { 875 | plan.Features = append(plan.Features, types.GitLabFeature) 876 | } else if plan.SCM == types.GitHubSCM { 877 | plan.Features = append(plan.Features, types.GitHubFeature) 878 | } else { 879 | return plan, fmt.Errorf("Error unsupported Git repository manager: %s", plan.SCM) 880 | } 881 | return plan, nil 882 | } 883 | 884 | func getTools(clientArch, clientOS, userDir string, install []string) error { 885 | tools := get.MakeTools() 886 | displayProgess := true 887 | for _, t := range install { 888 | if tool, err := getTool(t, tools); tool != nil { 889 | filePath := path.Join(path.Join(userDir, "bin"), tool.Name) 890 | if _, err := os.Stat(filePath); err != nil { 891 | _, finalName, err := get.Download(tool, clientArch, clientOS, tool.Version, get.DownloadArkadeDir, displayProgess) 892 | if err != nil { 893 | return err 894 | } 895 | fmt.Printf("Downloaded tool: %s\n", finalName) 896 | } else { 897 | fmt.Printf("Skipping tool: %s\n", tool.Name) 898 | } 899 | } else { 900 | return err 901 | } 902 | } 903 | return nil 904 | } 905 | 906 | func getTool(name string, tools []get.Tool) (*get.Tool, error) { 907 | var tool *get.Tool 908 | for _, t := range tools { 909 | if t.Name == name { 910 | tool = &t 911 | break 912 | } 913 | } 914 | if tool == nil { 915 | return nil, fmt.Errorf("unable to find tool definition") 916 | } 917 | 918 | return tool, nil 919 | } 920 | 921 | // createNamespaces is required for secrets to be created 922 | // before each app is installed. Including: cert-manager for TLS 923 | // secrets and openfaas/openfaas-fn for function secrets. 924 | func createNamespaces() error { 925 | res, err := k8s.KubectlTask("apply", "-f", "https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.yml") 926 | if err != nil { 927 | return err 928 | } 929 | if res.ExitCode != 0 { 930 | return fmt.Errorf("error creating openfaas namespaces: %s %s", res.Stdout, res.Stderr) 931 | } 932 | fmt.Printf("Applied namespaces for openfaas\n") 933 | 934 | ns := `apiVersion: v1 935 | kind: Namespace 936 | metadata: 937 | creationTimestamp: null 938 | name: cert-manager 939 | spec: {} 940 | status: {} 941 | ` 942 | buffer := bytes.NewReader([]byte(ns)) 943 | res, err = k8s.KubectlTaskStdin(buffer, "apply", "-f", "-") 944 | if err != nil { 945 | return err 946 | } 947 | if res.ExitCode != 0 { 948 | return fmt.Errorf("error creating openfaas namespaces: %s %s", res.Stdout, res.Stderr) 949 | } 950 | fmt.Printf("Applied namespaces for cert-manager\n") 951 | 952 | return nil 953 | } 954 | -------------------------------------------------------------------------------- /cmd/apply_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/openfaas/ofc-bootstrap/pkg/types" 9 | ) 10 | 11 | func Test_filterDNSFeature(t *testing.T) { 12 | tests := []struct { 13 | title string 14 | plan types.Plan 15 | expectedFeature string 16 | expectedErr error 17 | }{ 18 | { 19 | title: "DNS Service provider is Google", 20 | plan: types.Plan{TLSConfig: types.TLSConfig{DNSService: types.CloudDNS}}, 21 | expectedFeature: types.GCPDNS, 22 | expectedErr: nil, 23 | }, 24 | { 25 | title: "DNS Service provider is Amazon", 26 | plan: types.Plan{TLSConfig: types.TLSConfig{DNSService: types.Route53}}, 27 | expectedFeature: types.Route53DNS, 28 | expectedErr: nil, 29 | }, 30 | { 31 | title: "DNS Service provider is Digital Ocean", 32 | plan: types.Plan{TLSConfig: types.TLSConfig{DNSService: types.DigitalOcean}}, 33 | expectedFeature: types.DODNS, 34 | expectedErr: nil, 35 | }, 36 | { 37 | title: "DNS Service provider is Cloudflare", 38 | plan: types.Plan{TLSConfig: types.TLSConfig{DNSService: "cloudflare"}}, 39 | expectedFeature: types.CloudflareDNS, 40 | expectedErr: nil, 41 | }, 42 | { 43 | title: "DNS Service provider is not supported", 44 | plan: types.Plan{TLSConfig: types.TLSConfig{DNSService: "unsupporteddns"}}, 45 | expectedFeature: "", 46 | expectedErr: errors.New("Error unavailable DNS service provider"), 47 | }, 48 | } 49 | for _, test := range tests { 50 | t.Run(test.title, func(t *testing.T) { 51 | var planError error 52 | test.plan, planError = filterDNSFeature(test.plan) 53 | if planError != nil { 54 | wantErr := "" 55 | if test.expectedErr != nil { 56 | wantErr = test.expectedErr.Error() 57 | } 58 | 59 | if strings.Contains(planError.Error(), wantErr) == false || len(wantErr) == 0 { 60 | t.Errorf("Got plan error: %s", planError.Error()) 61 | } 62 | } 63 | 64 | for _, feature := range test.plan.Features { 65 | if feature != test.expectedFeature { 66 | t.Errorf("Unexpected feature: %s", feature) 67 | } 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func Test_filterFeatures(t *testing.T) { 74 | tests := []struct { 75 | title string 76 | planConfig types.Plan 77 | expectedFeatures []string 78 | expectedError error 79 | }{ 80 | { 81 | title: "Plan is empty only default feature is present", 82 | planConfig: types.Plan{}, 83 | expectedFeatures: []string{types.DefaultFeature}, 84 | expectedError: nil, 85 | }, 86 | { 87 | title: "Every field which defines populated feature is populated for github", 88 | planConfig: types.Plan{ 89 | SCM: types.GitHubSCM, 90 | TLS: true, 91 | TLSConfig: types.TLSConfig{ 92 | DNSService: types.Route53, 93 | }, 94 | EnableOAuth: true, 95 | }, 96 | expectedFeatures: []string{types.DefaultFeature, types.GitHubFeature, types.Auth, types.Route53DNS}, 97 | expectedError: nil, 98 | }, 99 | { 100 | title: "AWS ECR is detected as a feature", 101 | planConfig: types.Plan{ 102 | EnableECR: true, 103 | }, 104 | expectedFeatures: []string{types.DefaultFeature, types.ECRFeature}, 105 | expectedError: nil, 106 | }, 107 | { 108 | title: "AWS ECR is disabled when not set as a feature", 109 | planConfig: types.Plan{ 110 | EnableECR: false, 111 | }, 112 | expectedFeatures: []string{types.DefaultFeature}, 113 | expectedError: nil, 114 | }, 115 | { 116 | title: "Every field which defines populated feature is populated for gitlab", 117 | planConfig: types.Plan{ 118 | SCM: types.GitLabSCM, 119 | TLS: true, 120 | TLSConfig: types.TLSConfig{ 121 | DNSService: types.Route53, 122 | }, 123 | EnableOAuth: true, 124 | }, 125 | expectedFeatures: []string{types.DefaultFeature, types.GitLabFeature, types.Auth, types.Route53DNS}, 126 | expectedError: nil, 127 | }, 128 | { 129 | title: "Example in which the function throws error in this case the SCM field is empty", 130 | planConfig: types.Plan{ 131 | TLS: true, 132 | TLSConfig: types.TLSConfig{ 133 | DNSService: types.Route53, 134 | }, 135 | EnableOAuth: true, 136 | }, 137 | expectedFeatures: []string{types.DefaultFeature}, 138 | expectedError: errors.New("Error while filtering features"), 139 | }, 140 | { 141 | title: "Auth and TLS are enabled along with GitLab", 142 | planConfig: types.Plan{ 143 | TLS: true, 144 | TLSConfig: types.TLSConfig{ 145 | DNSService: types.Route53, 146 | }, 147 | EnableOAuth: true, 148 | SCM: types.GitHubSCM, 149 | }, 150 | expectedFeatures: []string{types.DefaultFeature, types.Auth, types.Route53DNS}, 151 | expectedError: nil, 152 | }, 153 | } 154 | 155 | for _, test := range tests { 156 | 157 | t.Run(test.title, func(t *testing.T) { 158 | 159 | var filterError error 160 | test.planConfig, filterError = filterFeatures(test.planConfig) 161 | t.Logf("Features in the plan: %v", test.planConfig.Features) 162 | 163 | if filterError != nil && test.expectedError != nil { 164 | 165 | if !strings.Contains(filterError.Error(), test.expectedError.Error()) { 166 | t.Errorf("Expected error to contain: `%s` got: `%s`", test.expectedError.Error(), filterError.Error()) 167 | } 168 | 169 | } 170 | 171 | for _, expectedFeature := range test.expectedFeatures { 172 | for allPlanFeatures, enabledFeature := range test.planConfig.Features { 173 | if len(test.planConfig.Features) == 0 { 174 | t.Errorf("Feature 'default' should always be present") 175 | } 176 | if expectedFeature == enabledFeature { 177 | break 178 | } 179 | if allPlanFeatures == len(test.planConfig.Features)-1 { 180 | t.Errorf("Feature: '%s' not found in: %v", expectedFeature, test.planConfig.Features) 181 | } 182 | } 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func Test_filterGitRepositoryManager(t *testing.T) { 189 | tests := []struct { 190 | title string 191 | planConfig types.Plan 192 | expectedFeature []string 193 | expectedError error 194 | }{ 195 | { 196 | title: "SCM field is populated for gitlab", 197 | planConfig: types.Plan{ 198 | SCM: types.GitLabSCM, 199 | }, 200 | expectedFeature: []string{types.GitLabFeature}, 201 | expectedError: nil, 202 | }, 203 | { 204 | title: "SCM field is populated for github", 205 | planConfig: types.Plan{ 206 | SCM: types.GitHubSCM, 207 | }, 208 | expectedFeature: []string{types.GitHubFeature}, 209 | expectedError: nil, 210 | }, 211 | { 212 | title: "SCM field is populated for with unsupported Git repository manager", 213 | planConfig: types.Plan{ 214 | SCM: "bitbucket", 215 | }, 216 | expectedFeature: []string{}, 217 | expectedError: errors.New("Error unsupported Git repository manager: bitbucket"), 218 | }, 219 | } 220 | for _, test := range tests { 221 | t.Run(test.title, func(t *testing.T) { 222 | var configErr error 223 | test.planConfig, configErr = filterGitRepositoryManager(test.planConfig) 224 | if configErr != nil && test.expectedError != nil { 225 | if !strings.EqualFold(configErr.Error(), test.expectedError.Error()) { 226 | t.Errorf("Expected error: '%s' got: '%s'", test.expectedError, configErr) 227 | } 228 | } 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /cmd/create_github.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "github.com/inlets/inletsctl/pkg/names" 16 | "github.com/openfaas/ofc-bootstrap/pkg/github" 17 | "github.com/openfaas/ofc-bootstrap/pkg/types" 18 | "github.com/pkg/errors" 19 | "github.com/spf13/cobra" 20 | yaml "gopkg.in/yaml.v2" 21 | ) 22 | 23 | var createGitHubAppCommand = &cobra.Command{ 24 | Use: "create-github-app", 25 | Short: "Create a GitHub App", 26 | Long: `Creates a GitHub App on GitHub.com. This is required for receiving 27 | webhooks, for checking out code for builds, and updating commit statuses. 28 | 29 | With a --root-domain of ofc.example.com, your webhooks will be sent to: 30 | https://system.ofc.example.com/github-event`, 31 | SilenceUsage: true, 32 | RunE: createGitHubAppE, 33 | Example: ` ofc-bootstrap create-github-app --root-domain o6s.io \ 34 | --name community-cluster 35 | `, 36 | } 37 | 38 | func init() { 39 | rootCommand.AddCommand(createGitHubAppCommand) 40 | 41 | createGitHubAppCommand.Flags().String("root-domain", "", "The root domain for your GitHub i.e. ofc.example.com") 42 | createGitHubAppCommand.Flags().String("name", "", "Name your GitHub App, or leave blank to get a generated name.") 43 | createGitHubAppCommand.Flags().Bool("insecure", false, "Use HTTP instead of HTTPS for webhooks") 44 | createGitHubAppCommand.Flags().Int("port", 30010, "HTTP port to listen to for GitHub App automation") 45 | } 46 | 47 | func createGitHubAppE(command *cobra.Command, _ []string) error { 48 | 49 | name := "" 50 | if nameFlagVal, _ := command.Flags().GetString("name"); len(nameFlagVal) > 0 { 51 | name = nameFlagVal 52 | } else { 53 | name = "OFC " + strings.Replace(names.GetRandomName(10), "_", " ", -1) 54 | } 55 | 56 | var rootDomain string 57 | if rootDomain, _ = command.Flags().GetString("root-domain"); len(rootDomain) == 0 { 58 | return fmt.Errorf("give a value for --root-domain") 59 | } 60 | 61 | port, portErr := command.Flags().GetInt("port") 62 | if portErr != nil { 63 | return fmt.Errorf("--port error: %s", portErr.Error()) 64 | } 65 | 66 | scheme := "https" 67 | if insecure, _ := command.Flags().GetBool("insecure"); insecure { 68 | scheme = "http" 69 | } 70 | 71 | inputMap := map[string]string{ 72 | "AppName": name, 73 | "GitHubEvent": fmt.Sprintf("%s://system.%s/github-event", scheme, rootDomain), 74 | } 75 | 76 | if _, err := os.Stat(path.Join("./templates", "github", "index.html")); err != nil { 77 | return fmt.Errorf(`cannot find template "index.html", run this command from the ofc-bootstrap repository`) 78 | } 79 | 80 | fmt.Printf("Name: %s\tRoot domain: %s\tScheme: %v\n", name, rootDomain, scheme) 81 | 82 | launchBrowser := true 83 | 84 | context, cancel := context.WithCancel(context.TODO()) 85 | defer cancel() 86 | resCh := make(chan github.AppResult) 87 | go func() { 88 | appRes := <-resCh 89 | fmt.Printf("GitHub App result received.\n") 90 | printResult(rootDomain, appRes) 91 | 92 | cancel() 93 | }() 94 | 95 | if err := receiveGitHubApp(context, inputMap, resCh, launchBrowser, port); err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func receiveGitHubApp(ctx context.Context, inputMap map[string]string, resCh chan github.AppResult, launchBrowser bool, listenPort int) error { 103 | 104 | server := &http.Server{ 105 | Addr: fmt.Sprintf(":%d", listenPort), 106 | ReadTimeout: 10 * time.Second, 107 | WriteTimeout: 10 * time.Second, 108 | MaxHeaderBytes: 1 << 20, // Max header of 1MB 109 | Handler: http.HandlerFunc(github.MakeHandler(inputMap, resCh)), 110 | } 111 | 112 | go func() { 113 | fmt.Printf("Starting local HTTP server on port %d\n", listenPort) 114 | if err := server.ListenAndServe(); err != nil { 115 | panic(err) 116 | } 117 | }() 118 | 119 | defer server.Shutdown(ctx) 120 | 121 | localURL, err := url.Parse("http://" + "127.0.0.1" + server.Addr) 122 | 123 | if err != nil { 124 | return err 125 | } 126 | 127 | fmt.Printf("Launching browser: %s\n", localURL.String()) 128 | if launchBrowser { 129 | err := launchURL(localURL.String()) 130 | if err != nil { 131 | return errors.Wrap(err, "unable to launch browser") 132 | } 133 | } 134 | 135 | fmt.Printf("Please complete the workflow in the browser to create your GitHub App.\n") 136 | 137 | <-ctx.Done() 138 | return nil 139 | } 140 | 141 | func printResult(rootDomain string, appRes github.AppResult) { 142 | p := types.Plan{ 143 | RootDomain: rootDomain, 144 | Github: types.Github{ 145 | AppID: fmt.Sprintf("%d", appRes.ID), 146 | }, 147 | Secrets: []types.KeyValueNamespaceTuple{ 148 | { 149 | Name: "github-webhook-secret", 150 | Literals: []types.KeyValueTuple{ 151 | { 152 | Name: "github-webhook-secret", 153 | Value: appRes.WebhookSecret, 154 | }, 155 | }, 156 | Filters: []string{"scm_github"}, 157 | Namespace: "openfaas-fn", 158 | }, 159 | { 160 | Name: "private-key", 161 | Literals: []types.KeyValueTuple{ 162 | { 163 | Name: "private-key", 164 | Value: appRes.PEM, 165 | }, 166 | }, 167 | Filters: []string{"scm_github"}, 168 | Namespace: "openfaas-fn", 169 | }, 170 | }, 171 | } 172 | res, _ := yaml.Marshal(p) 173 | 174 | fmt.Printf("App: %s\tURL: %s\nYAML file\n\n", appRes.Name, appRes.URL) 175 | 176 | fmt.Printf("%s\n", string(res)) 177 | 178 | } 179 | 180 | // launchURL opens a URL with the default browser for Linux, MacOS or Windows. 181 | func launchURL(serverURL string) error { 182 | ctx := context.Background() 183 | var command *exec.Cmd 184 | switch runtime.GOOS { 185 | case "linux": 186 | command = exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf(`xdg-open "%s"`, serverURL)) 187 | case "darwin": 188 | command = exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf(`open "%s"`, serverURL)) 189 | case "windows": 190 | escaped := strings.Replace(serverURL, "&", "^&", -1) 191 | command = exec.CommandContext(ctx, "cmd", "/c", fmt.Sprintf(`start %s`, escaped)) 192 | } 193 | command.Stdout = os.Stdout 194 | command.Stdin = os.Stdin 195 | command.Stderr = os.Stderr 196 | return command.Run() 197 | } 198 | -------------------------------------------------------------------------------- /cmd/registry_login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var registryLoginCommand = &cobra.Command{ 17 | Use: "registry-login", 18 | Short: "Generate and save the registry authentication file", 19 | SilenceUsage: true, 20 | RunE: generateRegistryAuthFile, 21 | } 22 | 23 | func init() { 24 | rootCommand.AddCommand(registryLoginCommand) 25 | 26 | registryLoginCommand.Flags().String("server", "https://index.docker.io/v1/", "The server URL, it is defaulted to the docker registry") 27 | registryLoginCommand.Flags().StringP("username", "u", "", "The Registry Username") 28 | registryLoginCommand.Flags().String("password", "", "The registry password") 29 | registryLoginCommand.Flags().BoolP("password-stdin", "s", false, "Reads the docker password from stdin, either pipe to the command or remember to press ctrl+d when reading interactively") 30 | 31 | registryLoginCommand.Flags().Bool("ecr", false, "If we are using ECR we need a different set of flags, so if this is set, we need to set --username and --password") 32 | registryLoginCommand.Flags().String("account-id", "", "Your AWS Account id") 33 | registryLoginCommand.Flags().String("region", "", "Your AWS region") 34 | } 35 | 36 | func generateRegistryAuthFile(command *cobra.Command, _ []string) error { 37 | ecrEnabled, _ := command.Flags().GetBool("ecr") 38 | accountID, _ := command.Flags().GetString("account-id") 39 | region, _ := command.Flags().GetString("region") 40 | username, _ := command.Flags().GetString("username") 41 | password, _ := command.Flags().GetString("password") 42 | server, _ := command.Flags().GetString("server") 43 | passStdIn, _ := command.Flags().GetBool("password-stdin") 44 | 45 | if len(username) == 0 { 46 | return fmt.Errorf("you must give --username (-u)") 47 | } 48 | 49 | var generateErr error 50 | if ecrEnabled { 51 | generateErr = generateECRFile(accountID, region) 52 | } else { 53 | if passStdIn { 54 | fmt.Printf("Enter your password, hit enter then type Ctrl+D\n\nPassword: ") 55 | passwordStdin, err := ioutil.ReadAll(os.Stdin) 56 | if err != nil { 57 | return err 58 | } 59 | generateErr = generateFile(username, strings.TrimSpace(string(passwordStdin)), server) 60 | } else { 61 | generateErr = generateFile(username, password, server) 62 | } 63 | } 64 | 65 | if generateErr != nil { 66 | return generateErr 67 | } 68 | 69 | fmt.Printf("\nWrote ./credentials/config.json..OK\n") 70 | 71 | return nil 72 | } 73 | 74 | func generateFile(username string, password string, server string) error { 75 | 76 | fileBytes, err := generateRegistryAuth(server, username, password) 77 | if err != nil { 78 | return err 79 | } 80 | return writeFileToOFCTmp(fileBytes) 81 | } 82 | 83 | func generateECRFile(accountID string, region string) error { 84 | 85 | fileBytes, err := generateECRRegistryAuth(accountID, region) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return writeFileToOFCTmp(fileBytes) 91 | } 92 | 93 | func generateRegistryAuth(server, username, password string) ([]byte, error) { 94 | if len(username) == 0 || len(password) == 0 || len(server) == 0 { 95 | return nil, errors.New("both --username and (--password-stdin or --password) are required") 96 | } 97 | 98 | encodedString := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) 99 | data := RegistryAuth{ 100 | AuthConfigs: map[string]Auth{ 101 | server: {Base64AuthString: encodedString}, 102 | }, 103 | } 104 | 105 | registryBytes, err := json.MarshalIndent(data, "", " ") 106 | 107 | return registryBytes, err 108 | } 109 | 110 | func generateECRRegistryAuth(accountID, region string) ([]byte, error) { 111 | if len(accountID) == 0 || len(region) == 0 { 112 | return nil, errors.New("you must provide an --account-id and --region when using --ecr") 113 | } 114 | 115 | data := ECRRegistryAuth{ 116 | CredsStore: "ecr-login", 117 | CredHelpers: map[string]string{ 118 | fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", accountID, region): "ecr-login", 119 | }, 120 | } 121 | 122 | registryBytes, err := json.MarshalIndent(data, "", " ") 123 | 124 | return registryBytes, err 125 | } 126 | 127 | func writeFileToOFCTmp(fileBytes []byte) error { 128 | path := "./credentials" 129 | if _, err := os.Stat(path); os.IsNotExist(err) { 130 | err := os.Mkdir(path, 0744) 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | 136 | writeErr := ioutil.WriteFile(filepath.Join(path, "config.json"), fileBytes, 0744) 137 | 138 | return writeErr 139 | 140 | } 141 | 142 | type Auth struct { 143 | Base64AuthString string `json:"auth"` 144 | } 145 | 146 | type RegistryAuth struct { 147 | AuthConfigs map[string]Auth `json:"auths"` 148 | } 149 | 150 | type ECRRegistryAuth struct { 151 | CredsStore string `json:"credsStore"` 152 | CredHelpers map[string]string `json:"credHelpers"` 153 | } 154 | -------------------------------------------------------------------------------- /cmd/registry_login_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func Test_GenerateRegistryAuth(t *testing.T) { 12 | registryURL := "https://index.docker.io/v1/" 13 | username := "docker_user" 14 | password := "docker_password" 15 | gotBytes, _ := generateRegistryAuth(registryURL, username, password) 16 | got, err := bytesToRegistryStruct(gotBytes) 17 | 18 | if err != nil { 19 | t.Errorf("Error converting bytes to struct, %q", err) 20 | t.Fail() 21 | } 22 | 23 | want := RegistryAuth{ 24 | AuthConfigs: map[string]Auth{ 25 | registryURL: {Base64AuthString: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))}, 26 | }, 27 | } 28 | 29 | if !reflect.DeepEqual(got, want) { 30 | t.Errorf("Structs were not equal, want: %q\ngot:%q", want, got) 31 | t.Fail() 32 | } 33 | } 34 | 35 | func Test_GenerateRegistryAuthNoRegistryURL(t *testing.T) { 36 | registryURL := "" 37 | username := "docker_user" 38 | password := "docker_password" 39 | _, err := generateRegistryAuth(registryURL, username, password) 40 | 41 | if err == nil { 42 | t.Error("Want err, got nil") 43 | t.Fail() 44 | } 45 | } 46 | func Test_GenerateRegistryAuthNoUsername(t *testing.T) { 47 | registryURL := "https://index.docker.io/v1" 48 | username := "" 49 | password := "docker_password" 50 | _, err := generateRegistryAuth(registryURL, username, password) 51 | 52 | if err == nil { 53 | t.Error("We were expecting an error as 'username' was empty, but we didnt get one") 54 | t.Fail() 55 | } 56 | } 57 | func Test_GenerateRegistryAuthNoPassword(t *testing.T) { 58 | registryURL := "https://index.docker.io/v1" 59 | username := "docker_user" 60 | password := "" 61 | _, err := generateRegistryAuth(registryURL, username, password) 62 | 63 | if err == nil { 64 | t.Error("We were expecting an error as 'password' was empty, but we didnt get one") 65 | t.Fail() 66 | } 67 | } 68 | func Test_GenerateRegistryAuthNoInputs(t *testing.T) { 69 | registryURL := "" 70 | username := "" 71 | password := "" 72 | _, err := generateRegistryAuth(registryURL, username, password) 73 | 74 | if err == nil { 75 | t.Error("We were expecting an error as all inputs were empty, but we didnt get one") 76 | t.Fail() 77 | } 78 | } 79 | 80 | func Test_GenerateECRRegistryAuth(t *testing.T) { 81 | region := "eu-west-2" 82 | accountId := "1234567" 83 | gotBytes, _ := generateECRRegistryAuth(accountId, region) 84 | got, err := bytesToECRStruct(gotBytes) 85 | 86 | if err != nil { 87 | t.Errorf("Error converting bytes to struct, %q", err) 88 | t.Fail() 89 | } 90 | 91 | want := ECRRegistryAuth{ 92 | CredsStore: "ecr-login", 93 | CredHelpers: map[string]string{ 94 | fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", accountId, region): "ecr-login", 95 | }, 96 | } 97 | 98 | if !reflect.DeepEqual(got, want) { 99 | t.Error("Structs were not equal, the generated bytes did not match the expected output") 100 | t.Fail() 101 | } 102 | } 103 | 104 | func Test_GenerateECRRegistryAuthNoRegion(t *testing.T) { 105 | region := "" 106 | accountId := "1234567" 107 | _, err := generateECRRegistryAuth(accountId, region) 108 | 109 | if err == nil { 110 | t.Error("We were expecting an error as 'region' was empty, but we didnt get one") 111 | t.Fail() 112 | } 113 | } 114 | 115 | func Test_GenerateECRRegistryAuthNoAccountId(t *testing.T) { 116 | region := "eu-west-2" 117 | accountId := "" 118 | _, err := generateECRRegistryAuth(accountId, region) 119 | 120 | if err == nil { 121 | t.Error("We were expecting an error as 'accountId' was empty, but we didnt get one") 122 | t.Fail() 123 | } 124 | } 125 | 126 | func Test_GenerateECRRegistryAuthNoAccountIdOrRegion(t *testing.T) { 127 | region := "" 128 | accountId := "" 129 | _, err := generateECRRegistryAuth(accountId, region) 130 | 131 | if err == nil { 132 | t.Error("We were expecting an error as 'accountId' and 'region' were empty, but we didnt get one") 133 | t.Fail() 134 | } 135 | } 136 | 137 | func bytesToECRStruct(bytes []byte) (ECRRegistryAuth, error) { 138 | obj := ECRRegistryAuth{} 139 | err := json.Unmarshal(bytes, &obj) 140 | 141 | return obj, err 142 | } 143 | 144 | func bytesToRegistryStruct(bytes []byte) (RegistryAuth, error) { 145 | obj := RegistryAuth{} 146 | err := json.Unmarshal(bytes, &obj) 147 | 148 | return obj, err 149 | 150 | } 151 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) OpenFaaS Author(s) 2020. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/morikuni/aec" 10 | "github.com/openfaas/ofc-bootstrap/version" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | // Version as per git repo 16 | Version string 17 | 18 | // GitCommit as per git repo 19 | GitCommit string 20 | ) 21 | 22 | // WelcomeMessage to introduce ofc-bootstrap 23 | const WelcomeMessage = "Welcome to ofc-bootstrap! Find out more at https://github.com/openfaas/ofc-bootstrap" 24 | 25 | func init() { 26 | rootCommand.AddCommand(versionCmd) 27 | rootCommand.Flags().StringArrayP("yaml", "f", []string{""}, "The init.yaml plan file") 28 | } 29 | 30 | var rootCommand = &cobra.Command{ 31 | Use: "ofc-bootstrap", 32 | Short: "Bootstrap OpenFaaS Cloud.", 33 | Long: ` 34 | Bootstrap OpenFaaS Cloud 35 | `, 36 | RunE: runRootCommand, 37 | SilenceUsage: true, 38 | } 39 | 40 | var versionCmd = &cobra.Command{ 41 | Use: "version", 42 | Short: "Display version information.", 43 | Run: parseBaseCommand, 44 | } 45 | 46 | func getVersion() string { 47 | if len(Version) != 0 { 48 | return Version 49 | } 50 | return "dev" 51 | } 52 | 53 | func parseBaseCommand(_ *cobra.Command, _ []string) { 54 | printLogo() 55 | 56 | fmt.Printf( 57 | `ofc-bootstrap 58 | 59 | Bootstrap your own self-hosted OpenFaaS Cloud 60 | 61 | Commit: %s 62 | Version: %s 63 | 64 | `, version.GitCommit, version.GetVersion()) 65 | } 66 | 67 | func Execute(version, gitCommit string) error { 68 | 69 | // Get Version and GitCommit values from main.go. 70 | Version = version 71 | GitCommit = gitCommit 72 | 73 | if err := rootCommand.Execute(); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func runRootCommand(cmd *cobra.Command, args []string) error { 80 | 81 | if cmd.Flags().Changed("yaml") { 82 | return fmt.Errorf("a breaking change was introduced, you now need to use ofc-bootstrap apply --file init.yaml") 83 | } 84 | 85 | printLogo() 86 | cmd.Help() 87 | 88 | return nil 89 | } 90 | 91 | func printLogo() { 92 | logoText := aec.WhiteF.Apply(version.Logo) 93 | fmt.Println(logoText) 94 | } 95 | -------------------------------------------------------------------------------- /docs/ofc-bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfaas/ofc-bootstrap/cc56acc3c51ab58ca16da22da967bec6ca031de5/docs/ofc-bootstrap.png -------------------------------------------------------------------------------- /example.init.yaml: -------------------------------------------------------------------------------- 1 | secrets: 2 | ### Generated secrets (do not edit) 3 | - name: s3-secret-key 4 | literals: 5 | - name: s3-secret-key 6 | filters: 7 | - "default" 8 | namespace: "openfaas-fn" 9 | - name: s3-access-key 10 | literals: 11 | - name: s3-access-key 12 | filters: 13 | - "default" 14 | namespace: "openfaas-fn" 15 | - name: basic-auth 16 | literals: 17 | - name: basic-auth-user 18 | value: admin 19 | - name: basic-auth-password 20 | value: "" 21 | filters: 22 | - "default" 23 | namespace: "openfaas" 24 | - name: "payload-secret" 25 | literals: 26 | - name: payload-secret 27 | value: "" 28 | filters: 29 | - "default" 30 | namespace: "openfaas" 31 | - name: "jwt-private-key" 32 | files: 33 | - name: "key" 34 | value_from: "./tmp/key" 35 | value_command: "openssl ecparam -genkey -name prime256v1 -noout -out ./tmp/key" 36 | filters: 37 | - "auth" 38 | namespace: "openfaas" 39 | - name: "jwt-public-key" 40 | files: 41 | - name: "key.pub" 42 | value_from: "./tmp/key.pub" 43 | value_command: "openssl ec -in ./tmp/key -pubout -out ./tmp/key.pub" 44 | filters: 45 | - "auth" 46 | namespace: "openfaas" 47 | 48 | ### User-input 49 | ### In this section, you must populate all your secrets or secret file-locations 50 | ### and your desired configuration. 51 | ### For more information see: https://github.com/openfaas/openfaas-cloud/tree/master/docs 52 | 53 | ## This value is used by Github to talk to system-github-event, the password will be 54 | ## generated if left blank. Alternatively, you can enter a password here of your own. 55 | ## Enter it into the GitHub App's UI. 56 | - name: "github-webhook-secret" 57 | literals: 58 | - name: "github-webhook-secret" 59 | value: "" 60 | filters: 61 | - "scm_github" 62 | namespace: "openfaas-fn" 63 | 64 | # Download from GitHub App on GitHub UI 65 | - name: "private-key" 66 | files: 67 | - name: "private-key" 68 | value_from: "~/Downloads/private-key.pem" 69 | filters: 70 | - "scm_github" 71 | namespace: "openfaas-fn" 72 | # Populate your OAuth client_secret 73 | - name: "of-client-secret" 74 | literals: 75 | - name: of-client-secret 76 | value: "79163355e553b477957d977b0b8addd3c42ff52d" 77 | filters: 78 | - "auth" 79 | namespace: "openfaas" 80 | 81 | # Enter your GitLab Webhook secret and API token 82 | - name: "gitlab-webhook-secret" 83 | literals: 84 | - name: "gitlab-webhook-secret" 85 | value: "" 86 | filters: 87 | - "scm_gitlab" 88 | namespace: "openfaas-fn" 89 | - name: "gitlab-api-token" 90 | literals: 91 | - name: "gitlab-api-token" 92 | value: "token" 93 | filters: 94 | - "scm_gitlab" 95 | namespace: "openfaas-fn" 96 | 97 | # DNS Service Account secret for DNS01 (wildcard) challenge 98 | 99 | ## Use DigitalOcean 100 | ### Create a Personal Access Token and save it into a file, with no new-lines 101 | - name: "digitalocean-dns" 102 | files: 103 | - name: "access-token" 104 | value_from: "~/Downloads/do-access-token" 105 | filters: 106 | - "do_dns01" 107 | namespace: "cert-manager" 108 | 109 | ## Use Google Cloud DNS 110 | ### Create a service account for DNS management and export it 111 | - name: "clouddns-service-account" 112 | files: 113 | - name: "service-account.json" 114 | value_from: "~/Downloads/service-account.json" 115 | filters: 116 | - "gcp_dns01" 117 | namespace: "cert-manager" 118 | 119 | ## Use Route 53 120 | ### Create role and download its secret access key 121 | - name: "route53-credentials-secret" 122 | files: 123 | - name: "secret-access-key" 124 | value_from: "~/Downloads/route53-secret-access-key" 125 | filters: 126 | - "route53_dns01" 127 | namespace: "cert-manager" 128 | 129 | ## Use Cloudflare 130 | ### Create role and download its secret access key 131 | - name: "cloudflare-api-key-secret" 132 | files: 133 | - name: "api-key" 134 | value_from: "~/Downloads/cloudflare-secret-access-key" 135 | filters: 136 | - "cloudflare_dns01" 137 | namespace: "cert-manager" 138 | 139 | # Used by Buildkit to push images to your registry 140 | - name: "registry-secret" 141 | files: 142 | - name: "config.json" 143 | value_from: "./credentials/config.json" 144 | filters: 145 | - "default" 146 | namespace: "openfaas" 147 | 148 | # Used to pull functions / images to nodes by Kubernetes 149 | - name: "registry-pull-secret" 150 | files: 151 | - name: ".dockerconfigjson" 152 | value_from: "./credentials/config.json" 153 | namespace: "openfaas-fn" 154 | filters: 155 | - "default" 156 | type: "kubernetes.io/dockerconfigjson" 157 | 158 | # ECR credentials to push to AWS ECR 159 | ## Make sure you do not use your admin account in ~/.aws/credentials, but a 160 | ## new user with ECR power-user permissions only. 161 | - name: "aws-ecr-credentials" 162 | files: 163 | - name: "credentials" 164 | value_from: "~/.aws/credentials" 165 | filters: 166 | - "ecr" 167 | namespace: "openfaas" 168 | 169 | - name: "aws-ecr-createrepo-credentials" 170 | files: 171 | - name: "credentials" 172 | value_from: "~/.aws/credentials" 173 | filters: 174 | - "ecr" 175 | namespace: "openfaas-fn" 176 | 177 | ## If not using a HTTPS URL, then set a list of CUSTOMERS 178 | ## To use this set "value_from" to a real file path, and put in a list (each item on a new line) of usernames, without other whitespace 179 | - name: "of-customers" 180 | files: 181 | - name: "of-customers" 182 | value_from: "/dev/null" 183 | namespace: "openfaas" 184 | filters: 185 | - "default" 186 | - name: "customers" 187 | files: 188 | - name: "customers" 189 | value_from: "/dev/null" 190 | namespace: "openfaas-fn" 191 | filters: 192 | - "default" 193 | 194 | ### Docker registry 195 | #### This can be any cluster accessible by your cluster. To populate the file 196 | #### run `docker login` with "store in keychain" turned off in Docker Desktop. 197 | #### This can also be your private registry 198 | #### Format: registry/username/ - i.e. replace ofctest with your login 199 | 200 | registry: docker.io/ofctest/ 201 | 202 | ### Use a secret instead of a publicly accessible URL for the ACL 203 | ### of valid users. 204 | customers_secret: false 205 | 206 | ### Enable only if using AWS ECR 207 | enable_ecr: false 208 | 209 | ### Change if your using ECR 210 | ecr_config: 211 | ### The region to use for ECR 212 | ecr_region: "eu-central-1" 213 | 214 | ### Your root DNS domain name, this can be a sub-domain i.e. staging.o6s.io / prod.o6s.io 215 | root_domain: "myfaas.club" 216 | 217 | ## Ingress into OpenFaaS Cloud 218 | 219 | ### Keep active if using a cluster with a LoadBalancer available. 220 | ingress: loadbalancer 221 | 222 | ### Uncomment if using on-premises or a host/cloud without a loadbalancer 223 | # ingress: host 224 | 225 | ## Define the custom templates available for your users 226 | ### If needed edit the git-tar Deployment after running the tool 227 | deployment: 228 | custom_templates: 229 | - "https://github.com/openfaas-incubator/golang-http-template.git" 230 | - "https://github.com/openfaas-incubator/node10-express-template.git" 231 | - "https://github.com/openfaas-incubator/python-flask-template.git" 232 | - "https://github.com/openfaas-incubator/ruby-http" 233 | 234 | ## Source Control Management 235 | ### Pick either github or gitlab 236 | scm: github 237 | # scm: gitlab 238 | 239 | ## Populate from GitHub App 240 | github: 241 | app_id: "24304" 242 | public_link: "https://github.com/apps/o6s-io" 243 | 244 | ## GitLab 245 | ### Public URL for your GitLab instance with a trailing slash 246 | gitlab: 247 | gitlab_instance: "https://gitlab.o6s.io/" 248 | 249 | 250 | ## Enable auth: 251 | ### When enabled users must log in with a valid GitHub account and be present in the 252 | ### customers file to view any dashboard 253 | enable_oauth: false 254 | 255 | ## Populate from OAuth App 256 | oauth: 257 | client_id: clientid 258 | 259 | ## For GitLab put your OAuth provider base URL 260 | # oauth_provider_base_url: "https://gitlab.o6s.io" 261 | 262 | ## For GitHub leave blank 263 | oauth_provider_base_url: "" 264 | 265 | ## Slack 266 | ### You can set your own url to get an audit trail in your Slack workspace 267 | ### You can edit this after deployment if needed in the audit function 268 | slack: 269 | url: http://gateway.openfaas:8080/function/echo 270 | 271 | ### Users allowed to access your OpenFaaS Cloud 272 | #### ACL for your users, it must be a raw text file or GitHub RAW URL 273 | #### At time of writing this _must_ be a public repo URL 274 | customers_url: "https://raw.githubusercontent.com/openfaas/openfaas-cloud/master/CUSTOMERS" 275 | 276 | ## S3 configuration 277 | ### Build logs from buildkit are stored in S3 278 | ### Defaults to in-cluster deployment of Minio. AWS S3 is also possible 279 | s3: 280 | s3_url: minio.openfaas.svc.cluster.local:9000 281 | s3_region: us-east-1 282 | s3_tls: false 283 | s3_bucket: pipeline 284 | 285 | ## TLS 286 | tls: false 287 | tls_config: 288 | # issuer_type: "prod" 289 | 290 | issuer_type: "staging" 291 | email: "your@email.com" 292 | 293 | ## Select DNS web service between Amazon Route 53 (route53) and Google Cloud DNS (clouddns) 294 | # by uncommenting the required option 295 | 296 | ### DigitalOcean 297 | dns_service: digitalocean 298 | 299 | ### Google Cloud DNS 300 | # dns_service: clouddns 301 | # project_id: "my-openfaas-cloud" 302 | 303 | ### AWS Route53 304 | # dns_service: route53 305 | # region: us-east-1 306 | # access_key_id: ASYAKIUJE8AYRQQ7DU3M 307 | 308 | ### Cloudflare 309 | # dns_service: cloudflare 310 | 311 | ## Dockerfile language support 312 | ### Use with caution, it allows any workload to be built and run 313 | enable_dockerfile_lang: false 314 | 315 | ## Set to true to enable scaling to zero 316 | ### When enabled, all functions are included by default, to turn off add a label 317 | ### of com.openfaas.scale.zero: "false" 318 | scale_to_zero: false 319 | 320 | ## Enable network policies 321 | ### Prevents functions from talking to the openfaas namespace, and to each other. 322 | ### Use the ingress address for the gateway or the external IP instead. 323 | network_policies: false 324 | 325 | ## Branch that OpenFaaS Cloud will build and deploy 326 | ## You should change this if you want a different branch to be built and deployed instead of master 327 | build_branch: master 328 | 329 | ## This setting, if true, will install the openfaas ingress-operator using the openfaas-fn namespace 330 | ## for finding functions, creating Ingress records in the openfaas namespace 331 | ingress_operator: false 332 | 333 | ## Version of OpenFaaS Cloud from https://github.com/openfaas/openfaas-cloud/releases/ 334 | ### Usage: release tag, a SHA or branch name 335 | openfaas_cloud_version: 0.14.6 336 | 337 | ## This setting, if true, will deploy OpenFaaS and use the OpenFaaS operator CRD controller, 338 | ## default uses faas-netes as the Kubernetes controller 339 | openfaas_operator: false 340 | -------------------------------------------------------------------------------- /get.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright OpenFaaS Author(s) 2020 4 | ######################### 5 | # Repo specific content # 6 | ######################### 7 | 8 | export VERIFY_CHECKSUM=0 9 | export ALIAS="" 10 | export OWNER=openfaas 11 | export REPO=ofc-bootstrap 12 | export SUCCESS_CMD="$REPO version" 13 | export BINLOCATION="/usr/local/bin" 14 | 15 | ############################### 16 | # Content common across repos # 17 | ############################### 18 | 19 | version=$(curl -sI https://github.com/$OWNER/$REPO/releases/latest | grep -i "location:" | awk -F"/" '{ printf "%s", $NF }' | tr -d '\r') 20 | if [ ! $version ]; then 21 | echo "Failed while attempting to install $REPO. Please manually install:" 22 | echo "" 23 | echo "1. Open your web browser and go to https://github.com/$OWNER/$REPO/releases" 24 | echo "2. Download the latest release for your platform. Call it '$REPO'." 25 | echo "3. chmod +x ./$REPO" 26 | echo "4. mv ./$REPO $BINLOCATION" 27 | if [ -n "$ALIAS_NAME" ]; then 28 | echo "5. ln -sf $BINLOCATION/$REPO /usr/local/bin/$ALIAS_NAME" 29 | fi 30 | exit 1 31 | fi 32 | 33 | hasCli() { 34 | 35 | hasCurl=$(which curl) 36 | if [ "$?" = "1" ]; then 37 | echo "You need curl to use this script." 38 | exit 1 39 | fi 40 | } 41 | 42 | checkHash(){ 43 | 44 | sha_cmd="sha256sum" 45 | 46 | if [ ! -x "$(command -v $sha_cmd)" ]; then 47 | sha_cmd="shasum -a 256" 48 | fi 49 | 50 | if [ -x "$(command -v $sha_cmd)" ]; then 51 | 52 | targetFileDir=${targetFile%/*} 53 | 54 | (cd $targetFileDir && curl -sSL $url.sha256|$sha_cmd -c >/dev/null) 55 | 56 | if [ "$?" != "0" ]; then 57 | rm $targetFile 58 | echo "Binary checksum didn't match. Exiting" 59 | exit 1 60 | fi 61 | fi 62 | } 63 | 64 | getPackage() { 65 | uname=$(uname) 66 | userid=$(id -u) 67 | 68 | suffix="" 69 | case $uname in 70 | "Darwin") 71 | suffix="-darwin" 72 | ;; 73 | "MINGW"*) 74 | suffix=".exe" 75 | BINLOCATION="$HOME/bin" 76 | mkdir -p $BINLOCATION 77 | 78 | ;; 79 | "Linux") 80 | arch=$(uname -m) 81 | echo $arch 82 | case $arch in 83 | "aarch64") 84 | suffix="-arm64" 85 | ;; 86 | esac 87 | case $arch in 88 | "armv6l" | "armv7l") 89 | suffix="-armhf" 90 | ;; 91 | esac 92 | ;; 93 | esac 94 | 95 | targetFile="/tmp/$REPO$suffix" 96 | 97 | if [ "$userid" != "0" ]; then 98 | targetFile="$(pwd)/$REPO$suffix" 99 | fi 100 | 101 | if [ -e $targetFile ]; then 102 | rm $targetFile 103 | fi 104 | 105 | url=https://github.com/$OWNER/$REPO/releases/download/$version/$REPO$suffix 106 | echo "Downloading package $url as $targetFile" 107 | 108 | curl -sSL $url --output $targetFile 109 | 110 | if [ "$?" = "0" ]; then 111 | 112 | if [ "$VERIFY_CHECKSUM" = "1" ]; then 113 | checkHash 114 | fi 115 | 116 | chmod +x $targetFile 117 | 118 | echo "Download complete." 119 | 120 | if [ ! -w "$BINLOCATION" ]; then 121 | 122 | echo 123 | echo "============================================================" 124 | echo " The script was run as a user who is unable to write" 125 | echo " to $BINLOCATION. To complete the installation the" 126 | echo " following commands may need to be run manually." 127 | echo "============================================================" 128 | echo 129 | echo " sudo cp $REPO$suffix $BINLOCATION/$REPO" 130 | 131 | if [ -n "$ALIAS_NAME" ]; then 132 | echo " sudo ln -sf $BINLOCATION/$REPO $BINLOCATION/$ALIAS_NAME" 133 | fi 134 | 135 | echo 136 | 137 | else 138 | 139 | echo 140 | echo "Running with sufficient permissions to attempt to move $REPO to $BINLOCATION" 141 | 142 | if [ ! -w "$BINLOCATION/$REPO" ] && [ -f "$BINLOCATION/$REPO" ]; then 143 | 144 | echo 145 | echo "================================================================" 146 | echo " $BINLOCATION/$REPO already exists and is not writeable" 147 | echo " by the current user. Please adjust the binary ownership" 148 | echo " or run sh/bash with sudo." 149 | echo "================================================================" 150 | echo 151 | exit 1 152 | 153 | fi 154 | 155 | mv $targetFile $BINLOCATION/$REPO 156 | 157 | if [ "$?" = "0" ]; then 158 | echo "New version of $REPO installed to $BINLOCATION" 159 | fi 160 | 161 | if [ -e $targetFile ]; then 162 | rm $targetFile 163 | fi 164 | 165 | if [ -n "$ALIAS_NAME" ]; then 166 | if [ ! -L $BINLOCATION/$ALIAS_NAME ]; then 167 | ln -s $BINLOCATION/$REPO $BINLOCATION/$ALIAS_NAME 168 | echo "Creating alias '$ALIAS_NAME' for '$REPO'." 169 | fi 170 | fi 171 | 172 | ${SUCCESS_CMD} 173 | fi 174 | fi 175 | } 176 | 177 | hasCli 178 | getPackage 179 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/openfaas/ofc-bootstrap 2 | 3 | go 1.13 4 | 5 | require ( 6 | code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 // indirect 7 | github.com/alexellis/arkade v0.0.0-20201213184027-cc231f9508b1 8 | github.com/alexellis/derek v0.0.0-20201203223145-52084a5968ea // indirect 9 | github.com/alexellis/go-execute v0.0.0-20201205082949-69a2cde04f4f 10 | github.com/bitnami-labs/sealed-secrets v0.13.1 // indirect 11 | github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 // indirect 12 | github.com/imdario/mergo v0.3.11 13 | github.com/inlets/inletsctl v0.0.0-20200211123457-caff14436308 14 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a 15 | github.com/minio/minio-go v6.0.14+incompatible // indirect 16 | github.com/moby/buildkit v0.8.1 // indirect 17 | github.com/morikuni/aec v1.0.0 18 | github.com/onsi/ginkgo v1.14.2 // indirect 19 | github.com/onsi/gomega v1.10.4 // indirect 20 | github.com/openfaas/faas-cli v0.0.0-20201211213129-87c59955dd17 // indirect 21 | github.com/openfaas/openfaas-cloud/edge-auth v0.0.0-20201214095559-b4ad89b94ed9 // indirect 22 | github.com/openfaas/openfaas-cloud/sdk v0.0.0-20201214095559-b4ad89b94ed9 // indirect 23 | github.com/pkg/errors v0.9.1 24 | github.com/sethvargo/go-password v0.1.3 25 | github.com/spf13/cobra v1.1.1 26 | gopkg.in/yaml.v2 v2.3.0 27 | k8s.io/apimachinery v0.20.0 // indirect 28 | k8s.io/client-go v11.0.0+incompatible // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /hack/hashgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | for f in bin/ofc*; do shasum -a 256 $f > $f.sha256; done 4 | -------------------------------------------------------------------------------- /hack/install-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | curl -sLSf https://get.docker.com | sudo sh 6 | 7 | echo "export GOPATH=\$HOME/go" | tee -a ~/.bash_profile 8 | echo "export PATH=\$GOPATH/bin:\$PATH:/usr/local/go/bin/" | tee -a ~/.bash_profile 9 | 10 | curl -sLSf https://dl.get-arkade.dev | sudo sh 11 | 12 | arkade get kind 13 | sudo mv $HOME/.arkade/bin/kind /usr/local/bin/ 14 | 15 | ./bin/ofc-bootstrap version 16 | -------------------------------------------------------------------------------- /hack/integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Create a KinD cluster 6 | kind create cluster 7 | 8 | # Fake the secrets from init.yaml 9 | mkdir -p ~/Downloads 10 | touch ~/Downloads/secret-access-key 11 | touch ~/Downloads/private-key.pem 12 | touch ~/Downloads/do-access-token 13 | 14 | # Run end to end 15 | 16 | ./bin/ofc-bootstrap registry-login --username fake --password also-fake 17 | ./bin/ofc-bootstrap apply --file example.init.yaml 18 | 19 | kubectl rollout status -n openfaas deploy/edge-router 20 | kubectl rollout status -n openfaas deploy/of-builder 21 | kubectl rollout status -n openfaas deploy/gateway 22 | 23 | kubectl rollout status -n openfaas-fn deploy/system-github-event 24 | kubectl rollout status -n openfaas-fn deploy/git-tar 25 | kubectl rollout status -n openfaas-fn deploy/list-functions 26 | kubectl rollout status -n openfaas-fn deploy/system-dashboard 27 | 28 | kubectl get deploy -n kube-system 29 | kubectl get deploy -n openfaas 30 | kubectl get deploy -n openfaas-fn 31 | 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/openfaas/ofc-bootstrap/cmd" 7 | 8 | "github.com/openfaas/ofc-bootstrap/version" 9 | ) 10 | 11 | func main() { 12 | 13 | if err := cmd.Execute(version.Version, version.GitCommit); err != nil { 14 | // fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 15 | os.Exit(1) 16 | } 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /pkg/github/handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "io/ioutil" 9 | "net/http" 10 | "path" 11 | ) 12 | 13 | type CodeReq struct { 14 | Code string `json:"code"` 15 | } 16 | 17 | type AppTemplate struct { 18 | AppID string 19 | AppURL string 20 | AppName string 21 | PEM string 22 | WebhookSecret string 23 | Response string 24 | } 25 | 26 | type AppResult struct { 27 | ID int `json:"id"` 28 | PEM string `json:"pem"` 29 | URL string `json:"html_url"` 30 | Name string `json:"name"` 31 | WebhookSecret string `json:"webhook_secret"` 32 | } 33 | 34 | func MakeHandler(inputMap map[string]string, resCh chan AppResult) func(w http.ResponseWriter, r *http.Request) { 35 | return func(w http.ResponseWriter, r *http.Request) { 36 | if r.Body != nil { 37 | defer r.Body.Close() 38 | } 39 | 40 | if r.URL.Path == "/" || r.URL.Path == "" { 41 | 42 | var outBuffer bytes.Buffer 43 | tmpl, err := template.ParseFiles(path.Join("./templates/github", "index.html")) 44 | err = tmpl.Execute(&outBuffer, &inputMap) 45 | if err != nil { 46 | http.Error(w, err.Error(), http.StatusInternalServerError) 47 | return 48 | } 49 | 50 | w.Header().Set("Content-Type", "text/html") 51 | w.WriteHeader(http.StatusOK) 52 | w.Write(outBuffer.Bytes()) 53 | return 54 | } 55 | 56 | if r.URL.Path == "/callback" { 57 | code := r.URL.Query().Get("code") 58 | 59 | req, _ := http.NewRequest(http.MethodPost, 60 | fmt.Sprintf("https://api.github.com/app-manifests/%s/conversions", code), nil) 61 | 62 | req.Header.Add("Accept", "application/vnd.github.fury-preview+json") 63 | res, err := http.DefaultClient.Do(req) 64 | 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusInternalServerError) 67 | return 68 | } 69 | 70 | if res.Body != nil { 71 | defer res.Body.Close() 72 | result, _ := ioutil.ReadAll(res.Body) 73 | 74 | appRes := AppResult{} 75 | 76 | err := json.Unmarshal(result, &appRes) 77 | 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusInternalServerError) 80 | return 81 | } 82 | 83 | w.Header().Set("Content-Type", "text/html") 84 | w.WriteHeader(http.StatusOK) 85 | w.Write([]byte(fmt.Sprintf("Thank you for creating your GitHub App: %s", appRes.Name))) 86 | 87 | resCh <- appRes 88 | close(resCh) 89 | 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/ingress/ingress.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | execute "github.com/alexellis/go-execute/pkg/v1" 12 | "github.com/openfaas/ofc-bootstrap/pkg/types" 13 | ) 14 | 15 | type IngressTemplate struct { 16 | RootDomain string 17 | TLS bool 18 | IssuerType string 19 | } 20 | 21 | // Apply templates and applies any ingress records required 22 | // for the OpenFaaS Cloud ingress configuration 23 | func Apply(plan types.Plan) error { 24 | 25 | if err := apply("ingress-wildcard.yml", "ingress-wildcard", IngressTemplate{ 26 | RootDomain: plan.RootDomain, 27 | TLS: plan.TLS, 28 | IssuerType: plan.TLSConfig.IssuerType, 29 | }); err != nil { 30 | return err 31 | } 32 | 33 | if err := apply("ingress-auth.yml", "ingress-auth", IngressTemplate{ 34 | RootDomain: plan.RootDomain, 35 | TLS: plan.TLS, 36 | IssuerType: plan.TLSConfig.IssuerType, 37 | }); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func apply(source string, name string, ingress IngressTemplate) error { 45 | 46 | generatedData, err := applyTemplate("templates/k8s/"+source, ingress) 47 | if err != nil { 48 | return fmt.Errorf("unable to read template %s (%s), error: %q", name, "templates/k8s/"+source, err) 49 | } 50 | 51 | tempFilePath := "tmp/generated-ingress-" + name + ".yaml" 52 | file, err := os.Create(tempFilePath) 53 | if err != nil { 54 | return err 55 | } 56 | defer file.Close() 57 | 58 | _, err = file.Write(generatedData) 59 | file.Close() 60 | 61 | if err != nil { 62 | return err 63 | } 64 | 65 | execTask := execute.ExecTask{ 66 | Command: "kubectl", 67 | Args: []string{"apply", "-f", tempFilePath}, 68 | Shell: false, 69 | StreamStdio: false, 70 | } 71 | 72 | execRes, err := execTask.Execute() 73 | if err != nil { 74 | return err 75 | } 76 | 77 | log.Println(execRes.Stdout, execRes.Stderr) 78 | 79 | return nil 80 | } 81 | 82 | func applyTemplate(templateFileName string, templateValues IngressTemplate) ([]byte, error) { 83 | data, err := ioutil.ReadFile(templateFileName) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | t := template.Must(template.New(templateFileName).Parse(string(data))) 89 | buffer := new(bytes.Buffer) 90 | if err := t.Execute(buffer, templateValues); err != nil { 91 | return []byte{}, err 92 | } 93 | 94 | return buffer.Bytes(), nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/ingress/ingress_test.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func Test_applyTemplateWithTLS(t *testing.T) { 9 | templateValues := IngressTemplate{ 10 | RootDomain: "test.com", 11 | TLS: true, 12 | } 13 | 14 | templateFileName := "../../templates/k8s/ingress-auth.yml" 15 | 16 | generatedValue, err := applyTemplate(templateFileName, templateValues) 17 | want := "tls" 18 | 19 | if err != nil { 20 | t.Errorf("expected no error generating template, but got %s", err.Error()) 21 | t.Fail() 22 | return 23 | } 24 | 25 | if strings.Contains(string(generatedValue), want) == false { 26 | t.Errorf("want generated value to contain: %q, generated was: %q", want, string(generatedValue)) 27 | t.Fail() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/openfaas/ofc-bootstrap/pkg/types" 10 | ) 11 | 12 | type gitlabConfig struct { 13 | GitLabInstance string `yaml:"gitlab_instance,omitempty"` 14 | CustomersSecretPath string 15 | } 16 | 17 | type gatewayConfig struct { 18 | Registry string 19 | RootDomain string 20 | CustomersURL string 21 | Scheme string 22 | S3 types.S3 23 | CustomTemplates string 24 | EnableDockerfileLang bool 25 | BuildBranch string 26 | CustomersSecretPath string 27 | } 28 | 29 | type authConfig struct { 30 | RootDomain string 31 | ClientId string 32 | CustomersURL string 33 | Scheme string 34 | OAuthProvider string 35 | OAuthProviderBaseURL string 36 | OFCustomersSecretPath string 37 | TLSEnabled bool 38 | } 39 | 40 | type builderConfig struct { 41 | ECR bool 42 | } 43 | 44 | type stackConfig struct { 45 | GitHub bool 46 | CustomersSecretPath string 47 | } 48 | 49 | type awsConfig struct { 50 | ECRRegion string 51 | } 52 | 53 | type dashboardConfig struct { 54 | RootDomain string 55 | Scheme string 56 | GitHubAppUrl string 57 | GitLabInstance string 58 | } 59 | 60 | // Apply creates `templates/gateway_config.yml` to be referenced by stack.yml 61 | func Apply(plan types.Plan) error { 62 | scheme := "http" 63 | if plan.TLS { 64 | scheme += "s" 65 | } 66 | 67 | customersSecretPath := "" 68 | 69 | if plan.CustomersSecret { 70 | customersSecretPath = "/var/openfaas/secrets/customers" 71 | } 72 | 73 | if gwConfigErr := generateTemplate("gateway_config", plan, gatewayConfig{ 74 | Registry: plan.Registry, 75 | RootDomain: plan.RootDomain, 76 | CustomersURL: plan.CustomersURL, 77 | Scheme: scheme, 78 | S3: plan.S3, 79 | CustomTemplates: plan.Deployment.FormatCustomTemplates(), 80 | EnableDockerfileLang: plan.EnableDockerfileLang, 81 | BuildBranch: plan.BuildBranch, 82 | }); gwConfigErr != nil { 83 | return gwConfigErr 84 | } 85 | 86 | if githubConfigErr := generateTemplate("github", plan, types.Github{ 87 | AppID: plan.Github.AppID, 88 | PrivateKeyFile: plan.Github.PrivateKeyFile, 89 | }); githubConfigErr != nil { 90 | return githubConfigErr 91 | } 92 | 93 | if slackConfigErr := generateTemplate("slack", plan, types.Slack{ 94 | URL: plan.Slack.URL, 95 | }); slackConfigErr != nil { 96 | return slackConfigErr 97 | } 98 | 99 | if plan.SCM == "gitlab" { 100 | if gitlabConfigErr := generateTemplate("gitlab", plan, gitlabConfig{ 101 | GitLabInstance: plan.Gitlab.GitLabInstance, 102 | CustomersSecretPath: customersSecretPath, 103 | }); gitlabConfigErr != nil { 104 | return gitlabConfigErr 105 | } 106 | } 107 | 108 | var gitHubAppUrl, gitLabInstance string 109 | if plan.SCM == types.GitHubSCM { 110 | gitHubAppUrl = plan.Github.PublicLink 111 | } else if plan.SCM == types.GitLabSCM { 112 | gitLabInstance = plan.Gitlab.GitLabInstance 113 | } 114 | dashboardConfigErr := generateTemplate("dashboard_config", plan, dashboardConfig{ 115 | RootDomain: plan.RootDomain, 116 | Scheme: scheme, 117 | GitHubAppUrl: gitHubAppUrl, 118 | GitLabInstance: gitLabInstance, 119 | }) 120 | if dashboardConfigErr != nil { 121 | return dashboardConfigErr 122 | } 123 | 124 | if plan.EnableOAuth { 125 | ofCustomersSecretPath := "" 126 | if plan.CustomersSecret { 127 | ofCustomersSecretPath = "/var/secrets/of-customers/of-customers" 128 | } 129 | 130 | if ofAuthDepErr := generateTemplate("edge-auth-dep", plan, authConfig{ 131 | RootDomain: plan.RootDomain, 132 | ClientId: plan.OAuth.ClientId, 133 | CustomersURL: plan.CustomersURL, 134 | Scheme: scheme, 135 | OAuthProvider: plan.SCM, 136 | OAuthProviderBaseURL: plan.OAuth.OAuthProviderBaseURL, 137 | OFCustomersSecretPath: ofCustomersSecretPath, 138 | TLSEnabled: plan.TLS, 139 | }); ofAuthDepErr != nil { 140 | return ofAuthDepErr 141 | } 142 | } 143 | 144 | isGitHub := plan.SCM == "github" 145 | if stackErr := generateTemplate("stack", plan, stackConfig{ 146 | GitHub: isGitHub, 147 | CustomersSecretPath: customersSecretPath, 148 | }); stackErr != nil { 149 | return stackErr 150 | } 151 | 152 | if builderErr := generateTemplate("of-builder-dep", plan, builderConfig{ 153 | ECR: plan.EnableECR, 154 | }); builderErr != nil { 155 | return builderErr 156 | } 157 | 158 | if ecrErr := generateTemplate("aws", plan, awsConfig{ 159 | ECRRegion: plan.ECRConfig.ECRRegion, 160 | }); ecrErr != nil { 161 | return ecrErr 162 | } 163 | 164 | return nil 165 | } 166 | 167 | func generateTemplate(fileName string, plan types.Plan, templateType interface{}) error { 168 | 169 | generatedData, err := applyTemplate("templates/"+fileName+".yml", templateType) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | tempFilePath := "tmp/generated-" + fileName + ".yml" 175 | file, fileErr := os.Create(tempFilePath) 176 | if fileErr != nil { 177 | return fileErr 178 | } 179 | defer file.Close() 180 | 181 | _, writeErr := file.Write(generatedData) 182 | file.Close() 183 | 184 | if writeErr != nil { 185 | return writeErr 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func applyTemplate(templateFileName string, templateType interface{}) ([]byte, error) { 192 | data, err := ioutil.ReadFile(templateFileName) 193 | if err != nil { 194 | return nil, err 195 | } 196 | t := template.Must(template.New(templateFileName).Parse(string(data))) 197 | 198 | buffer := new(bytes.Buffer) 199 | 200 | executeErr := t.Execute(buffer, templateType) 201 | 202 | return buffer.Bytes(), executeErr 203 | } 204 | -------------------------------------------------------------------------------- /pkg/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func Test_applyTemplateWithAuth(t *testing.T) { 9 | 10 | clientID := "test_oauth_app_client_id" 11 | customersURL := "https://raw.githubusercontent.com/test/path/CUSTOMERS" 12 | 13 | templateValues := authConfig{ 14 | ClientId: clientID, 15 | CustomersURL: customersURL, 16 | Scheme: "http", 17 | } 18 | 19 | templateFileName := "../../templates/edge-auth-dep.yml" 20 | 21 | generatedValue, err := applyTemplate(templateFileName, templateValues) 22 | 23 | if err != nil { 24 | t.Errorf("expected no error generating template, but got %s", err.Error()) 25 | t.Fail() 26 | return 27 | } 28 | 29 | values := []string{clientID, customersURL} 30 | for _, want := range values { 31 | if strings.Contains(string(generatedValue), want) == false { 32 | t.Errorf("want generated value to contain: %q, generated was: %q", want, string(generatedValue)) 33 | t.Fail() 34 | } 35 | } 36 | } 37 | 38 | func Test_gitlabTemplates(t *testing.T) { 39 | gitLabInstance := "https://gitlab.test.o6s.io/" 40 | 41 | gitlabTemplateFileName := "../../templates/gitlab.yml" 42 | 43 | generatedValue, err := applyTemplate(gitlabTemplateFileName, gitlabConfig{ 44 | GitLabInstance: gitLabInstance, 45 | CustomersSecretPath: "", 46 | }) 47 | 48 | if err != nil { 49 | t.Errorf("expected no error generating template, but got %s", err.Error()) 50 | t.Fail() 51 | return 52 | } 53 | 54 | want := gitLabInstance 55 | if strings.Contains(string(generatedValue), want) == false { 56 | t.Errorf("want generated value to contain: %q, generated was: %q", want, string(generatedValue)) 57 | t.Fail() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/tls/.gitignore: -------------------------------------------------------------------------------- 1 | *.yaml 2 | -------------------------------------------------------------------------------- /pkg/tls/issuer_test.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func Test_DigitalOcean_Issuer(t *testing.T) { 11 | tlsTemplate := TLSTemplate{ 12 | Email: "sales@openfaas.com", 13 | IssuerType: "ClusterIssuer", 14 | DNSService: "digitalocean", 15 | } 16 | 17 | templatePath := "../../templates/k8s/tls/issuer-prod.yml" 18 | templateData, err := ioutil.ReadFile(templatePath) 19 | if err != nil { 20 | t.Error(err) 21 | return 22 | } 23 | 24 | templateRes := template.Must(template.New("prod-issuer").Parse(string(templateData))) 25 | buf := bytes.Buffer{} 26 | 27 | templateRes.Execute(&buf, &tlsTemplate) 28 | 29 | wantTemplate := `apiVersion: cert-manager.io/v1alpha2 30 | kind: ClusterIssuer 31 | metadata: 32 | name: letsencrypt-prod 33 | namespace: openfaas 34 | spec: 35 | acme: 36 | email: "sales@openfaas.com" 37 | server: https://acme-v02.api.letsencrypt.org/directory 38 | privateKeySecretRef: 39 | name: letsencrypt-prod 40 | solvers: 41 | - dns01: 42 | digitalocean: 43 | 44 | tokenSecretRef: 45 | name: digitalocean-dns 46 | key: access-token 47 | ` 48 | 49 | got := string(buf.Bytes()) 50 | if len(got) == 0 { 51 | t.Errorf("No bytes generated from template") 52 | t.Fail() 53 | } 54 | 55 | if debugYAML { 56 | ioutil.WriteFile("want-"+tlsTemplate.DNSService+".yaml", []byte(wantTemplate), 0700) 57 | ioutil.WriteFile("got-"+tlsTemplate.DNSService+".yaml", []byte(got), 0700) 58 | } 59 | 60 | if got != wantTemplate { 61 | t.Errorf("Want\n`%q`\n, but got\n`%q`", wantTemplate, got) 62 | } 63 | 64 | } 65 | 66 | func Test_Route53_Issuer(t *testing.T) { 67 | tlsTemplate := TLSTemplate{ 68 | Email: "sales@openfaas.com", 69 | IssuerType: "ClusterIssuer", 70 | DNSService: "route53", 71 | Region: "us-east-1", 72 | AccessKeyID: "key-id", 73 | } 74 | 75 | templatePath := "../../templates/k8s/tls/issuer-prod.yml" 76 | templateData, err := ioutil.ReadFile(templatePath) 77 | if err != nil { 78 | t.Error(err) 79 | return 80 | } 81 | 82 | templateRes := template.Must(template.New("prod-issuer").Parse(string(templateData))) 83 | buf := bytes.Buffer{} 84 | templateRes.Execute(&buf, &tlsTemplate) 85 | 86 | wantTemplate := `apiVersion: cert-manager.io/v1alpha2 87 | kind: ClusterIssuer 88 | metadata: 89 | name: letsencrypt-prod 90 | namespace: openfaas 91 | spec: 92 | acme: 93 | email: "sales@openfaas.com" 94 | server: https://acme-v02.api.letsencrypt.org/directory 95 | privateKeySecretRef: 96 | name: letsencrypt-prod 97 | solvers: 98 | - dns01: 99 | route53: 100 | 101 | region: us-east-1 102 | # optional if ambient credentials are available; see ambient credentials documentation 103 | accessKeyID: key-id 104 | secretAccessKeySecretRef: 105 | name: "route53-credentials-secret" 106 | key: secret-access-key 107 | ` 108 | 109 | got := string(buf.Bytes()) 110 | if len(got) == 0 { 111 | t.Errorf("No bytes generated from template") 112 | t.Fail() 113 | } 114 | 115 | if debugYAML { 116 | ioutil.WriteFile("want-"+tlsTemplate.DNSService+".yaml", []byte(wantTemplate), 0700) 117 | ioutil.WriteFile("got-"+tlsTemplate.DNSService+".yaml", []byte(got), 0700) 118 | } 119 | 120 | if got != wantTemplate { 121 | t.Errorf("Want\n`%q`\n, but got\n`%q`", wantTemplate, got) 122 | } 123 | } 124 | 125 | func Test_GoogleCloudDNS_Issuer(t *testing.T) { 126 | tlsTemplate := TLSTemplate{ 127 | Email: "sales@openfaas.com", 128 | IssuerType: "ClusterIssuer", 129 | DNSService: "clouddns", 130 | ProjectID: "project-1", 131 | } 132 | 133 | templatePath := "../../templates/k8s/tls/issuer-prod.yml" 134 | templateData, err := ioutil.ReadFile(templatePath) 135 | if err != nil { 136 | t.Error(err) 137 | return 138 | } 139 | 140 | templateRes := template.Must(template.New("prod-issuer").Parse(string(templateData))) 141 | buf := bytes.Buffer{} 142 | templateRes.Execute(&buf, &tlsTemplate) 143 | 144 | wantTemplate := `apiVersion: cert-manager.io/v1alpha2 145 | kind: ClusterIssuer 146 | metadata: 147 | name: letsencrypt-prod 148 | namespace: openfaas 149 | spec: 150 | acme: 151 | email: "sales@openfaas.com" 152 | server: https://acme-v02.api.letsencrypt.org/directory 153 | privateKeySecretRef: 154 | name: letsencrypt-prod 155 | solvers: 156 | - dns01: 157 | clouddns: 158 | 159 | project: "project-1" 160 | serviceAccountSecretRef: 161 | name: "clouddns-service-account" 162 | key: service-account.json 163 | ` 164 | 165 | got := string(buf.Bytes()) 166 | if len(got) == 0 { 167 | t.Errorf("No bytes generated from template") 168 | t.Fail() 169 | } 170 | 171 | if debugYAML { 172 | ioutil.WriteFile("want-"+tlsTemplate.DNSService+".yaml", []byte(wantTemplate), 0700) 173 | ioutil.WriteFile("got-"+tlsTemplate.DNSService+".yaml", []byte(got), 0700) 174 | } 175 | 176 | if got != wantTemplate { 177 | t.Errorf("Want\n`%q`\n, but got\n`%q`", wantTemplate, got) 178 | } 179 | } 180 | 181 | func Test_CloudFlare_Issuer(t *testing.T) { 182 | tlsTemplate := TLSTemplate{ 183 | Email: "sales@openfaas.com", 184 | IssuerType: "ClusterIssuer", 185 | DNSService: "cloudflare", 186 | } 187 | 188 | templatePath := "../../templates/k8s/tls/issuer-prod.yml" 189 | templateData, err := ioutil.ReadFile(templatePath) 190 | if err != nil { 191 | t.Error(err) 192 | return 193 | } 194 | 195 | templateRes := template.Must(template.New("prod-issuer").Parse(string(templateData))) 196 | buf := bytes.Buffer{} 197 | templateRes.Execute(&buf, &tlsTemplate) 198 | 199 | wantTemplate := `apiVersion: cert-manager.io/v1alpha2 200 | kind: ClusterIssuer 201 | metadata: 202 | name: letsencrypt-prod 203 | namespace: openfaas 204 | spec: 205 | acme: 206 | email: "sales@openfaas.com" 207 | server: https://acme-v02.api.letsencrypt.org/directory 208 | privateKeySecretRef: 209 | name: letsencrypt-prod 210 | solvers: 211 | - dns01: 212 | cloudflare: 213 | 214 | email: sales@openfaas.com 215 | apiKeySecretRef: 216 | name: cloudflare-api-key-secret 217 | key: api-key 218 | ` 219 | 220 | got := string(buf.Bytes()) 221 | if len(got) == 0 { 222 | t.Errorf("No bytes generated from template") 223 | t.Fail() 224 | } 225 | 226 | if debugYAML { 227 | ioutil.WriteFile("want-"+tlsTemplate.DNSService+".yaml", []byte(wantTemplate), 0700) 228 | ioutil.WriteFile("got-"+tlsTemplate.DNSService+".yaml", []byte(got), 0700) 229 | } 230 | 231 | if got != wantTemplate { 232 | t.Errorf("Want\n`%q`\n, but got\n`%q`", wantTemplate, got) 233 | } 234 | } 235 | 236 | var debugYAML bool 237 | -------------------------------------------------------------------------------- /pkg/tls/tls.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "html/template" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | execute "github.com/alexellis/go-execute/pkg/v1" 10 | "github.com/openfaas/ofc-bootstrap/pkg/types" 11 | ) 12 | 13 | // TLSTemplate TLS configuration 14 | type TLSTemplate struct { 15 | RootDomain string 16 | Email string 17 | DNSService string 18 | ProjectID string 19 | IssuerType string 20 | Region string 21 | AccessKeyID string 22 | } 23 | 24 | // Apply executes the plan 25 | func Apply(plan types.Plan) error { 26 | 27 | tlsTemplatesList, _ := listTLSTemplates() 28 | tlsTemplate := TLSTemplate{ 29 | RootDomain: plan.RootDomain, 30 | Email: plan.TLSConfig.Email, 31 | DNSService: plan.TLSConfig.DNSService, 32 | ProjectID: plan.TLSConfig.ProjectID, 33 | IssuerType: plan.TLSConfig.IssuerType, 34 | Region: plan.TLSConfig.Region, 35 | AccessKeyID: plan.TLSConfig.AccessKeyID, 36 | } 37 | 38 | for _, template := range tlsTemplatesList { 39 | tempFilePath, tlsTemplateErr := generateTemplate(template, tlsTemplate) 40 | if tlsTemplateErr != nil { 41 | return tlsTemplateErr 42 | } 43 | 44 | if err := applyTemplate(tempFilePath); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func listTLSTemplates() ([]string, error) { 53 | 54 | return []string{ 55 | "issuer-prod.yml", 56 | "issuer-staging.yml", 57 | "wildcard-domain-cert.yml", 58 | "auth-domain-cert.yml", 59 | }, nil 60 | } 61 | 62 | func generateTemplate(fileName string, tlsTemplate TLSTemplate) (string, error) { 63 | tlsTemplatesPath := "templates/k8s/tls/" 64 | 65 | data, err := ioutil.ReadFile(tlsTemplatesPath + fileName) 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | t := template.Must(template.New(fileName).Parse(string(data))) 71 | tempFilePath := "tmp/generated-tls-" + fileName 72 | file, fileErr := os.Create(tempFilePath) 73 | if fileErr != nil { 74 | return "", fileErr 75 | } 76 | defer file.Close() 77 | 78 | if err := t.Execute(file, tlsTemplate); err != nil { 79 | return "", err 80 | } 81 | 82 | return tempFilePath, nil 83 | } 84 | 85 | func applyTemplate(tempFilePath string) error { 86 | 87 | execTask := execute.ExecTask{ 88 | Command: "kubectl apply -f " + tempFilePath, 89 | Shell: false, 90 | StreamStdio: false, 91 | } 92 | 93 | execRes, err := execTask.Execute() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | log.Println(execRes.Stdout, execRes.Stderr) 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/types/deployment_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFormatCustomTemplates_None(t *testing.T) { 8 | 9 | d := Deployment{ 10 | CustomTemplate: []string{}, 11 | } 12 | 13 | want := "" 14 | got := d.FormatCustomTemplates() 15 | if got != want { 16 | t.Errorf("FormatCustomTemplates want: %s, got %s", want, got) 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestFormatCustomTemplates_Single(t *testing.T) { 22 | 23 | d := Deployment{ 24 | CustomTemplate: []string{"https://w.com/repo1"}, 25 | } 26 | 27 | want := d.CustomTemplate[0] 28 | got := d.FormatCustomTemplates() 29 | if got != want { 30 | t.Errorf("FormatCustomTemplates want: %s, got %s", want, got) 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestFormatCustomTemplates_Two(t *testing.T) { 36 | 37 | d := Deployment{ 38 | CustomTemplate: []string{ 39 | "https://w.com/repo1", 40 | "https://w.com/repo2", 41 | }, 42 | } 43 | 44 | want := d.CustomTemplate[0] + ", " + d.CustomTemplate[1] 45 | got := d.FormatCustomTemplates() 46 | if got != want { 47 | t.Errorf("FormatCustomTemplates want: %s, got %s", want, got) 48 | t.Fail() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/types/merge.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) OpenFaaS Author(s) 2020. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | package types 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/imdario/mergo" 10 | "github.com/jinzhu/copier" 11 | ) 12 | 13 | // MergePlans combines one or more plan with a manual merge for 14 | // the list of secrets. 15 | func MergePlans(plans []Plan) (*Plan, error) { 16 | var err error 17 | masterPlan := &Plan{} 18 | 19 | if len(plans) == 1 { 20 | return &plans[0], nil 21 | } 22 | 23 | if len(plans) == 0 { 24 | return masterPlan, fmt.Errorf("at least one plan is required") 25 | } 26 | 27 | for _, plan := range plans { 28 | mergeErr := mergo.Merge(masterPlan, plan, mergo.WithOverride) 29 | if mergeErr != nil { 30 | return masterPlan, mergeErr 31 | } 32 | } 33 | 34 | patchSecrets(masterPlan, plans) 35 | 36 | return masterPlan, err 37 | } 38 | 39 | func patchSecrets(masterPlan *Plan, plans []Plan) { 40 | masterList := []KeyValueNamespaceTuple{} 41 | 42 | // Read each plan 43 | for _, plan := range plans { 44 | 45 | // Process each secret 46 | for _, v := range plan.Secrets { 47 | 48 | // Apply to master list 49 | index := -1 50 | for i, mv := range masterList { 51 | if mv.Name == v.Name { 52 | index = i 53 | break 54 | } 55 | } 56 | 57 | if index == -1 { 58 | item := KeyValueNamespaceTuple{} 59 | copier.Copy(&item, &v) 60 | masterList = append(masterList, item) 61 | } else { 62 | copier.Copy(&masterList[index], &v) 63 | } 64 | } 65 | } 66 | masterPlan.Secrets = masterList 67 | } 68 | -------------------------------------------------------------------------------- /pkg/types/merge_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) OpenFaaS Author(s) 2020. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | package types 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func Test_mergePlans_Empty(t *testing.T) { 11 | 12 | _, err := MergePlans([]Plan{}) 13 | 14 | if err == nil { 15 | t.Errorf("Expected an error for no plans") 16 | t.Fail() 17 | } 18 | want := "at least one plan is required" 19 | if err.Error() != want { 20 | t.Errorf("no plans error want: %s, got: %s", want, err.Error()) 21 | t.Fail() 22 | } 23 | } 24 | 25 | func Test_mergePlans_OnlyOneItem(t *testing.T) { 26 | 27 | plan1 := Plan{ 28 | OpenFaaSCloudVersion: "master", 29 | } 30 | 31 | planOut, err := MergePlans([]Plan{plan1}) 32 | 33 | if err != nil { 34 | t.Errorf("Got error for a single plan, expected no error: %s", err.Error()) 35 | t.Fail() 36 | } 37 | 38 | if plan1.OpenFaaSCloudVersion != planOut.OpenFaaSCloudVersion { 39 | t.Errorf("OpenFaaSCloudVersion want: %s, but got: %s", plan1.OpenFaaSCloudVersion, planOut.OpenFaaSCloudVersion) 40 | } 41 | } 42 | 43 | func Test_mergePlans_MergeEmptyItemsFromBoth(t *testing.T) { 44 | 45 | plan1 := Plan{ 46 | OpenFaaSCloudVersion: "master", 47 | } 48 | 49 | plan2 := Plan{ 50 | CustomersURL: "https://127.0.0.1:8443/customers", 51 | } 52 | 53 | planOut, err := MergePlans([]Plan{plan1, plan2}) 54 | 55 | if err != nil { 56 | t.Errorf("Got error, expected no error: %s", err.Error()) 57 | t.Fail() 58 | } 59 | 60 | if planOut.OpenFaaSCloudVersion != plan1.OpenFaaSCloudVersion { 61 | t.Errorf("OpenFaaSCloudVersion want: %s, but got: %s", plan1.OpenFaaSCloudVersion, planOut.OpenFaaSCloudVersion) 62 | } 63 | 64 | if planOut.CustomersURL != plan2.CustomersURL { 65 | t.Errorf("CustomersURL want: %s, but got: %s", plan2.CustomersURL, planOut.CustomersURL) 66 | } 67 | } 68 | 69 | func Test_mergePlans_PlanValuesOverwriteAccordingToOrder(t *testing.T) { 70 | 71 | plan1 := Plan{ 72 | OpenFaaSCloudVersion: "0.11.0", 73 | } 74 | 75 | plan2 := Plan{ 76 | OpenFaaSCloudVersion: "0.12.0", 77 | } 78 | 79 | planOut, err := MergePlans([]Plan{plan1, plan2}) 80 | 81 | if err != nil { 82 | t.Errorf("Got error, expected no error: %s", err.Error()) 83 | t.Fail() 84 | } 85 | 86 | wantVer := plan2.OpenFaaSCloudVersion 87 | gotVer := planOut.OpenFaaSCloudVersion 88 | if gotVer != wantVer { 89 | t.Errorf("OpenFaaSCloudVersion want: %s, but got: %s", wantVer, gotVer) 90 | } 91 | } 92 | 93 | func Test_mergePlans_CombineSecretsDifferentNames(t *testing.T) { 94 | 95 | plan1 := Plan{ 96 | Secrets: []KeyValueNamespaceTuple{ 97 | {Name: "one"}, 98 | }, 99 | } 100 | 101 | plan2 := Plan{ 102 | Secrets: []KeyValueNamespaceTuple{ 103 | {Name: "two"}, 104 | }, 105 | } 106 | 107 | planOut, err := MergePlans([]Plan{plan1, plan2}) 108 | 109 | if err != nil { 110 | t.Errorf("Got error, expected no error: %s", err.Error()) 111 | t.Fail() 112 | } 113 | 114 | wantLen := 2 115 | gotLen := len(planOut.Secrets) 116 | if gotLen != wantLen { 117 | t.Errorf("Secrets want length %d, but got: %d", wantLen, gotLen) 118 | } 119 | } 120 | 121 | func Test_mergePlans_CombineSecretsMatchingNames(t *testing.T) { 122 | 123 | plan1 := Plan{ 124 | Secrets: []KeyValueNamespaceTuple{ 125 | {Name: "one", 126 | Namespace: "openfaas-fn", 127 | }, 128 | }, 129 | } 130 | 131 | plan2 := Plan{ 132 | Secrets: []KeyValueNamespaceTuple{ 133 | {Name: "one", 134 | Namespace: "openfaas-stag"}, 135 | }, 136 | } 137 | 138 | planOut, err := MergePlans([]Plan{plan1, plan2}) 139 | 140 | if err != nil { 141 | t.Errorf("Got error, expected no error: %s", err.Error()) 142 | t.Fail() 143 | } 144 | 145 | wantLen := 1 146 | gotLen := len(planOut.Secrets) 147 | if gotLen != wantLen { 148 | t.Errorf("Secrets want length %d, but got: %d", wantLen, gotLen) 149 | } 150 | } 151 | 152 | func Test_mergePlans_CombineSecretsMatchingNamesLiterals(t *testing.T) { 153 | 154 | plan1 := Plan{ 155 | Secrets: []KeyValueNamespaceTuple{ 156 | {Name: "one", 157 | Namespace: "openfaas-fn", 158 | Literals: []KeyValueTuple{ 159 | { 160 | Name: "password", 161 | Value: "", 162 | }, 163 | }, 164 | }, 165 | }, 166 | } 167 | 168 | wantPass := "test1234" 169 | plan2 := Plan{ 170 | Secrets: []KeyValueNamespaceTuple{ 171 | {Name: "one", 172 | Namespace: "openfaas-stag", 173 | Literals: []KeyValueTuple{ 174 | { 175 | Name: "password", 176 | Value: wantPass, 177 | }, 178 | }, 179 | }, 180 | }, 181 | } 182 | 183 | planOut, err := MergePlans([]Plan{plan1, plan2}) 184 | 185 | if err != nil { 186 | t.Errorf("Got error, expected no error: %s", err.Error()) 187 | t.Fail() 188 | } 189 | 190 | wantLen := 1 191 | gotLen := len(planOut.Secrets) 192 | if gotLen != wantLen { 193 | t.Errorf("Secrets want length %d, but got: %d", wantLen, gotLen) 194 | } 195 | 196 | if planOut.Secrets[0].Literals[0].Value != wantPass { 197 | t.Errorf("Secret merged incorrectly, want: %s, got %s", wantPass, planOut.Secrets[0].Literals[0].Value) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /pkg/types/secrets.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | execute "github.com/alexellis/go-execute/pkg/v1" 9 | "github.com/sethvargo/go-password/password" 10 | ) 11 | 12 | func BuildSecretTask(kvn KeyValueNamespaceTuple) execute.ExecTask { 13 | task := execute.ExecTask{ 14 | Command: "kubectl", 15 | Args: []string{"create", "secret", "generic", "-n=" + kvn.Namespace, kvn.Name}, 16 | StreamStdio: false, 17 | } 18 | 19 | if len(kvn.Type) > 0 { 20 | task.Args = append(task.Args, "--type="+kvn.Type) 21 | } 22 | 23 | for _, key := range kvn.Literals { 24 | secretValue := key.Value 25 | if len(secretValue) == 0 { 26 | val, err := generateSecret() 27 | if err != nil { 28 | fmt.Fprintln(os.Stderr, err) 29 | os.Exit(1) 30 | } 31 | secretValue = val 32 | } 33 | task.Args = append(task.Args, fmt.Sprintf("--from-literal=%s=%s", key.Name, secretValue)) 34 | } 35 | 36 | for _, file := range kvn.Files { 37 | filePath := file.ExpandValueFrom() 38 | if len(file.ValueCommand) > 0 { 39 | if _, err := os.Stat(filePath); err != nil { 40 | 41 | valueTask := execute.ExecTask{ 42 | Command: file.ValueCommand, 43 | StreamStdio: false, 44 | } 45 | res, err := valueTask.Execute() 46 | if err != nil { 47 | log.Fatal(fmt.Errorf("error executing value_command: %s", file.ValueCommand)) 48 | } 49 | 50 | if res.ExitCode != 0 { 51 | log.Fatal(fmt.Errorf("error running value_command: %s, stderr: %s", file.ValueCommand, res.Stderr)) 52 | } 53 | } else { 54 | fmt.Printf("%s exists, not running value_command\n", filePath) 55 | } 56 | } 57 | 58 | task.Args = append(task.Args, fmt.Sprintf("--from-file=%s=%s", file.Name, file.ExpandValueFrom())) 59 | 60 | } 61 | 62 | return task 63 | } 64 | 65 | func generateSecret() (string, error) { 66 | pass, err := password.Generate(25, 10, 0, false, true) 67 | if err != nil { 68 | return "", err 69 | } 70 | return pass, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // DefaultFeature filter is for the features which are mandatory 10 | DefaultFeature = "default" 11 | // GitHubFeature filter enables secrets created with the scm_github filter 12 | GitHubFeature = "scm_github" 13 | // GitLabFeature filter is the feature which enables secret creation for GitLab 14 | GitLabFeature = "scm_gitlab" 15 | 16 | // Auth filter enables OAuth secret creation 17 | Auth = "auth" 18 | 19 | // GCPDNS filter enables the creation of secrets for Google Cloud Platform DNS when TLS is enabled 20 | GCPDNS = "gcp_dns01" 21 | // DODNS filter enables the creation of secrets for Digital Ocean DNS when TLS is enabled 22 | DODNS = "do_dns01" 23 | // Route53DNS filter enables the creation of secrets for Amazon Route53 DNS when TLS is enabled 24 | Route53DNS = "route53_dns01" 25 | // CloudflareDNS filter enables the creation of secrets for Cloudflare DNS when TLS is enabled 26 | CloudflareDNS = "cloudflare_dns01" 27 | 28 | // CloudDNS is the dns_service field in yaml file for Google Cloud Platform 29 | CloudDNS = "clouddns" 30 | // DigitalOcean is the dns_service field in yaml file for Digital Ocean 31 | DigitalOcean = "digitalocean" 32 | // Route53 is the dns_service field in yaml file for Amazon 33 | Route53 = "route53" 34 | // Cloudflare for dns_service 35 | Cloudflare = "cloudflare" 36 | 37 | // GitLabSCM repository manager name as displayed in the init.yaml file 38 | GitLabSCM = "gitlab" 39 | // GitHubSCM repository manager name as displayed in the init.yaml file 40 | GitHubSCM = "github" 41 | 42 | // ECRFeature enable ECR 43 | ECRFeature = "ecr" 44 | ) 45 | 46 | type Plan struct { 47 | Features []string `yaml:"features,omitempty"` 48 | Orchestration string `yaml:"orchestration,omitempty"` 49 | Secrets []KeyValueNamespaceTuple `yaml:"secrets,omitempty"` 50 | RootDomain string `yaml:"root_domain,omitempty"` 51 | Registry string `yaml:"registry,omitempty"` 52 | CustomersURL string `yaml:"customers_url,omitempty"` 53 | SCM string `yaml:"scm,omitempty"` 54 | Github Github `yaml:"github,omitempty"` 55 | Gitlab Gitlab `yaml:"gitlab,omitempty"` 56 | TLS bool `yaml:"tls,omitempty"` 57 | OAuth OAuth `yaml:"oauth,omitempty"` 58 | S3 S3 `yaml:"s3,omitempty"` 59 | EnableOAuth bool `yaml:"enable_oauth,omitempty"` 60 | TLSConfig TLSConfig `yaml:"tls_config,omitempty"` 61 | Slack Slack `yaml:"slack,omitempty"` 62 | Ingress string `yaml:"ingress,omitempty"` 63 | Deployment Deployment `yaml:"deployment,omitempty"` 64 | EnableDockerfileLang bool `yaml:"enable_dockerfile_lang,omitempty"` 65 | ScaleToZero bool `yaml:"scale_to_zero,omitempty"` 66 | OpenFaaSCloudVersion string `yaml:"openfaas_cloud_version,omitempty"` 67 | NetworkPolicies bool `yaml:"network_policies,omitempty"` 68 | BuildBranch string `yaml:"build_branch,omitempty"` 69 | EnableECR bool `yaml:"enable_ecr,omitempty"` 70 | ECRConfig ECRConfig `yaml:"ecr_config,omitempty"` 71 | CustomersSecret bool `yaml:"customers_secret,omitempty"` 72 | IngressOperator bool `yaml:"ingress_operator,omitempty"` 73 | OpenFaaSOperator bool `yaml:"openfaas_operator,omitempty"` 74 | } 75 | 76 | // Deployment is the deployment section of YAML concerning 77 | // functions as deployed 78 | type Deployment struct { 79 | CustomTemplate []string `yaml:"custom_templates,omitempty"` 80 | } 81 | 82 | // FormatCustomTemplates are formatted in a CSV format with a space after each comma 83 | func (d Deployment) FormatCustomTemplates() string { 84 | val := "" 85 | for _, templateURL := range d.CustomTemplate { 86 | val = val + templateURL + ", " 87 | } 88 | 89 | return strings.TrimRight(val, " ,") 90 | } 91 | 92 | type KeyValueTuple struct { 93 | Name string `yaml:"name,omitempty"` 94 | Value string `yaml:"value,omitempty"` 95 | } 96 | 97 | type FileSecret struct { 98 | Name string `yaml:"name,omitempty"` 99 | ValueFrom string `yaml:"value_from,omitempty"` 100 | 101 | // ValueCommand is a command to execute to generate 102 | // a secret file specified in ValueFrom 103 | ValueCommand string `yaml:"value_command,omitempty"` 104 | } 105 | 106 | // ExpandValueFrom expands ~ to the home directory of the current user 107 | // kept in the HOME env-var. 108 | func (fs FileSecret) ExpandValueFrom() string { 109 | value := fs.ValueFrom 110 | value = strings.Replace(value, "~", os.Getenv("HOME"), -1) 111 | return value 112 | } 113 | 114 | type KeyValueNamespaceTuple struct { 115 | Name string `yaml:"name,omitempty"` 116 | Literals []KeyValueTuple `yaml:"literals,omitempty"` 117 | Namespace string `yaml:"namespace,omitempty"` 118 | Files []FileSecret `yaml:"files,omitempty"` 119 | Type string `yaml:"type,omitempty"` 120 | Filters []string `yaml:"filters,omitempty"` 121 | } 122 | 123 | type Github struct { 124 | AppID string `yaml:"app_id,omitempty"` 125 | PrivateKeyFile string `yaml:"private_key_filename,omitempty"` 126 | PublicLink string `yaml:"public_link,omitempty"` 127 | } 128 | 129 | type Gitlab struct { 130 | GitLabInstance string `yaml:"gitlab_instance,omitempty"` 131 | } 132 | 133 | type Slack struct { 134 | URL string `yaml:"url,omitempty"` 135 | } 136 | 137 | type OAuth struct { 138 | ClientId string `yaml:"client_id,omitempty"` 139 | OAuthProviderBaseURL string `yaml:"oauth_provider_base_url,omitempty"` 140 | } 141 | 142 | type S3 struct { 143 | Url string `yaml:"s3_url,omitempty"` 144 | Region string `yaml:"s3_region,omitempty"` 145 | TLS bool `yaml:"s3_tls,omitempty"` 146 | Bucket string `yaml:"s3_bucket,omitempty"` 147 | } 148 | 149 | type TLSConfig struct { 150 | Email string `yaml:"email,omitempty"` 151 | DNSService string `yaml:"dns_service,omitempty"` 152 | ProjectID string `yaml:"project_id,omitempty"` 153 | IssuerType string `yaml:"issuer_type,omitempty"` 154 | Region string `yaml:"region,omitempty"` 155 | AccessKeyID string `yaml:"access_key_id,omitempty"` 156 | } 157 | 158 | type ECRConfig struct { 159 | ECRRegion string `yaml:"ecr_region,omitempty"` 160 | } 161 | -------------------------------------------------------------------------------- /pkg/types/types_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestFileSecret_ExpandValueFrom(t *testing.T) { 9 | os.Setenv("HOME", "/home/user") 10 | fs := FileSecret{ 11 | ValueFrom: "~/.docker/config.json", 12 | } 13 | want := "/home/user/.docker/config.json" 14 | got := fs.ExpandValueFrom() 15 | if got != want { 16 | t.Errorf("error want: %s, got %s", want, got) 17 | t.Fail() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/validators/validators.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | type AuthConfig struct { 12 | Auth string `json:"auth,omitempty"` 13 | } 14 | 15 | type DockerConfigJson struct { 16 | AuthConfigs map[string]AuthConfig `json:"auths"` 17 | } 18 | 19 | func ValidateRegistryAuth(registryEndpoint string, configFileBytes []byte) error { 20 | 21 | registryData, err := unmarshalRegistryConfig(configFileBytes) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if err := validate(registryData, registryEndpoint); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func unmarshalRegistryConfig(data []byte) (*DockerConfigJson, error) { 34 | var registryConfig DockerConfigJson 35 | 36 | err := json.Unmarshal(data, ®istryConfig) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return ®istryConfig, nil 41 | } 42 | 43 | func validate(registryData *DockerConfigJson, endpoint string) error { 44 | var fileEndpoint string 45 | if strings.HasPrefix(endpoint, "docker.io") { 46 | fileEndpoint = "https://index.docker.io/v1/" 47 | } else { 48 | fileEndpoint = endpoint 49 | } 50 | if endpointConfig, ok := registryData.AuthConfigs[fileEndpoint]; ok { 51 | if endpointConfig.Auth != "" { 52 | _, err := base64.StdEncoding.DecodeString(endpointConfig.Auth) 53 | return err 54 | } else { 55 | return errors.New("docker credentials file is not valid (no base64 credentials). Please re-create this file") 56 | } 57 | } 58 | return errors.New(fmt.Sprintf("docker auth file does not contain registry %q that you specified in config. Please use docker login", endpoint)) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/validators/validators_test.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_ValidateRegistryAuthNoCredStore(t *testing.T) { 9 | file := []byte(fmt.Sprintf("{ \"auths\": { \"%s\": {\"auth\": \"%s\"} } } ", 10 | "https://index.docker.io/v1/", 11 | "Zm9vCg==")) 12 | 13 | got := ValidateRegistryAuth("docker.io/some-user", file) 14 | if got != nil { 15 | t.Errorf("error want: %s, got %s", "nil", got) 16 | t.Fail() 17 | } 18 | } 19 | 20 | func Test_ValidateRegistryAuthCredStoreNoAuth(t *testing.T) { 21 | file := []byte(fmt.Sprintf("{ \"auths\": { \"%s\": {} }, \"credsStore\": \"something\" } ", 22 | "https://index.docker.io/v1/")) 23 | got := ValidateRegistryAuth("docker.io/some-user", file) 24 | if got == nil { 25 | t.Errorf("error was nil.") 26 | t.Fail() 27 | } 28 | } 29 | 30 | func Test_ValidateRegistryAuthNoCredStoreNoAuth(t *testing.T) { 31 | file := []byte(fmt.Sprintf("{ \"auths\": { \"%s\": {} } } ", 32 | "index.docker.io/index.html")) 33 | got := ValidateRegistryAuth("docker.io/some-user", file) 34 | if got == nil { 35 | t.Errorf("error was nil.") 36 | t.Fail() 37 | } 38 | } 39 | 40 | func Test_ValidateRegistryAuthNoValidEndpoint(t *testing.T) { 41 | file := []byte(fmt.Sprintf("{ \"auths\": { \"%s\": {} } } ", 42 | "")) 43 | got := ValidateRegistryAuth("docker.io/some-user", file) 44 | if got == nil { 45 | t.Errorf("error was nil.") 46 | t.Fail() 47 | } 48 | } 49 | 50 | func Test_Validate_CorrectDetails(t *testing.T) { 51 | data := createDockerConfigJson("https://index.docker.io/v1/", "Zm9vOmJhcgo=") 52 | 53 | got := validate(data, "docker.io") 54 | if got != nil { 55 | t.Errorf("error want no error, got %q", got) 56 | t.Fail() 57 | } 58 | } 59 | 60 | func Test_Validate_CorrectDetailsNotDockerRegistry(t *testing.T) { 61 | data := createDockerConfigJson("https://my.other.registry/auth/v22", "Zm9vOmJhcgo=") 62 | 63 | got := validate(data, "https://my.other.registry/auth/v22") 64 | if got != nil { 65 | t.Errorf("error want no error, got %q", got) 66 | t.Fail() 67 | } 68 | } 69 | 70 | func Test_Validate_NoEntryForDomain(t *testing.T) { 71 | data := createDockerConfigJson("http://notdocker.io/some-user", "dsonosc") 72 | 73 | got := validate(data, "docker.io") 74 | if got == nil { 75 | t.Errorf("error wanted error, got %q", got) 76 | t.Fail() 77 | } 78 | } 79 | 80 | func Test_Validate_EntryDoesNotContainAuth(t *testing.T) { 81 | data := createDockerConfigJson("https://index.docker.io/v1/", "Zm9vOmJhcgo=") 82 | 83 | got := validate(data, "notdocker.io") 84 | if got == nil { 85 | t.Errorf("error want no error, got %q", got) 86 | t.Fail() 87 | } 88 | } 89 | 90 | func Test_Validate_EntryDoesNotContainAuthNotDocker(t *testing.T) { 91 | data := createDockerConfigJson("https://index.myregistry.io/v1/", "Zm9vOmJhcgo=") 92 | 93 | got := validate(data, "https://index.not.my.registry.io/v1/") 94 | if got == nil { 95 | t.Errorf("error want no error, got %q", got) 96 | t.Fail() 97 | } 98 | } 99 | 100 | func Test_Validate_EntryEmptyAuthString(t *testing.T) { 101 | data := createDockerConfigJson("https://index.docker.io/v1/", "Zm9vOmJhcgo=") 102 | 103 | got := validate(data, "docker.io") 104 | if got != nil { 105 | t.Errorf("error want no error, got %q", got) 106 | t.Fail() 107 | } 108 | } 109 | 110 | func Test_Validate_NonBase64AuthString(t *testing.T) { 111 | data := createDockerConfigJson("https://index.docker.io/v1/", "ds:\\/onosc") 112 | 113 | got := validate(data, "docker.io") 114 | if got == nil { 115 | t.Errorf("want error, got %q", got) 116 | t.Fail() 117 | } 118 | } 119 | 120 | func createDockerConfigJson(endpoint string, authString string) *DockerConfigJson { 121 | 122 | conf := DockerConfigJson{ 123 | AuthConfigs: map[string]AuthConfig{ 124 | endpoint: { 125 | Auth: authString, 126 | }, 127 | }, 128 | } 129 | 130 | return &conf 131 | } 132 | -------------------------------------------------------------------------------- /scripts/clone-cloud-components.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf ./tmp/openfaas-cloud 4 | 5 | git clone https://github.com/openfaas/openfaas-cloud ./tmp/openfaas-cloud 6 | 7 | cd ./tmp/openfaas-cloud 8 | echo "Checking out openfaas/openfaas-cloud@$TAG" 9 | git checkout $TAG 10 | -------------------------------------------------------------------------------- /scripts/create-functions-auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export USER=$(kubectl get secret -n openfaas basic-auth -o jsonpath='{.data.basic-auth-user}'| base64 --decode) 4 | export PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath='{.data.basic-auth-password}'| base64 --decode) 5 | 6 | kubectl create secret generic basic-auth-user \ 7 | --from-literal=basic-auth-user=$USER --namespace openfaas-fn \ 8 | --dry-run=client -o yaml | kubectl apply -f - 9 | 10 | kubectl create secret generic basic-auth-password \ 11 | --from-literal=basic-auth-password=$PASSWORD --namespace openfaas-fn \ 12 | --dry-run=client -o yaml | kubectl apply -f - 13 | -------------------------------------------------------------------------------- /scripts/deploy-cloud-components.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp ./tmp/generated-gateway_config.yml ./tmp/openfaas-cloud/gateway_config.yml 4 | cp ./tmp/generated-github.yml ./tmp/openfaas-cloud/github.yml 5 | cp ./tmp/generated-slack.yml ./tmp/openfaas-cloud/slack.yml 6 | cp ./tmp/generated-dashboard_config.yml ./tmp/openfaas-cloud/dashboard/dashboard_config.yml 7 | cp ./tmp/generated-aws.yml ./tmp/openfaas-cloud/aws.yml 8 | 9 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/of-builder-svc.yml 10 | 11 | # Update builder for any ECR secrets needed 12 | cp ./tmp/generated-of-builder-dep.yml ./tmp/openfaas-cloud/yaml/core/of-builder-dep.yml 13 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/of-builder-dep.yml 14 | 15 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/rbac-import-secrets.yml 16 | 17 | if [ "$ENABLE_OAUTH" = "true" ] ; then 18 | cp ./tmp/generated-edge-auth-dep.yml ./tmp/openfaas-cloud/yaml/core/edge-auth-dep.yml 19 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/edge-auth-dep.yml 20 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/edge-auth-svc.yml 21 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/edge-router-dep.yml 22 | else 23 | # Disable auth service by pointing the router at the echo function: 24 | sed s/edge-auth.openfaas/echo.openfaas-fn/g ./tmp/openfaas-cloud/yaml/core/edge-router-dep.yml | kubectl apply -f - 25 | fi 26 | 27 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/edge-router-svc.yml 28 | 29 | kubectl apply -f ./tmp/openfaas-cloud/yaml/core/edge-auth-svc.yml 30 | 31 | if [ "$ENABLE_NETWORK_POLICIES" = "true" ] ; then 32 | kubectl apply -f ./tmp/openfaas-cloud/yaml/network-policy/ 33 | fi 34 | 35 | cd ./tmp/openfaas-cloud 36 | 37 | echo "Creating payload-secret in openfaas-fn" 38 | 39 | export PAYLOAD_SECRET=$(kubectl get secret -n openfaas payload-secret -o jsonpath='{.data.payload-secret}'| base64 --decode) 40 | 41 | kubectl create secret generic payload-secret -n openfaas-fn --from-literal payload-secret="$PAYLOAD_SECRET" \ 42 | --dry-run=client -o yaml | kubectl apply -f - 43 | 44 | export ADMIN_PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath='{.data.basic-auth-password}'| base64 --decode) 45 | 46 | faas-cli template pull 47 | 48 | kubectl port-forward svc/gateway -n openfaas 31111:8080 & 49 | sleep 2 50 | 51 | for i in {1..60}; 52 | do 53 | echo "Checking if OpenFaaS GW is up." 54 | 55 | curl -if 127.0.0.1:31111 56 | if [ $? == 0 ]; 57 | then 58 | break 59 | fi 60 | 61 | sleep 1 62 | done 63 | 64 | 65 | export OPENFAAS_URL=http://127.0.0.1:31111 66 | echo -n $ADMIN_PASSWORD | faas-cli login --username admin --password-stdin 67 | 68 | cp ../generated-stack.yml ./stack.yml 69 | 70 | faas-cli deploy 71 | 72 | if [ "$GITLAB" = "true" ] ; then 73 | cp ../generated-gitlab.yml ./gitlab.yml 74 | echo "Deploying gitlab functions..." 75 | faas-cli deploy -f ./gitlab.yml 76 | fi 77 | 78 | if [ "$ENABLE_AWS_ECR" = "true" ] ; then 79 | echo "Deploying AWS ECR functions (register-image)..." 80 | faas-cli deploy -f ./aws.yml 81 | fi 82 | 83 | kubectl create secret generic sealedsecrets-public-key -n openfaas-fn --from-file=../pub-cert.pem \ 84 | --dry-run=client -o yaml | kubectl apply -f - 85 | 86 | TAG=0.14.6 faas-cli deploy -f ./dashboard/stack.yml 87 | 88 | sleep 2 89 | 90 | # Close the kubectl port-forward 91 | kill %1 92 | -------------------------------------------------------------------------------- /scripts/export-sealed-secret-pubcert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | kubeseal --version 3 | kubeseal --fetch-cert --controller-name=sealed-secrets > tmp/pub-cert.pem \ 4 | && cat tmp/pub-cert.pem 5 | -------------------------------------------------------------------------------- /scripts/get-cert-manager.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cert-manager is ready for CRD objects when this condition is "True" 4 | INJECTOR_READY=$(kubectl get deploy/cert-manager-cainjector -n cert-manager -o jsonpath="{.status.conditions[0].status}") 5 | CERT_MANAGER_READY=$(kubectl get deploy/cert-manager -n cert-manager -o jsonpath="{.status.conditions[0].status}") 6 | CERT_MANAGER_WEBHOOK_READY=$(kubectl get deploy/cert-manager-webhook -n cert-manager -o jsonpath="{.status.conditions[0].status}") 7 | 8 | if [ "$CERT_MANAGER_READY" = "True" ] 9 | then 10 | if [ "$INJECTOR_READY" = "True" ] 11 | then 12 | if [ "$CERT_MANAGER_WEBHOOK_READY" = "True" ] 13 | then 14 | echo -n True 15 | fi 16 | fi 17 | fi 18 | -------------------------------------------------------------------------------- /scripts/get-sealedsecretscontroller.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | kubectl get deploy/ofc-sealedsecrets-sealed-secrets -n kube-system --output="jsonpath={.status.availableReplicas}" 4 | -------------------------------------------------------------------------------- /scripts/patch-fn-serviceaccount.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "registry-pull-secret"}]}' -n openfaas-fn -------------------------------------------------------------------------------- /templates/aws.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: openfaas 3 | gateway: http://127.0.0.1:8080 4 | 5 | functions: 6 | # register-image creates a repo in a hosted/managed registry, which is needed 7 | # for AWS ECR before any image can be pushed. 8 | register-image: 9 | lang: go 10 | handler: ./register-image 11 | image: ghcr.io/${REPO:-openfaas}/ofc-aws-register-image:0.14.6 12 | labels: 13 | openfaas-cloud: "1" 14 | role: openfaas-system 15 | com.openfaas.scale.zero: false 16 | environment: 17 | write_debug: true 18 | read_debug: true 19 | content_type: "application/json" 20 | AWS_DEFAULT_REGION: "{{.ECRRegion}}" 21 | AWS_SHARED_CREDENTIALS_FILE: "/var/openfaas/secrets/credentials" 22 | secrets: 23 | - payload-secret 24 | - aws-ecr-createrepo-credentials 25 | -------------------------------------------------------------------------------- /templates/dashboard_config.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | write_debug: true 3 | # gateway_url: http://gateway:8080/ # when using Swarm 4 | gateway_url: http://gateway.openfaas:8080/ 5 | # base_href: `/function/system-dashboard/` # if not using router 6 | base_href: '/dashboard/' 7 | # public_url: http://laptop-ip:8080/ # use IP of laptop or remote machine, do not use localhost/127.0.0.1 8 | public_url: {{.Scheme}}://system.{{.RootDomain}} 9 | # Comment out if not using public pretty-URL 10 | pretty_url: {{.Scheme}}://user.{{.RootDomain}}/function 11 | # Comment out if not using public pretty-URL 12 | query_pretty_url: 'true' 13 | # Cookie root domain is needed to remove OAuth tokens when using OAuth, it doesnt matter if its set when not using OAuth 14 | cookie_root_domain: '.system.{{.RootDomain}}' 15 | # Public URL for your GitHub app 16 | # see https://github.com/settings/apps/ 17 | github_app_url: {{.GitHubAppUrl}} 18 | # Public URL for your GitLab instance 19 | gitlab_url: {{.GitLabInstance}} 20 | -------------------------------------------------------------------------------- /templates/edge-auth-dep.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: edge-auth 5 | namespace: openfaas 6 | labels: 7 | app: edge-auth 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: edge-auth 13 | template: 14 | metadata: 15 | annotations: 16 | prometheus.io.scrape: "false" 17 | labels: 18 | app: edge-auth 19 | spec: 20 | volumes: 21 | - name: jwt-private-key 22 | secret: 23 | secretName: jwt-private-key 24 | - name: jwt-public-key 25 | secret: 26 | secretName: jwt-public-key 27 | - name: of-client-secret 28 | secret: 29 | secretName: of-client-secret 30 | - name: of-customers 31 | secret: 32 | secretName: of-customers 33 | containers: 34 | - name: edge-auth 35 | image: ghcr.io/openfaas/ofc-edge-auth:0.14.6 36 | imagePullPolicy: Always 37 | livenessProbe: 38 | httpGet: 39 | path: /healthz 40 | port: 8080 41 | initialDelaySeconds: 2 42 | periodSeconds: 10 43 | timeoutSeconds: 2 44 | env: 45 | - name: port 46 | value: "8080" 47 | - name: oauth_client_secret_path 48 | value: "/var/secrets/of-client-secret/of-client-secret" 49 | - name: public_key_path 50 | value: "/var/secrets/public/key.pub" 51 | - name: private_key_path 52 | value: "/var/secrets/private/key" 53 | - name: customers_path 54 | value: "{{.OFCustomersSecretPath}}" 55 | # Update for your configuration: 56 | - name: client_secret # this can also be provided via a secret named of-client-secret 57 | value: "" 58 | - name: client_id 59 | value: "{{.ClientId}}" 60 | - name: oauth_provider_base_url 61 | value: "{{.OAuthProviderBaseURL}}" 62 | - name: oauth_provider 63 | value: "{{.OAuthProvider}}" 64 | # Local test config 65 | # - name: external_redirect_domain 66 | # value: "http://auth.system.gw.io:8081" 67 | # - name: cookie_root_domain 68 | # value: ".system.gw.io" 69 | 70 | # Community cluster config: 71 | - name: external_redirect_domain 72 | value: "{{.Scheme}}://auth.system.{{.RootDomain}}" 73 | - name: cookie_root_domain 74 | value: ".system.{{.RootDomain}}" 75 | # This is a default and can be overridden 76 | - name: customers_url 77 | value: "{{.CustomersURL}}" 78 | - name: write_debug 79 | value: "false" 80 | # Config for setting the cookie to "secure", set this to true for HTTPS only OAuth 81 | - name: secure_cookie 82 | value: "{{.TLSEnabled}}" 83 | 84 | 85 | ports: 86 | - containerPort: 8080 87 | protocol: TCP 88 | volumeMounts: 89 | - name: jwt-private-key 90 | readOnly: true 91 | mountPath: "/var/secrets/private/" 92 | - name: jwt-public-key 93 | readOnly: true 94 | mountPath: "/var/secrets/public" 95 | - name: of-client-secret 96 | readOnly: true 97 | mountPath: "/var/secrets/of-client-secret" 98 | - name: of-customers 99 | readOnly: true 100 | mountPath: "/var/secrets/of-customers" 101 | -------------------------------------------------------------------------------- /templates/gateway_config.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | validate_hmac: "1" 3 | # URLs 4 | gateway_url: http://gateway.openfaas:8080/ 5 | gateway_public_url: {{.Scheme}}://cloud.{{.RootDomain}}/ 6 | audit_url: http://gateway.openfaas:8080/function/audit-event 7 | # Remove gateway_pretty_url if not using pretty URL 8 | gateway_pretty_url: {{.Scheme}}://user.{{.RootDomain}}/function 9 | # Add your custom templates by adding a coma separated URL, i.e. extend the current value with: 10 | # ", https://github.com/custom/template.git, https://github.com/custom/template2.git" 11 | custom_templates: {{ .CustomTemplates }} 12 | 13 | # Security 14 | customers_url: "{{.CustomersURL}}" 15 | basic_auth: true 16 | secret_mount_path: /var/openfaas/secrets 17 | 18 | # Container builder 19 | repository_url: {{.Registry}} 20 | push_repository_url: {{.Registry}} 21 | builder_url: http://of-builder.openfaas:8080/ 22 | 23 | # Logging 24 | s3_url: {{.S3.Url}} 25 | s3_region: {{.S3.Region}} 26 | s3_tls: {{.S3.TLS}} 27 | s3_bucket: {{.S3.Bucket}} 28 | 29 | # Function policy 30 | readonly_root_filesystem: true 31 | scaling_min_limit: 1 32 | scaling_max_limit: 4 33 | prometheus_host: prometheus.openfaas 34 | prometheus_port: 9090 35 | metrics_window: 60m 36 | 37 | # Dockerfile language support 38 | enable_dockerfile_lang: {{.EnableDockerfileLang}} 39 | 40 | # Set the build branch to be used by ofc 41 | build_branch: {{.BuildBranch}} 42 | 43 | -------------------------------------------------------------------------------- /templates/github.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | github_app_id: "{{.AppID}}" 3 | report_status: "true" 4 | private_key_filename: "{{.PrivateKeyFile}}" -------------------------------------------------------------------------------- /templates/github/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OpenFaaS Cloud App Creation 8 | 9 | 10 |
11 |

Create your OpenFaaS Cloud GitHub App

12 |

Use this page to create your GitHub App, or follow the manual steps

13 |
14 |

Webhook URL i.e. https://system.example.com/github-event
15 | 16 |

17 | 18 |

19 | Preview of your app manifest
20 | 21 |
22 | 23 |

24 |
25 | 26 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /templates/gitlab.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: openfaas 3 | gateway: http://127.0.0.1:8080 4 | 5 | functions: 6 | system-gitlab-event: 7 | lang: go 8 | handler: ./gitlab-event 9 | image: ghcr.io/openfaas/ofc-gitlab-event:0.14.6 10 | labels: 11 | openfaas-cloud: "1" 12 | role: openfaas-system 13 | com.openfaas.scale.zero: false 14 | environment: 15 | content_type: text/plain 16 | validate_customers: true 17 | validate_token: false 18 | write_debug: true 19 | read_debug: true 20 | installation_tag: "openfaas-cloud" 21 | gitlab_instance: "{{.GitLabInstance}}" 22 | customers_path: "{{.CustomersSecretPath}}" 23 | environment_file: 24 | - gateway_config.yml 25 | secrets: 26 | - gitlab-webhook-secret 27 | - payload-secret 28 | - gitlab-api-token 29 | - customers 30 | 31 | gitlab-push: 32 | lang: go 33 | handler: ./gitlab-push 34 | image: ghcr.io/openfaas/ofc-gitlab-push:0.14.6 35 | labels: 36 | openfaas-cloud: "1" 37 | role: openfaas-system 38 | com.openfaas.scale.zero: false 39 | environment: 40 | write_debug: true 41 | read_debug: true 42 | environment_file: 43 | - gateway_config.yml 44 | secrets: 45 | - payload-secret 46 | 47 | gitlab-status: 48 | lang: go 49 | handler: ./gitlab-status 50 | image: ghcr.io/openfaas/ofc-gitlab-status:0.14.6 51 | labels: 52 | openfaas-cloud: "1" 53 | role: openfaas-system 54 | com.openfaas.scale.zero: false 55 | environment: 56 | write_debug: true 57 | read_debug: true 58 | environment_file: 59 | - gateway_config.yml 60 | secrets: 61 | - gitlab-api-token 62 | - payload-secret 63 | 64 | ## Post-deployed with gitlab with `gitlab-api-token` secret 65 | git-tar: 66 | lang: dockerfile 67 | handler: ./git-tar 68 | image: ghcr.io/openfaas/ofc-git-tar:0.14.6 69 | labels: 70 | openfaas-cloud: "1" 71 | role: openfaas-system 72 | com.openfaas.scale.zero: false 73 | environment: 74 | read_timeout: 15m 75 | write_timeout: 15m 76 | write_debug: true 77 | read_debug: true 78 | environment_file: 79 | - gateway_config.yml 80 | - github.yml 81 | secrets: 82 | - payload-secret 83 | - gitlab-api-token 84 | -------------------------------------------------------------------------------- /templates/issue-prod.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-prod 5 | namespace: default 6 | spec: 7 | acme: 8 | server: https://acme-v02.api.letsencrypt.org/directory 9 | email: your-email@gmail.com 10 | privateKeySecretRef: 11 | name: letsencrypt-prod 12 | dns01: 13 | providers: 14 | - name: prod-dns 15 | clouddns: 16 | serviceAccountSecretRef: 17 | name: clouddns-service-account 18 | key: service-account.json 19 | project: proj-name 20 | 21 | -------------------------------------------------------------------------------- /templates/k8s/ingress-auth.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: openfaas-auth-ingress 6 | namespace: openfaas 7 | annotations: 8 | kubernetes.io/ingress.class: "nginx" 9 | nginx.ingress.kubernetes.io/limit-connections: "20" 10 | nginx.ingress.kubernetes.io/limit-rpm: "600" 11 | labels: 12 | app: faas-netesd 13 | spec: 14 | {{ if .TLS }} 15 | tls: 16 | - hosts: 17 | - auth.system.{{.RootDomain}} 18 | secretName: auth-system-{{.RootDomain}}-cert 19 | {{ end }} 20 | rules: 21 | - host: auth.system.{{.RootDomain}} 22 | http: 23 | paths: 24 | - path: / 25 | backend: 26 | serviceName: edge-router 27 | servicePort: 8080 28 | -------------------------------------------------------------------------------- /templates/k8s/ingress-wildcard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: openfaas-ingress 6 | namespace: openfaas 7 | annotations: 8 | kubernetes.io/ingress.class: "nginx" 9 | nginx.ingress.kubernetes.io/limit-connections: "20" 10 | nginx.ingress.kubernetes.io/limit-rpm: "600" 11 | labels: 12 | app: faas-netesd 13 | spec: 14 | {{ if .TLS }} 15 | tls: 16 | - hosts: 17 | - '*.{{.RootDomain}}' 18 | secretName: wildcard-{{.RootDomain}}-cert 19 | {{ end }} 20 | rules: 21 | - host: '*.{{.RootDomain}}' 22 | http: 23 | paths: 24 | - path: / 25 | backend: 26 | serviceName: edge-router 27 | servicePort: 8080 28 | - host: 'gateway.{{.RootDomain}}' 29 | http: 30 | paths: 31 | - path: / 32 | backend: 33 | serviceName: gateway 34 | servicePort: 8080 35 | -------------------------------------------------------------------------------- /templates/k8s/tls/auth-domain-cert.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1alpha2 3 | kind: Certificate 4 | metadata: 5 | name: auth-system-{{.RootDomain}} 6 | namespace: openfaas 7 | spec: 8 | secretName: auth-system-{{.RootDomain}}-cert 9 | issuerRef: 10 | name: letsencrypt-{{.IssuerType}} 11 | kind: ClusterIssuer 12 | commonName: 'auth.system.{{.RootDomain}}' 13 | dnsNames: 14 | - 'auth.system.{{.RootDomain}}' 15 | -------------------------------------------------------------------------------- /templates/k8s/tls/issuer-prod.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-prod 5 | namespace: openfaas 6 | spec: 7 | acme: 8 | email: "{{.Email}}" 9 | server: https://acme-v02.api.letsencrypt.org/directory 10 | privateKeySecretRef: 11 | name: letsencrypt-prod 12 | solvers: 13 | - dns01: 14 | {{.DNSService}}: 15 | {{ if eq .DNSService "clouddns" }} 16 | project: "{{.ProjectID}}" 17 | serviceAccountSecretRef: 18 | name: "{{.DNSService}}-service-account" 19 | key: service-account.json 20 | {{else if eq .DNSService "route53" }} 21 | region: {{.Region}} 22 | # optional if ambient credentials are available; see ambient credentials documentation 23 | accessKeyID: {{.AccessKeyID}} 24 | secretAccessKeySecretRef: 25 | name: "{{.DNSService}}-credentials-secret" 26 | key: secret-access-key 27 | {{else if eq .DNSService "cloudflare" }} 28 | email: {{.Email}} 29 | apiKeySecretRef: 30 | name: cloudflare-api-key-secret 31 | key: api-key 32 | {{else if eq .DNSService "digitalocean" }} 33 | tokenSecretRef: 34 | name: digitalocean-dns 35 | key: access-token 36 | {{ end }} -------------------------------------------------------------------------------- /templates/k8s/tls/issuer-staging.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-staging 5 | namespace: openfaas 6 | spec: 7 | acme: 8 | email: "{{.Email}}" 9 | server: https://acme-staging-v02.api.letsencrypt.org/directory 10 | privateKeySecretRef: 11 | name: letsencrypt-staging 12 | solvers: 13 | - dns01: 14 | {{.DNSService}}: 15 | {{ if eq .DNSService "clouddns" }} 16 | project: "{{.ProjectID}}" 17 | serviceAccountSecretRef: 18 | name: "{{.DNSService}}-service-account" 19 | key: service-account.json 20 | {{else if eq .DNSService "route53" }} 21 | region: {{.Region}} 22 | # optional if ambient credentials are available; see ambient credentials documentation 23 | accessKeyID: {{.AccessKeyID}} 24 | secretAccessKeySecretRef: 25 | name: "{{.DNSService}}-credentials-secret" 26 | key: secret-access-key 27 | {{else if eq .DNSService "cloudflare" }} 28 | email: {{.Email}} 29 | apiKeySecretRef: 30 | name: cloudflare-api-key-secret 31 | key: api-key 32 | {{else if eq .DNSService "digitalocean" }} 33 | tokenSecretRef: 34 | name: digitalocean-dns 35 | key: access-token 36 | {{ end }} -------------------------------------------------------------------------------- /templates/k8s/tls/wildcard-domain-cert.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: cert-manager.io/v1alpha2 3 | kind: Certificate 4 | metadata: 5 | name: wildcard-{{.RootDomain}} 6 | namespace: openfaas 7 | spec: 8 | secretName: wildcard-{{.RootDomain}}-cert 9 | issuerRef: 10 | name: letsencrypt-{{.IssuerType}} 11 | kind: ClusterIssuer 12 | commonName: '*.{{.RootDomain}}' 13 | dnsNames: 14 | - '*.{{.RootDomain}}' 15 | -------------------------------------------------------------------------------- /templates/of-builder-dep.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: of-builder 5 | namespace: openfaas 6 | labels: 7 | app: of-builder 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: of-builder 13 | template: 14 | metadata: 15 | annotations: 16 | prometheus.io.scrape: "false" 17 | labels: 18 | app: of-builder 19 | spec: 20 | volumes: 21 | - name: registry-secret 22 | secret: 23 | secretName: registry-secret 24 | - name: payload-secret 25 | secret: 26 | secretName: payload-secret 27 | {{ if .ECR }} 28 | - name: aws-ecr-credentials 29 | secret: 30 | defaultMode: 420 31 | secretName: aws-ecr-credentials 32 | {{ end }} 33 | containers: 34 | - name: of-builder 35 | image: ghcr.io/openfaas/ofc-of-builder:0.14.6 36 | imagePullPolicy: Always 37 | livenessProbe: 38 | httpGet: 39 | path: /healthz 40 | port: 8080 41 | initialDelaySeconds: 2 42 | periodSeconds: 10 43 | timeoutSeconds: 2 44 | env: 45 | - name: enable_lchown 46 | value: "true" 47 | - name: insecure 48 | value: "false" 49 | - name: buildkit_url 50 | value: "tcp://127.0.0.1:1234" 51 | - name: "disable_hmac" 52 | value: "false" 53 | ports: 54 | - containerPort: 8080 55 | protocol: TCP 56 | volumeMounts: 57 | - name: registry-secret 58 | readOnly: true 59 | mountPath: "/home/app/.docker/" 60 | - name: payload-secret 61 | readOnly: true 62 | mountPath: "/var/openfaas/secrets/" 63 | {{ if .ECR }} 64 | - mountPath: /home/app/.aws/ 65 | readOnly: true 66 | name: aws-ecr-credentials 67 | {{ end }} 68 | - name: of-buildkit 69 | args: ["--addr", "tcp://0.0.0.0:1234"] 70 | image: moby/buildkit:v0.7.2 71 | imagePullPolicy: Always 72 | ports: 73 | - containerPort: 1234 74 | protocol: TCP 75 | securityContext: 76 | privileged: true 77 | -------------------------------------------------------------------------------- /templates/slack.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | slack_url: "{{.URL}}" 3 | -------------------------------------------------------------------------------- /templates/stack.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: openfaas 3 | gateway: http://127.0.0.1:8080 4 | 5 | functions: 6 | {{ if .GitHub }} 7 | system-github-event: 8 | lang: go 9 | handler: ./github-event 10 | image: ghcr.io/${REPO:-openfaas}/ofc-github-event:0.14.6 11 | labels: 12 | openfaas-cloud: "1" 13 | role: openfaas-system 14 | com.openfaas.scale.zero: false 15 | environment: 16 | validate_hmac: true 17 | write_debug: true 18 | read_debug: true 19 | validate_customers: true 20 | customers_path: "{{.CustomersSecretPath}}" 21 | environment_file: 22 | - github.yml 23 | - gateway_config.yml 24 | secrets: 25 | - github-webhook-secret 26 | - payload-secret 27 | - customers 28 | 29 | github-push: 30 | lang: go 31 | handler: ./github-push 32 | image: ghcr.io/${REPO:-openfaas}/ofc-github-push:0.14.6 33 | labels: 34 | openfaas-cloud: "1" 35 | role: openfaas-system 36 | com.openfaas.scale.zero: false 37 | environment: 38 | validate_hmac: true 39 | read_timeout: 10s 40 | write_timeout: 10s 41 | write_debug: true 42 | read_debug: true 43 | environment_file: 44 | - gateway_config.yml 45 | - github.yml 46 | secrets: 47 | - github-webhook-secret 48 | - payload-secret 49 | {{ end }} 50 | 51 | git-tar: 52 | lang: dockerfile 53 | handler: ./git-tar 54 | image: ghcr.io/${REPO:-openfaas}/ofc-git-tar:0.14.6 55 | labels: 56 | openfaas-cloud: "1" 57 | role: openfaas-system 58 | com.openfaas.scale.zero: false 59 | environment: 60 | read_timeout: 15m 61 | write_timeout: 15m 62 | write_debug: true 63 | read_debug: true 64 | environment_file: 65 | - gateway_config.yml 66 | - github.yml 67 | secrets: 68 | - payload-secret 69 | {{ if .GitHub }} 70 | - private-key 71 | {{ end }} 72 | # Uncomment this for GitLab 73 | {{ if not .GitHub }} 74 | - gitlab-api-token 75 | {{ end }} 76 | 77 | buildshiprun: 78 | lang: go 79 | handler: ./buildshiprun 80 | image: ghcr.io/${REPO:-openfaas}/ofc-buildshiprun:0.14.6 81 | labels: 82 | openfaas-cloud: "1" 83 | role: openfaas-system 84 | com.openfaas.scale.zero: false 85 | environment: 86 | read_timeout: 5m 87 | write_timeout: 5m 88 | write_debug: true 89 | read_debug: true 90 | scaling_factor: 50 91 | environment_file: 92 | - buildshiprun_limits.yml 93 | - gateway_config.yml 94 | - github.yml 95 | secrets: 96 | - basic-auth-user 97 | - basic-auth-password 98 | - payload-secret 99 | # - swarm-pull-secret 100 | 101 | garbage-collect: 102 | lang: go 103 | handler: ./garbage-collect 104 | image: ghcr.io/${REPO:-openfaas}/ofc-garbage-collect:0.14.6 105 | labels: 106 | openfaas-cloud: "1" 107 | role: openfaas-system 108 | com.openfaas.scale.zero: false 109 | environment: 110 | write_debug: true 111 | read_debug: true 112 | read_timeout: 30s 113 | write_timeout: 30s 114 | environment_file: 115 | - gateway_config.yml 116 | secrets: 117 | - basic-auth-user 118 | - basic-auth-password 119 | - payload-secret 120 | 121 | {{ if .GitHub }} 122 | github-status: 123 | lang: go 124 | handler: ./github-status 125 | image: ghcr.io/${REPO:-openfaas}/ofc-github-status:0.14.6 126 | labels: 127 | openfaas-cloud: "1" 128 | role: openfaas-system 129 | com.openfaas.scale.zero: false 130 | environment: 131 | write_debug: false 132 | read_debug: false 133 | combine_output: false 134 | validate_hmac: true 135 | environment_file: 136 | - gateway_config.yml 137 | - github.yml 138 | secrets: 139 | - private-key 140 | - payload-secret 141 | {{ end }} 142 | 143 | import-secrets: 144 | lang: go 145 | handler: ./import-secrets 146 | image: ghcr.io/${REPO:-openfaas}/ofc-import-secrets:0.14.6 147 | annotations: 148 | com.openfaas.serviceaccount: sealedsecrets-importer-rw 149 | labels: 150 | openfaas-cloud: "1" 151 | role: openfaas-system 152 | com.openfaas.scale.zero: false 153 | environment: 154 | write_debug: true 155 | read_debug: true 156 | validate_hmac: true 157 | combined_output: false 158 | environment_file: 159 | - github.yml 160 | secrets: 161 | - payload-secret 162 | 163 | pipeline-log: 164 | lang: go 165 | handler: ./pipeline-log 166 | image: ghcr.io/${REPO:-openfaas}/ofc-pipeline-log:0.14.6 167 | labels: 168 | openfaas-cloud: "1" 169 | role: openfaas-system 170 | com.openfaas.scale.zero: false 171 | environment: 172 | write_debug: true 173 | read_debug: true 174 | combine_output: false 175 | environment_file: 176 | - gateway_config.yml 177 | secrets: 178 | - s3-access-key 179 | - s3-secret-key 180 | - payload-secret 181 | 182 | list-functions: 183 | lang: go 184 | handler: ./list-functions 185 | image: ghcr.io/${REPO:-openfaas}/ofc-list-functions:0.14.6 186 | labels: 187 | openfaas-cloud: "1" 188 | role: openfaas-system 189 | com.openfaas.scale.zero: false 190 | environment: 191 | write_debug: true 192 | read_debug: true 193 | environment_file: 194 | - gateway_config.yml 195 | secrets: 196 | - basic-auth-user 197 | - basic-auth-password 198 | 199 | audit-event: 200 | lang: go 201 | handler: ./audit-event 202 | image: ghcr.io/${REPO:-openfaas}/ofc-audit-event:0.14.6 203 | labels: 204 | openfaas-cloud: "1" 205 | role: openfaas-system 206 | com.openfaas.scale.zero: false 207 | environment_file: 208 | - slack.yml 209 | 210 | echo: 211 | lang: go 212 | handler: ./echo 213 | image: ghcr.io/${REPO:-openfaas}/ofc-echo:0.14.6 214 | labels: 215 | openfaas-cloud: "1" 216 | com.openfaas.scale.zero: false 217 | environment: 218 | write_debug: true 219 | read_debug: true 220 | limits: 221 | memory: 64Mi 222 | requests: 223 | memory: 32Mi 224 | cpu: 50m 225 | 226 | metrics: 227 | lang: go 228 | handler: ./metrics 229 | image: ghcr.io/${REPO:-openfaas}/ofc-system-metrics:0.14.6 230 | labels: 231 | openfaas-cloud: "1" 232 | role: openfaas-system 233 | com.openfaas.scale.zero: false 234 | environment_file: 235 | - gateway_config.yml 236 | environment: 237 | content_type: "application/json" 238 | 239 | function-logs: 240 | lang: go 241 | handler: ./function-logs 242 | image: ghcr.io/${REPO:-openfaas}/ofc-function-logs:0.14.6 243 | labels: 244 | openfaas-cloud: "1" 245 | role: openfaas-system 246 | com.openfaas.scale.zero: false 247 | environment: 248 | write_debug: true 249 | read_debug: true 250 | environment_file: 251 | - gateway_config.yml 252 | secrets: 253 | - basic-auth-user 254 | - basic-auth-password 255 | 256 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | //GitCommit Git Commit SHA 5 | GitCommit string 6 | //Version version of the CLI 7 | Version string 8 | ) 9 | 10 | //GetVersion get latest version 11 | func GetVersion() string { 12 | if len(Version) == 0 { 13 | return "dev" 14 | } 15 | return Version 16 | } 17 | 18 | const Logo = ` ___ _____ ____ 19 | / _ \| ___/ ___| 20 | | | | | |_ | | 21 | | |_| | _|| |___ 22 | \___/|_| \____|` 23 | --------------------------------------------------------------------------------