1 package kubeapply
2
3 import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "io/ioutil"
10 "os"
11 _path "path"
12 "path/filepath"
13 "strings"
14 "text/template"
15
16 "github.com/Masterminds/sprig"
17 "github.com/pkg/errors"
18 "gopkg.in/yaml.v2"
19
20 "github.com/datawire/dlib/dexec"
21 "github.com/emissary-ingress/emissary/v3/pkg/k8s"
22 )
23
24 var readyChecks = map[string]func(k8s.Resource) bool{
25 "": func(_ k8s.Resource) bool { return false },
26 "Deployment": func(r k8s.Resource) bool {
27
28
29
30
31
32 if r.Spec().GetInt64("replicas") == 0 {
33 return true
34 }
35
36 return r.Status().GetInt64("readyReplicas") > 0
37 },
38 "Service": func(r k8s.Resource) bool {
39 return true
40 },
41 "Pod": func(r k8s.Resource) bool {
42 css := r.Status().GetMaps("containerStatuses")
43 for _, cs := range css {
44 if !k8s.Map(cs).GetBool("ready") {
45 return false
46 }
47 }
48 return true
49 },
50 "Namespace": func(r k8s.Resource) bool {
51 return r.Status().GetString("phase") == "Active"
52 },
53 "ServiceAccount": func(r k8s.Resource) bool {
54 _, ok := r["secrets"]
55 return ok
56 },
57 "ClusterRole": func(r k8s.Resource) bool {
58 return true
59 },
60 "ClusterRoleBinding": func(r k8s.Resource) bool {
61 return true
62 },
63 "CustomResourceDefinition": func(r k8s.Resource) bool {
64 conditions := r.Status().GetMaps("conditions")
65 if len(conditions) == 0 {
66 return false
67 }
68 last := conditions[len(conditions)-1]
69 return last["status"] == "True"
70 },
71 }
72
73
74
75 func ReadyImplemented(r k8s.Resource) bool {
76 if r.Empty() {
77 return false
78 }
79 kind := r.Kind()
80 _, ok := readyChecks[kind]
81 return ok
82 }
83
84
85
86
87 func Ready(r k8s.Resource) bool {
88 if r.Empty() {
89 return false
90 }
91 kind := r.Kind()
92 fn, fnOK := readyChecks[kind]
93 if !fnOK {
94 return true
95 }
96 return fn(r)
97 }
98
99 func isTemplate(input []byte) bool {
100 return strings.Contains(string(input), "@TEMPLATE@")
101 }
102
103 func image(ctx context.Context, dir, dockerfile string) (string, error) {
104 iidfile, err := ioutil.TempFile("", "iid")
105 if err != nil {
106 return "", err
107 }
108 defer os.Remove(iidfile.Name())
109 err = iidfile.Close()
110 if err != nil {
111 return "", err
112 }
113
114 dockerCtx := filepath.Dir(filepath.Join(dir, dockerfile))
115 cmd := dexec.CommandContext(ctx, "docker", "build", "-f", filepath.Base(dockerfile), ".", "--iidfile", iidfile.Name())
116 cmd.Dir = dockerCtx
117 err = cmd.Run()
118 if err != nil {
119 return "", err
120 }
121 content, err := ioutil.ReadFile(iidfile.Name())
122 if err != nil {
123 return "", err
124 }
125 iid := strings.Split(strings.TrimSpace(string(content)), ":")[1]
126 short := iid[:12]
127
128 registry := strings.TrimSpace(os.Getenv("DOCKER_REGISTRY"))
129 if registry == "" {
130 return "", errors.Errorf("please set the DOCKER_REGISTRY environment variable")
131 }
132 tag := fmt.Sprintf("%s/kubeapply:%s", registry, short)
133
134 if err := dexec.CommandContext(ctx, "docker", "tag", iid, tag).Run(); err != nil {
135 return "", err
136 }
137
138 cmd = dexec.CommandContext(ctx, "docker", "push", tag)
139 if err := cmd.Run(); err != nil {
140 return "", err
141 }
142
143 return tag, nil
144 }
145
146
147
148 func ExpandResource(ctx context.Context, path string) (result []byte, err error) {
149 input, err := ioutil.ReadFile(path)
150 if err != nil {
151 return nil, fmt.Errorf("%s: %v", path, err)
152 }
153 if isTemplate(input) {
154 funcs := sprig.TxtFuncMap()
155 usedImage := false
156 funcs["image"] = func(dockerfile string) (string, error) {
157 usedImage = true
158 return image(ctx, filepath.Dir(path), dockerfile)
159 }
160 tmpl := template.New(filepath.Base(path)).Funcs(funcs)
161 _, err := tmpl.Parse(string(input))
162 if err != nil {
163 return nil, fmt.Errorf("%s: %v", path, err)
164 }
165
166 buf := bytes.NewBuffer(nil)
167 err = tmpl.ExecuteTemplate(buf, filepath.Base(path), nil)
168 if err != nil {
169 return nil, fmt.Errorf("%s: %v", path, err)
170 }
171
172 result = buf.Bytes()
173
174 if usedImage && os.Getenv("DEV_USE_IMAGEPULLSECRET") != "" {
175 dockercfg, err := json.Marshal(map[string]interface{}{
176 "auths": map[string]interface{}{
177 _path.Dir(os.Getenv("DEV_REGISTRY")): map[string]string{
178 "auth": base64.StdEncoding.EncodeToString([]byte(os.Getenv("DOCKER_BUILD_USERNAME") + ":" + os.Getenv("DOCKER_BUILD_PASSWORD"))),
179 },
180 },
181 })
182 if err != nil {
183 return nil, errors.Wrap(err, "DEV_USE_IMAGEPULLSECRET")
184 }
185
186 secretYaml := fmt.Sprintf(`
187
188 ---
189 apiVersion: v1
190 kind: Secret
191 metadata:
192 name: dev-image-pull-secret
193 type: kubernetes.io/dockerconfigjson
194 data:
195 ".dockerconfigjson": %q
196 ---
197 apiVersion: v1
198 kind: ServiceAccount
199 metadata:
200 name: default
201 imagePullSecrets:
202 - name: dev-image-pull-secret
203 `, base64.StdEncoding.EncodeToString(dockercfg))
204
205 result = append(result, secretYaml...)
206 }
207 } else {
208 result = input
209 }
210
211 return
212 }
213
214
215
216 func LoadResources(ctx context.Context, path string) (result []k8s.Resource, err error) {
217 var input []byte
218 input, err = ExpandResource(ctx, path)
219 if err != nil {
220 return
221 }
222 result, err = k8s.ParseResources(path, string(input))
223 return
224 }
225
226
227 func SaveResources(path string, resources []k8s.Resource) error {
228 output, err := MarshalResources(resources)
229 if err != nil {
230 return fmt.Errorf("%s: %v", path, err)
231 }
232 err = ioutil.WriteFile(path, output, 0644)
233 if err != nil {
234 return fmt.Errorf("%s: %v", path, err)
235 }
236 return nil
237 }
238
239
240 func MarshalResources(resources []k8s.Resource) ([]byte, error) {
241 buf := bytes.NewBuffer(nil)
242 e := yaml.NewEncoder(buf)
243 for _, r := range resources {
244 err := e.Encode(r)
245 if err != nil {
246 return nil, err
247 }
248 }
249 e.Close()
250 return buf.Bytes(), nil
251 }
252
View as plain text