github.com/splunk/dan1-qbec@v0.7.3/internal/commands/init.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package commands 18 19 import ( 20 "bytes" 21 "fmt" 22 "html/template" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 27 "github.com/ghodss/yaml" 28 "github.com/spf13/cobra" 29 "github.com/splunk/qbec/internal/model" 30 "github.com/splunk/qbec/internal/remote" 31 "github.com/splunk/qbec/internal/sio" 32 ) 33 34 type initCommandConfig struct { 35 withExample bool // create a hello world example 36 } 37 38 var baseParamsTemplate = template.Must(template.New("base").Parse(` 39 // this file has the baseline default parameters 40 { 41 components: { {{- if .AddExample}} 42 hello: { 43 indexData: 'hello baseline\n', 44 replicas: 1, 45 }, {{- end}} 46 }, 47 } 48 `)) 49 50 var envParamsTemplate = template.Must(template.New("env").Parse(` 51 // this file has the param overrides for the default environment 52 local base = import './base.libsonnet'; 53 54 base { 55 components +: { {{- if .AddExample}} 56 hello +: { 57 indexData: 'hello default\n', 58 replicas: 2, 59 }, {{- end}} 60 } 61 } 62 `)) 63 64 var paramsTemplate = template.Must(template.New("any-env").Parse(` 65 // this file returns the params for the current qbec environment 66 // you need to add an entry here every time you add a new environment. 67 68 local env = std.extVar('qbec.io/env'); 69 local paramsMap = { 70 _: import './environments/base.libsonnet', 71 default: import './environments/default.libsonnet', 72 }; 73 74 if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile 75 76 `)) 77 78 var componentExampleTemplate = template.Must(template.New("comp").Parse(` 79 local p = import '../params.libsonnet'; 80 local params = p.components.hello; 81 82 [ 83 { 84 apiVersion: 'v1', 85 kind: 'ConfigMap', 86 metadata: { 87 name: 'demo-config', 88 }, 89 data: { 90 'index.html': params.indexData, 91 }, 92 }, 93 { 94 apiVersion: 'apps/v1', 95 kind: 'Deployment', 96 metadata: { 97 name: 'demo-deploy', 98 labels: { 99 app: 'demo-deploy', 100 }, 101 }, 102 spec: { 103 replicas: params.replicas, 104 selector: { 105 matchLabels: { 106 app: 'demo-deploy', 107 }, 108 }, 109 template: { 110 metadata: { 111 labels: { 112 app: 'demo-deploy', 113 }, 114 }, 115 spec: { 116 containers: [ 117 { 118 name: 'main', 119 image: 'nginx:stable', 120 imagePullPolicy: 'Always', 121 volumeMounts: [ 122 { 123 name: 'web', 124 mountPath: '/usr/share/nginx/html', 125 }, 126 ], 127 }, 128 ], 129 volumes: [ 130 { 131 name: 'web', 132 configMap: { 133 name: 'demo-config', 134 }, 135 }, 136 ], 137 }, 138 }, 139 }, 140 }, 141 ] 142 `)) 143 144 func writeTemplateFile(file string, t *template.Template, data interface{}) error { 145 var w bytes.Buffer 146 if err := t.Execute(&w, data); err != nil { 147 return fmt.Errorf("unable to expand template for file %s, %v", file, err) 148 } 149 if err := ioutil.WriteFile(file, w.Bytes(), 0644); err != nil { 150 return err 151 } 152 sio.Noticeln("wrote", file) 153 return nil 154 } 155 156 func writeFiles(dir string, app model.QbecApp, config initCommandConfig) error { 157 if err := os.Mkdir(app.Metadata.Name, 0755); err != nil { 158 return err 159 } 160 161 templateData := struct { 162 AddExample bool 163 }{config.withExample} 164 165 type templateFile struct { 166 t *template.Template 167 f string 168 } 169 170 compsDir, envDir := filepath.Join(dir, "components"), filepath.Join(dir, "environments") 171 for _, dir := range []string{compsDir, envDir} { 172 if err := os.MkdirAll(dir, 0755); err != nil { 173 return err 174 } 175 } 176 files := []templateFile{ 177 { 178 t: paramsTemplate, 179 f: filepath.Join(dir, "params.libsonnet"), 180 }, 181 { 182 t: baseParamsTemplate, 183 f: filepath.Join(envDir, "base.libsonnet"), 184 }, 185 { 186 t: envParamsTemplate, 187 f: filepath.Join(envDir, "default.libsonnet"), 188 }, 189 } 190 if config.withExample { 191 files = append(files, templateFile{ 192 t: componentExampleTemplate, 193 f: filepath.Join(compsDir, "hello.jsonnet"), 194 }) 195 } 196 for _, tf := range files { 197 if err := writeTemplateFile(tf.f, tf.t, templateData); err != nil { 198 return err 199 } 200 } 201 202 b, err := yaml.Marshal(app) 203 if err != nil { 204 return fmt.Errorf("yaml marshal: %v", err) 205 } 206 file := filepath.Join(dir, "qbec.yaml") 207 if err := ioutil.WriteFile(file, b, 0644); err != nil { 208 return err 209 } 210 sio.Noticeln("wrote", file) 211 return nil 212 } 213 214 func doInit(args []string, config initCommandConfig) error { 215 if len(args) != 1 { 216 return fmt.Errorf("a single app name argument must be supplied") 217 } 218 name := args[0] 219 _, err := os.Stat(name) 220 if err == nil { 221 return fmt.Errorf("directory %s already exists", name) 222 } 223 if !os.IsNotExist(err) { 224 return err 225 } 226 227 ctx, err := remote.CurrentContextInfo() 228 if err != nil { 229 sio.Warnf("could not get current K8s context info, %v\n", err) 230 sio.Warnln("using fake parameters for the default environment") 231 ctx = &remote.ContextInfo{ 232 ServerURL: "https://minikube", 233 } 234 } 235 sio.Noticef("using server URL %q and default namespace %q for the default environment\n", ctx.ServerURL, ctx.Namespace) 236 app := model.QbecApp{ 237 Kind: "App", 238 APIVersion: model.LatestAPIVersion, 239 Metadata: model.AppMeta{ 240 Name: name, 241 }, 242 Spec: model.AppSpec{ 243 Environments: map[string]model.Environment{ 244 "default": { 245 Server: ctx.ServerURL, 246 DefaultNamespace: ctx.Namespace, 247 }, 248 }, 249 }, 250 } 251 return writeFiles(name, app, config) 252 } 253 254 func newInitCommand() *cobra.Command { 255 cmd := &cobra.Command{ 256 Use: "init <app-name>", 257 Short: "initialize a qbec app", 258 } 259 260 config := initCommandConfig{} 261 cmd.Flags().BoolVar(&config.withExample, "with-example", false, "create a hello world sample component") 262 263 cmd.RunE = func(c *cobra.Command, args []string) error { 264 return wrapError(doInit(args, config)) 265 } 266 return cmd 267 }