Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read templates from and to directories #47

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ The following parameters are used to configure this plugin:
* `zone` - zone of the container cluster
* `cluster` - name of the container cluster
* *optional* `namespace` - Kubernetes namespace to operate in (defaults to `default`)
* *optional* `template` - Kubernetes manifest template (defaults to `.kube.yml`)
* *optional* `secret_template` - Kubernetes [_Secret_ resource](http://kubernetes.io/docs/user-guide/secrets/) manifest template (defaults to `.kube.sec.yml`)
* `vars` - variables to use in `template` and `secret_template`
* `secrets` - credential and variables to use in `secret_template` (see [below](#secrets) for details)
* *optional* `input_dir` - directory with Kubernetes manifest templates (defaults to `.kube`)
* *optional* `output_dir` - directory to output rendered Kubernetes manifests (defaults to `.kube-out`)
* `vars` - variables to use in all `.yml` templates
* `secrets` - credentials to use as variables in templates with the `.sec.yml` extension (see [below](#secrets) for details)

### Debugging parameters

Expand Down Expand Up @@ -56,9 +56,14 @@ drone secret add \

## Secrets

`drone-gke` also supports creating Kubernetes secrets for you. These secrets should be passed from Drone secrets to the plugin as environment variables with targets with the prefix `secret_`. These secrets will be used as variables in the `secret_template` in their environment variable form (uppercased).
`drone-gke` also supports creating Kubernetes secrets for you.
These secrets:
- Should be passed from Drone secrets to the plugin as environment variables with targets with the prefix `secret_`.
- Will be used as variables in templates with the `.sec.yml` extension in their environment variable form (uppercased).
- Will **not** be available in templates with only the `.yml` extension.

Kubernetes expects secrets to be base64 encoded, `drone-gke` does that for you. If you pass in a secret that is already base64 encoded, please apply the prefix `secret_base64_` and the plugin will not re-encode them.
Kubernetes expects secrets to be base64 encoded, `drone-gke` does that for you.
If you pass in a secret that is already base64 encoded, please apply the prefix `secret_base64_` and the plugin will not re-encode them.

## Example reference usage

Expand Down Expand Up @@ -89,7 +94,8 @@ pipeline:
username: _json_key
registry: us.gcr.io
repo: us.gcr.io/my-gke-project/my-app
tag: ${DRONE_COMMIT}
tag:
- ${DRONE_COMMIT}
secrets:
- source: GOOGLE_CREDENTIALS
target: docker_password
Expand Down Expand Up @@ -118,9 +124,7 @@ pipeline:
branch: master
```

### `.kube.yml`

Note the two Kubernetes yml resource manifests separated by `---`.
### `.kube/deployment.yml`

```yml
---
Expand Down Expand Up @@ -151,6 +155,11 @@ spec:
secretKeyRef:
name: {{.app}}-{{.env}}
key: api-token
```

### `.kube/service.yml`

```yml
---
apiVersion: v1
kind: Service
Expand All @@ -170,9 +179,9 @@ spec:
targetPort: 8000
```

### `.kube.sec.yml`
### `.kube/secret.sec.yml`

Note that the templated output will not be dumped when debugging.
Note that the rendered manifest will not be dumped when debugging.

```yml
---
Expand Down
17 changes: 0 additions & 17 deletions example/.kube.yml → example/.kube/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,3 @@ spec:
image: {{.image}}
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service

metadata:
name: {{.app}}-{{.env}}

spec:
type: LoadBalancer
selector:
app: {{.app}}
env: {{.env}}
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
File renamed without changes.
17 changes: 17 additions & 0 deletions example/.kube/service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service

metadata:
name: {{.app}}-{{.env}}

spec:
type: LoadBalancer
selector:
app: {{.app}}
env: {{.env}}
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
136 changes: 80 additions & 56 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"

Expand Down Expand Up @@ -90,16 +91,16 @@ func wrapMain() error {
EnvVar: "PLUGIN_NAMESPACE",
},
cli.StringFlag{
Name: "kube-template",
Usage: "optional - template for Kubernetes resources, e.g. deployments",
EnvVar: "PLUGIN_TEMPLATE",
Value: ".kube.yml",
Name: "input-dir",
Usage: "optional - input directory with templates for Kubernetes resources",
EnvVar: "PLUGIN_INPUT_DIR",
Value: ".kube/",
},
cli.StringFlag{
Name: "secret-template",
Usage: "optional - template for Kubernetes Secret resources",
EnvVar: "PLUGIN_SECRET_TEMPLATE",
Value: ".kube.sec.yml",
Name: "output-dir",
Usage: "optional - output directory for rendered manifests for Kubernetes resources",
EnvVar: "PLUGIN_OUTPUT_DIR",
Value: ".kube-out/",
},
cli.StringFlag{
Name: "vars",
Expand Down Expand Up @@ -157,17 +158,6 @@ func run(c *cli.Context) error {
return fmt.Errorf("Missing required param: zone")
}

// Enforce default values.
kubeTemplate := c.String("kube-template")
if kubeTemplate == "" {
kubeTemplate = ".kube.yml"
}

secretTemplate := c.String("secret-template")
if secretTemplate == "" {
secretTemplate = ".kube.sec.yml"
}

// Parse variables.
vars := make(map[string]interface{})
varsJSON := c.String("vars")
Expand Down Expand Up @@ -245,6 +235,7 @@ func run(c *cli.Context) error {
return fmt.Errorf("Error: %s\n", err)
}

// Set up the variables available to templates.
data := map[string]interface{}{
"BUILD_NUMBER": c.String("drone-build-number"),
"COMMIT": c.String("drone-commit"),
Expand All @@ -262,7 +253,7 @@ func run(c *cli.Context) error {
secretsAndData := map[string]interface{}{}
secretsAndDataKeys := map[string]string{}

// Add variables to data used for rendering both templates.
// Add variables to data used for rendering all templates.
for k, v := range vars {
// Don't allow vars to be overridden.
// We do this to ensure that the built-in template vars (above) can be relied upon.
Expand All @@ -274,7 +265,7 @@ func run(c *cli.Context) error {
secretsAndData[k] = v
}

// Add secrets to data used for rendering the Secret template.
// Add secrets to data used for rendering the secret (.sec) templates.
for k, v := range secrets {
// Don't allow vars to be overridden.
// We do this to ensure that the built-in template vars (above) can be relied upon.
Expand All @@ -291,64 +282,99 @@ func run(c *cli.Context) error {
dumpData(os.Stdout, "ADDITIONAL SECRET VARIABLES AVAILABLE FOR .sec.yml TEMPLATES", secretsAndDataKeys)
}

// mapping is a map of the template filename to the data it uses for rendering.
mapping := map[string]map[string]interface{}{
kubeTemplate: data,
secretTemplate: secretsAndData,
}
inputDir := c.String("input-dir")
outputDir := c.String("output-dir")

outPaths := make(map[string]string)
pathArg := []string{}
// Ensure output directory does not exist.
_, err = os.Stat(outputDir)
if err == nil {
return fmt.Errorf("Error: output directory %s already exists, will not pollute existing directory\n", outputDir)
}

for t, content := range mapping {
if t == "" {
continue
// Create the output directory.
if os.IsNotExist(err) {
err = os.MkdirAll(outputDir, os.ModePerm)
if err != nil {
return fmt.Errorf("Error: unable to create output directory %s for rendered manifests\n", outputDir)
}
} else {
return fmt.Errorf("Error creating output directory: %s\n", err)
}

// Ensure the required template file exists.
_, err := os.Stat(t)
if os.IsNotExist(err) {
if t == kubeTemplate {
return fmt.Errorf("Error finding template: %s\n", err)
}
// Loop over all files in input directory.
files, err := ioutil.ReadDir(inputDir)
if err != nil {
return fmt.Errorf("Error reading templates from input directory: %s\n", err)
}

log("Warning: skipping optional template %s because it was not found\n", t)
for _, f := range files {
if f.IsDir() {
// Skip sub-directories.
continue
}

// Create the output file.
outPaths[t] = fmt.Sprintf("/tmp/%s", t)
f, err := os.Create(outPaths[t])
if err != nil {
return fmt.Errorf("Error creating deployment file: %s\n", err)
filename := f.Name()
templateData := map[string]interface{}{}

switch {
case strings.HasSuffix(filename, ".sec.yml"):
// Generate the manifest with `secretsAndData`.
templateData = secretsAndData
case strings.HasSuffix(filename, ".yml"):
// Generate the manifest with `data`.
templateData = data
default:
log("Warning: skipped rendering %s because it is not a .sec.yml or .yml file\n", filename)
continue
}

inName := filepath.Join(inputDir, filename)
outName := filepath.Join(outputDir, filename)

// Read the template.
blob, err := ioutil.ReadFile(t)
blob, err := ioutil.ReadFile(inName)
if err != nil {
return fmt.Errorf("Error reading template: %s\n", err)
}

// Parse the template.
tmpl, err := template.New(t).Option("missingkey=error").Parse(string(blob))
tmpl, err := template.New(outName).Option("missingkey=error").Parse(string(blob))
if err != nil {
return fmt.Errorf("Error parsing template: %s\n", err)
}

// Generate the manifest.
err = tmpl.Execute(f, content)
// Create the output file.
f, err := os.Create(outName)
if err != nil {
return fmt.Errorf("Error rendering deployment manifest from template: %s\n", err)
return fmt.Errorf("Error creating output file: %s\n", err)
}

f.Close()
// Generate the output file.
err = tmpl.Execute(f, templateData)
if err != nil {
return fmt.Errorf("Error rendering manifest from template: %s\n", err)
}

// Add the manifest filepath to the list of manifests to apply.
pathArg = append(pathArg, outPaths[t])
f.Close()
}

if c.Bool("verbose") {
dumpFile(os.Stdout, "RENDERED MANIFEST (Secret Manifest Omitted)", outPaths[kubeTemplate])
for _, f := range files {
if f.IsDir() {
// Skip sub-directories.
continue
}

filename := f.Name()
outName := filepath.Join(outputDir, filename)

switch {
case strings.HasSuffix(filename, ".sec.yml"):
log("Skipped dumping %s because it contains secrets\n", outName)
case strings.HasSuffix(filename, ".yml"):
dumpFile(os.Stdout, fmt.Sprintf("RENDERED MANIFEST (%s)", outName), outName)
}
}
}

// Print kubectl version.
Expand Down Expand Up @@ -387,13 +413,11 @@ func run(c *cli.Context) error {
}
}

manifests := strings.Join(pathArg, ",")

// If it is not a dry run, do a dry run first to validate Kubernetes manifests.
log("Validating Kubernetes manifests with a dry-run\n")

if !c.Bool("dry-run") {
args := applyArgs(true, manifests)
args := applyArgs(true, outputDir)
err = runner.Run(kubectlCmd, args...)
if err != nil {
return fmt.Errorf("Error: %s\n", err)
Expand All @@ -404,7 +428,7 @@ func run(c *cli.Context) error {

// Actually apply Kubernetes manifests.

args := applyArgs(c.Bool("dry-run"), manifests)
args := applyArgs(c.Bool("dry-run"), outputDir)
err = runner.Run(kubectlCmd, args...)
if err != nil {
return fmt.Errorf("Error: %s\n", err)
Expand Down