~egtann/terrafirma

ref: 5ea0c0991b761c75ce2fa3be24c98780be672695 terrafirma/cmd/terrafirma/main.go -rw-r--r-- 4.5 KiB
5ea0c099 — Evan Tann add ignore deleted flag for inventory 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"os"
	"time"

	tf "egt.run/terrafirma"
	"egt.run/terrafirma/google"
)

type providerName string

type config struct {
	Boxes     map[tf.BoxName]*tf.Box             `json:"boxes"`
	Providers map[providerName]map[string]string `json:"providers"`
}

func main() {
	rand.Seed(time.Now().UnixNano())
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	const defaultPlanFile = "tf_plan.json"
	var (
		configFile    = flag.String("f", "services.json", "services file")
		planFile      = flag.String("p", defaultPlanFile, "plan file")
		bins          = flag.String("b", "", "bins")
		timeout       = flag.Duration("t", 5*time.Minute, "timeout")
		external      = flag.Bool("x", false, "show external ips (inventory only)")
		ignoreDestroy = flag.Bool("i", false, "ignore servers to be destroyed")
	)
	flag.Parse()

	args := flag.Args()
	badCmd := errors.New("must use: plan, create, destroy, inventory")
	if len(args) != 1 {
		return badCmd
	}
	cmd := args[0]
	switch cmd {
	case "plan", "create", "destroy", "inventory":
	default:
		return badCmd
	}

	if cmd == "plan" {
		if *bins == "" {
			return errors.New("bins -b must be specified")
		}
		if *planFile != defaultPlanFile {
			return errors.New("-p cannot be specified with plan")
		}
	}

	conf, err := parseConfig(*configFile)
	if err != nil {
		return fmt.Errorf("parse config: %w", err)
	}

	// TODO(egtann) ultimately we can support multiple providers, making
	// even a cross-cloud setup pretty easy
	project := conf.Providers["google"]["project"]
	region := conf.Providers["google"]["region"]
	zone := conf.Providers["google"]["zone"]
	token := os.Getenv("TOKEN")
	cloudProvider := google.New(&http.Client{}, project, region, zone,
		token)
	terra := tf.New(cloudProvider, *timeout)

	if cmd == "plan" {
		var services map[tf.BoxName][][]string
		err := json.Unmarshal([]byte(*bins), &services)
		if err != nil {
			return fmt.Errorf("unmarshal services: %w", err)
		}
		tfPlan, err := terra.Plan(conf.Boxes, services)
		if err != nil {
			return fmt.Errorf("plan: %w", err)
		}
		err = json.NewEncoder(os.Stdout).Encode(tfPlan)
		if err != nil {
			return fmt.Errorf("encode: %w", err)
		}
		return nil
	}

	// We're creating or destroying servers or taking inventory. In any
	// case, parse our planfile
	tfPlan, err := parsePlan(*planFile)
	switch {
	case os.IsNotExist(err) && cmd == "inventory":
		tfPlan = &tf.Plan{}
	case err != nil:
		return fmt.Errorf("load plan: %w", err)
	}

	if cmd == "inventory" {
		vms, err := terra.Inventory()
		if err != nil {
			return fmt.Errorf("get all: %w", err)
		}

		destroy := make(map[string]struct{}, len(tfPlan.Destroy))
		for _, vm := range tfPlan.Destroy {
			destroy[vm.Name] = struct{}{}
		}

		// TODO(egtann) allow each service to configure whether
		// inventory should include external or internal IPs?
		inv := map[string][]string{}
		for _, vm := range vms {
			if _, ok := destroy[vm.Name]; ok && *ignoreDestroy {
				continue
			}
			for _, ip := range vm.IPs {
				if *external {
					if ip.Type == tf.IPExternal {
						inv[ip.Addr] = vm.Tags
					}
					continue
				}
				if ip.Type == tf.IPInternal {
					inv[ip.Addr] = vm.Tags
				}
			}
		}
		if err := json.NewEncoder(os.Stdout).Encode(inv); err != nil {
			return fmt.Errorf("encode: %w", err)
		}
		return nil
	}

	switch cmd {
	case "create":
		if len(tfPlan.Create) == 0 {
			return errors.New("create: nothing to do")
		}
		if err = terra.CreateAll(conf.Boxes, tfPlan); err != nil {
			return fmt.Errorf("create: %w", err)
		}
		return nil
	case "destroy":
		if len(tfPlan.Destroy) == 0 {
			return errors.New("destroy: nothing to do")
		}
		if err = terra.DestroyAll(tfPlan); err != nil {
			return fmt.Errorf("destroy: %w", err)
		}
		return nil
	default:
		return errors.New("unknown action")
	}
}

func parseConfig(filename string) (*config, error) {
	fi, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer func() { _ = fi.Close() }()

	var conf config
	if err := json.NewDecoder(fi).Decode(&conf); err != nil {
		return nil, fmt.Errorf("decode: %w", err)
	}
	return &conf, nil
}

func parsePlan(filename string) (*tf.Plan, error) {
	var r io.Reader
	if filename == "-" {
		r = os.Stdin
	} else {
		fi, err := os.Open(filename)
		if err != nil {
			// Must return the error without wrapping
			return nil, err
		}
		defer func() { _ = fi.Close() }()
		r = fi
	}

	var plan tf.Plan
	if err := json.NewDecoder(r).Decode(&plan); err != nil {
		return nil, fmt.Errorf("decode: %w", err)
	}
	return &plan, nil
}