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

feat: Prune old resources #251

Merged
merged 9 commits into from
Apr 20, 2020
Merged
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
1 change: 1 addition & 0 deletions cmd/tk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func main() {
applyCmd(),
showCmd(),
diffCmd(),
pruneCmd(),
)

rootCmd.AddCommand(
Expand Down
22 changes: 22 additions & 0 deletions cmd/tk/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ func applyCmd() *cli.Command {
return cmd
}

func pruneCmd() *cli.Command {
cmd := &cli.Command{
Use: "prune <path>",
Short: "delete resources removed from Jsonnet",
Args: workflowArgs,
}

getExtCode := extCodeParser(cmd.Flags())
autoApprove := cmd.Flags().Bool("dangerous-auto-approve", false, "skip interactive approval. Only for automation!")
force := cmd.Flags().Bool("force", false, "force deleting (kubectl delete --force)")

cmd.Run = func(cmd *cli.Command, args []string) error {
return tanka.Prune(args[0],
tanka.WithExtCode(getExtCode()),
tanka.WithApplyAutoApprove(*autoApprove),
tanka.WithApplyForce(*force),
)
}

return cmd
}

func diffCmd() *cli.Command {
cmd := &cli.Command{
Use: "diff <path>",
Expand Down
64 changes: 64 additions & 0 deletions docs/docs/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
name: "Configuration Reference"
route: "/config"
---

# Configuration Reference

Tanka's behavior can be customized per Environment using a file called `spec.json`

## File format

```json
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
{
// Config format revision. Currently only "v1alpha1"
"apiVersion": "v1alpha1",
// Always "Environment". Reserved for future use
"kind": "Environment",

// Descriptive fields
"metadata": {
// Name of the Environment. Automatically set to the relative
// path from the project root
"name": "<string>",

// Arbitrary key:value string pairs. Not parsed by Tanka
"labels": { "<string>": "<string>" }
},

// Properties influencing Tanka's behavior
"spec": {
// The Kubernetes cluster to use.
// Must be the full URL, e.g. https://cluster.fqdn:6443
"apiServer": "<url>",

// Default namespace for objects that don't explicitely specify one
"namespace": "<string>" | default = "default",

// diffStrategy to use. Automatically chosen by default based on
// the availability of "kubectl diff".
// - native: uses "kubectl diff". Recommended
// - subset: fallback for k8s versions below 1.13.0
"diffStrategy": "[native, subset]" | default = "auto",

// Whether to add a "tanka.dev/environment" label to each created resource.
// Required for garbage collection ("tk prune").
"injectLabels": <boolean> | default = false
}
}
```

## Jsonnet access

It is possible to access above data from Jsonnet:

```jsonnet
local tk = import "tk.libsonnet";

{
// The cluster IP
cluster: tk.env.spec.apiServer,
// The labels of your Environment
labels: tk.env.metadata.labels,
}
```
32 changes: 32 additions & 0 deletions docs/docs/garbage-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: "Garbage collection"
route: "/garbage-collection"
---

# Garbage collection

Tanka can automatically delete resources from your cluster once you remove them
from Jsonnet.

> **Note:** This feature is **experimental**. Please report problems at https://github.com/grafana/tanka/issues.

To accomplish this, it appends the `tanka.dev/environment: <name>` label to each created
resource. This is used to identify those which are missing from the local state in the
future.

Because the label causes a `diff` for every single object in your cluster and
not everybody wants this, it needs to be explicitly enabled. To do so, add the
following field to your `spec.json`:

```diff
{
"spec": {
+ "injectLabels": true,
}
}
```

Once added, run a `tk apply`, make sure the label is actually added and confirm
by typing `yes`.

From now on, you can use `tk prune` to remove old resources from your cluster.
2 changes: 2 additions & 0 deletions docs/doczrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ export default {
// additional features
"Output filtering",
"Exporting as YAML",
"Garbage collection",
"Command-line completion",
"Diff strategies",

// reference
"Configuration Reference",
"Directory structure",
"Environment variables",

Expand Down
105 changes: 86 additions & 19 deletions pkg/kubernetes/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,102 @@ package kubernetes

import (
"fmt"

"github.com/fatih/color"
"strings"
"time"

"github.com/grafana/tanka/pkg/kubernetes/client"
"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/grafana/tanka/pkg/term"
)

// ApplyOpts allow set additional parameters for the apply operation
type ApplyOpts client.ApplyOpts

// Apply receives a state object generated using `Reconcile()` and may apply it to the target system
func (k *Kubernetes) Apply(state manifest.List, opts ApplyOpts) error {
alert := color.New(color.FgRed, color.Bold).SprintFunc()

cluster := k.ctl.Info().Kubeconfig.Cluster
context := k.ctl.Info().Kubeconfig.Context
if !opts.AutoApprove {
if err := term.Confirm(
fmt.Sprintf(`Applying to namespace '%s' of cluster '%s' at '%s' using context '%s'.`,
alert(k.Spec.Namespace),
alert(cluster.Name),
alert(cluster.Cluster.Server),
alert(context.Name),
),
"yes",
); err != nil {
return err
return k.ctl.Apply(state, client.ApplyOpts(opts))
}

// AnnoationLastApplied is the last-applied-configuration annotation used by kubectl
const AnnotationLastApplied = "kubectl.kubernetes.io/last-applied-configuration"

// Orphaned returns previously created resources that are missing from the
// local state. It uses UIDs to safely identify objects.
func (k *Kubernetes) Orphaned(state manifest.List) (manifest.List, error) {
if !k.Env.Spec.InjectLabels {
return nil, fmt.Errorf(`spec.injectLabels is set to false in your spec.json. Tanka needs to add
a label to your resources to reliably detect which were removed from Jsonnet.
See https://tanka.dev/garbage-collection for more details.`)
}

apiResources, err := k.ctl.Resources()
if err != nil {
return nil, err
}

start := time.Now()
fmt.Print("fetching UID's .. ")
uids, err := k.uids(state)
if err != nil {
return nil, err
}
fmt.Println("done", time.Since(start))

var orphaned manifest.List

// join all kinds that support LIST into a comma separated string for
// kubectl
kinds := ""
for _, r := range apiResources {
if !strings.Contains(r.Verbs, "list") {
continue
}

kinds += "," + r.FQN()
}
return k.ctl.Apply(state, client.ApplyOpts(opts))
kinds = strings.TrimPrefix(kinds, ",")

start = time.Now()
fmt.Print("fetching previously created resources .. ")
// get all resources matching our label
matched, err := k.ctl.GetByLabels("", kinds, map[string]string{
LabelEnvironment: k.Env.Metadata.NameLabel(),
})
if err != nil {
return nil, err
}
fmt.Println("done", time.Since(start))

// filter unknown
for _, m := range matched {
// ignore known ones
if uids[m.Metadata().UID()] {
continue
}

// skip objects not created explicitely
if _, ok := m.Metadata().Annotations()[AnnotationLastApplied]; !ok {
continue
}

// record and skip from now on
orphaned = append(orphaned, m)
uids[m.Metadata().UID()] = true
}

return orphaned, nil
}

func (k *Kubernetes) uids(state manifest.List) (map[string]bool, error) {
uids := make(map[string]bool)

live, err := k.ctl.GetByState(state)
if err != nil {
return nil, err
}

for _, m := range live {
uids[m.Metadata().UID()] = true
}

return uids, nil
}
4 changes: 2 additions & 2 deletions pkg/kubernetes/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
type Client interface {
// Get the specified object(s) from the cluster
Get(namespace, kind, name string) (manifest.Manifest, error)
GetByLabels(namespace string, labels map[string]interface{}) (manifest.List, error)
GetByLabels(namespace, kind string, labels map[string]string) (manifest.List, error)
GetByState(data manifest.List) (manifest.List, error)

// Apply the configuration to the cluster. `data` must contain a plaintext
// format that is `kubectl-apply(1)` compatible
Expand All @@ -20,7 +21,6 @@ type Client interface {

// Delete the specified object(s) from the cluster
Delete(namespace, kind, name string, opts DeleteOpts) error
DeleteByLabels(namespace string, labels map[string]interface{}, opts DeleteOpts) error

// Namespaces the cluster currently has
Namespaces() (map[string]bool, error)
Expand Down
19 changes: 3 additions & 16 deletions pkg/kubernetes/client/delete.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
package client

import (
"fmt"
"os"
)

// Delete removes the specified object from the cluster
func (k Kubectl) Delete(namespace, kind, name string, opts DeleteOpts) error {
return k.delete(namespace, []string{kind, name}, opts)
}

// DeleteByLabels removes all objects matched by the given labels from the cluster
func (k Kubectl) DeleteByLabels(namespace string, labels map[string]interface{}, opts DeleteOpts) error {
lArgs := make([]string, 0, len(labels))
for k, v := range labels {
lArgs = append(lArgs, fmt.Sprintf("-l=%s=%s", k, v))
argv := []string{
"-n", namespace,
kind, name,
}

return k.delete(namespace, lArgs, opts)
}

func (k Kubectl) delete(namespace string, sel []string, opts DeleteOpts) error {
argv := append([]string{"-n", namespace}, sel...)
if opts.Force {
argv = append(argv, "--force")
}
Expand Down
Loading