-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathcloud_setup.go
285 lines (230 loc) · 8.75 KB
/
cloud_setup.go
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package commands
import (
"bytes"
"fmt"
"kool-dev/kool/core/environment"
"kool-dev/kool/core/shell"
"kool-dev/kool/services/cloud"
"kool-dev/kool/services/cloud/setup"
"kool-dev/kool/services/compose"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/spf13/cobra"
yaml3 "gopkg.in/yaml.v3"
)
// KoolCloudSetup holds handlers and functions for setting up deployment configuration
type KoolCloudSetup struct {
DefaultKoolService
setupParser setup.CloudSetupParser
promptSelect shell.PromptSelect
env environment.EnvStorage
}
// NewSetupCommand initializes new kool deploy Cobra command
func NewSetupCommand(setup *KoolCloudSetup) *cobra.Command {
return &cobra.Command{
Use: "setup",
Short: "Set up local configuration files for deployment",
RunE: DefaultCommandRunFunction(setup),
Args: cobra.NoArgs,
DisableFlagsInUseLine: true,
}
}
// NewKoolCloudSetup factories new KoolCloudSetup instance pointer
func NewKoolCloudSetup() *KoolCloudSetup {
env := environment.NewEnvStorage()
return &KoolCloudSetup{
*newDefaultKoolService(),
setup.NewDefaultCloudSetupParser(env.Get("PWD")),
shell.NewPromptSelect(),
env,
}
}
// Execute runs the setup logic.
func (s *KoolCloudSetup) Execute(args []string) (err error) {
var (
composeConfig *compose.DockerComposeConfig
serviceName string
deployConfig *cloud.CloudConfig = &cloud.CloudConfig{
Version: "1.0",
Services: make(map[string]*cloud.DeployConfigService),
}
postInstructions []func()
)
if s.setupParser.HasDeployConfig() {
err = fmt.Errorf("you already have a %s file - if you want to create a new one using the setup wizard rename/remove the existing file", setup.KoolDeployFile)
return
}
if !s.Shell().IsTerminal() {
err = fmt.Errorf("setup command is not available in non-interactive mode")
return
}
s.Shell().Warning("Kool.dev Cloud auto-setup is an experimental feature. Make sure to review all the generated configuration files before deploying.")
s.Shell().Info("Loading docker compose configuration...")
if composeConfig, err = compose.ParseConsolidatedDockerComposeConfig(s.env.Get("PWD")); err != nil {
return
}
s.Shell().Info("Docker compose configuration loaded. Starting interactive setup:")
var hasPublicPort bool = false
var serviceNames []string
for serviceName = range composeConfig.Services {
serviceNames = append(serviceNames, serviceName)
}
slices.Sort[[]string](serviceNames)
for _, serviceName = range serviceNames {
var (
confirmed bool
isPublic bool = false
answer string
composeService = composeConfig.Services[serviceName]
)
if confirmed, err = s.promptSelect.Confirm("Do you want to deploy the service container '%s'?", serviceName); err != nil {
return
} else if !confirmed {
s.Shell().Warning(fmt.Sprintf("SKIP - not deploying service container '%s'", serviceName))
continue
}
s.Shell().Info(fmt.Sprintf("Setting up service container '%s' for deployment", serviceName))
deployConfig.Services[serviceName] = &cloud.DeployConfigService{
Environment: map[string]string{},
}
// services needs to have either a build config or refer to a pre-built image
if composeService.Build == nil && composeService.Image == nil {
err = fmt.Errorf("unable to deploy service '%s': it needs to define an image or spec to build one", serviceName)
return
}
// handle image/build config
if composeService.Build != nil {
// validate the referenced file exists
var buildFilePath string
if ctx, isString := (*composeService.Build).(string); isString {
// if it's a string, that should be the build path
buildFilePath = filepath.Join(ctx, "Dockerfile")
} else if buildConfig, isMap := (*composeService.Build).(map[string]interface{}); isMap {
ctx, exists := buildConfig["context"].(string)
if !exists || ctx == "" {
ctx = "."
}
if customFilename, exists := buildConfig["dockerfile"].(string); exists {
buildFilePath = filepath.Join(ctx, customFilename)
} else {
buildFilePath = filepath.Join(ctx, "Dockerfile")
}
}
// now just make sure we can see/have this file
if _, buildPathErr := os.Stat(buildFilePath); os.IsNotExist(buildPathErr) {
err = fmt.Errorf("build config error: service '%s' points to non-existing Dockerfile '%s'", serviceName, buildFilePath)
return
}
s.Shell().Info(fmt.Sprintf("Service container '%s' builds its image from '%s'", serviceName, buildFilePath))
} else {
s.Shell().Info(fmt.Sprintf("Service container '%s' uses image '%v'", serviceName, *composeService.Image))
// no build config, so we'll need to build
if len(composeService.Volumes) > 0 {
if confirmed, err = s.promptSelect.Confirm("Do you want to create a new Dockerfile to build service '%s'?", serviceName); err != nil {
return
} else if confirmed {
s.Shell().Info(fmt.Sprintf("Going to create Dockerfile for service '%s'", serviceName))
// so here we should build the basic/simplest Dockerfile
var strPtr interface{} = new(string)
deployConfig.Services[serviceName].Build = &strPtr
(*deployConfig.Services[serviceName].Build) = "."
if _, errStat := os.Stat("Dockerfile"); os.IsNotExist(errStat) {
// we don't have a Dockerfile, let's make a basic one!
var (
dockerfile *os.File
content bytes.Buffer
)
if dockerfile, err = os.Create("Dockerfile"); err != nil {
return
}
content.WriteString(fmt.Sprintf("FROM %s\n", (*composeService.Image).(string)))
for _, vol := range composeService.Volumes {
volParts := strings.Split(vol, ":")
if !strings.HasPrefix(volParts[0], ".") && !strings.HasPrefix(volParts[0], "/") {
s.Shell().Println(fmt.Sprintf("Skipping named volume '%s'", volParts[0]))
continue
}
if confirmed, err = s.promptSelect.Confirm("Do you want to add folder '%s' onto '%s' in the Dockerfile for service '%s'?", volParts[0], volParts[1], serviceName); err != nil {
return
} else if confirmed {
content.WriteString(fmt.Sprintf("\nCOPY %s %s\n", volParts[0], volParts[1]))
}
}
if _, err = dockerfile.Write(content.Bytes()); err != nil {
return
}
_ = dockerfile.Close()
postInstructions = append(postInstructions, func(serviceName string) func() {
return func() {
s.Shell().Info(fmt.Sprintf("⇒ New Dockerfile was created to build service '%s' for deploy. Review and make sure it has all the required steps. ", serviceName))
}
}(serviceName))
}
} else {
postInstructions = append(postInstructions, func(serviceName string) func() {
return func() {
s.Shell().Info(fmt.Sprintf("⇒ Service '%s' uses volumes. Make sure to create the necessary Dockerfile and build it to deploy if necessary.", serviceName))
}
}(serviceName))
}
}
}
// handle port/public config
ports := composeService.Ports
if len(ports) > 0 {
s.Shell().Info(fmt.Sprintf("Service container '%s' exposes network ports", serviceName))
potentialPorts := []string{}
for i := range ports {
mappedPorts := strings.Split(ports[i], ":")
potentialPorts = append(potentialPorts, mappedPorts[len(mappedPorts)-1])
}
if !hasPublicPort {
if confirmed, err = s.promptSelect.Confirm("Do you want to make service '%s' publicly accessible?", serviceName); err != nil {
return
} else if confirmed {
hasPublicPort = true
isPublic = true
}
}
if len(potentialPorts) > 1 {
if answer, err = s.promptSelect.Ask("Which port do you want to use for this service?", potentialPorts); err != nil {
return
}
} else {
answer = potentialPorts[0]
}
deployConfig.Services[serviceName].Expose = new(int)
*deployConfig.Services[serviceName].Expose, _ = strconv.Atoi(answer)
if isPublic {
// public := &cloud.DeployConfigPublicEntry{}
// public.Port = new(int)
// *public.Port = *deployConfig.Services[serviceName].Expose
// deployConfig.Services[serviceName].Public = append(deployConfig.Services[serviceName].Public, public)
deployConfig.Services[serviceName].Public = true
}
}
}
var yaml []byte
if yaml, err = yaml3.Marshal(deployConfig); err != nil {
return
}
if err = os.WriteFile(s.setupParser.ConfigFilePath(), yaml, 0644); err != nil {
return
}
s.Shell().Println("")
for _, instruction := range postInstructions {
instruction()
}
s.Shell().Println("")
s.Shell().Println("")
s.Shell().Success("Setup completed. Please review the generated configuration file before deploying.")
s.Shell().Println("")
s.Shell().Println("Configuration file: " + s.setupParser.ConfigFilePath())
s.Shell().Println("")
s.Shell().Println("Reference: https://kool.dev/docs/deploy-to-kool-cloud/kool-cloud-yml-reference")
s.Shell().Println("")
return
}